├── lang ├── _manifest_exclude ├── de.yml ├── es.yml ├── en.yml └── nl.yml ├── 1.png ├── 2.png ├── .gitattributes ├── client ├── images │ ├── star.png │ ├── screen1.png │ ├── screen2.png │ └── screen3.png ├── js │ └── seo.js └── css │ └── seo.css ├── _config └── config.yml ├── .editorconfig ├── templates ├── SeoBreadcrumbsTemplate.ss ├── Hubertusanton │ └── SilverStripeSeo │ │ └── Includes │ │ └── SocialTags.ss └── SimplePageSubjectTest.ss ├── composer.json ├── LICENCE ├── src ├── SeoSiteConfig.php ├── GoogleSuggestField.php └── SeoObjectExtension.php ├── .scrutinizer.yml └── README.md /lang/_manifest_exclude: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hubertusanton/silverstripe-seo/HEAD/1.png -------------------------------------------------------------------------------- /2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hubertusanton/silverstripe-seo/HEAD/2.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore 2 | /.gitignore export-ignore 3 | /.travis.yml export-ignore 4 | -------------------------------------------------------------------------------- /client/images/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hubertusanton/silverstripe-seo/HEAD/client/images/star.png -------------------------------------------------------------------------------- /client/images/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hubertusanton/silverstripe-seo/HEAD/client/images/screen1.png -------------------------------------------------------------------------------- /client/images/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hubertusanton/silverstripe-seo/HEAD/client/images/screen2.png -------------------------------------------------------------------------------- /client/images/screen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hubertusanton/silverstripe-seo/HEAD/client/images/screen3.png -------------------------------------------------------------------------------- /_config/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: 'seoextensions' 3 | --- 4 | Page: 5 | extensions: 6 | - Hubertusanton\SilverStripeSeo\SeoObjectExtension 7 | SiteConfig: 8 | extensions: 9 | - Hubertusanton\SilverStripeSeo\SeoSiteConfig 10 | # Example of how to exclude extra page types from showing the SEO tab: 11 | # SeoObjectExtension: 12 | # excluded_page_types: 13 | # - 'SomePage' 14 | 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [{*.yml,package.json}] 14 | indent_size = 2 15 | 16 | # The indent size used in the package.json file cannot be changed: 17 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 18 | -------------------------------------------------------------------------------- /templates/SeoBreadcrumbsTemplate.ss: -------------------------------------------------------------------------------- 1 | <% if $Pages %> 2 | 16 | <% end_if %> -------------------------------------------------------------------------------- /templates/Hubertusanton/SilverStripeSeo/Includes/SocialTags.ss: -------------------------------------------------------------------------------- 1 | <% if not $SEOHideSocialData %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <% if $MetaDescription %> 9 | 10 | 11 | <% end_if %> 12 | 13 | 14 | 15 | 16 | <% if $SEOPreferedSocialImage.exists %> 17 | 18 | 19 | <% end_if %> 20 | <% end_if %> -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubertusanton/silverstripe-seo", 3 | "description": "SEO module for Silverstripe", 4 | "type": "silverstripe-vendormodule", 5 | "homepage": "http://github.com/hubertusanton/silverstripe-seo", 6 | "keywords": ["silverstripe", "seo"], 7 | "license": "BSD-3-Clause", 8 | "authors": [ 9 | { 10 | "name": "Bart van Irsel", 11 | "email": "bart@30.nl" 12 | } 13 | ], 14 | "support": { 15 | "issues": "http://github.com/hubertusanton/silverstripe-seo/issues" 16 | }, 17 | "require": { 18 | "silverstripe/framework": "^6" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Hubertusanton\\SilverStripeSeo\\": "src/" 23 | } 24 | }, 25 | "extra": { 26 | "expose": [ 27 | "client" 28 | ], 29 | "screenshots": [ 30 | "client/images/screen2.png", 31 | "client/images/screen3.png" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 2 | 3 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 4 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 5 | 6 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 7 | -------------------------------------------------------------------------------- /src/SeoSiteConfig.php: -------------------------------------------------------------------------------- 1 | 'Varchar(512)' 17 | ]; 18 | 19 | /** 20 | * updateCMSFields. 21 | * Update Silverstripe CMS Fields for SEO Module 22 | * 23 | * @param FieldList 24 | */ 25 | public function updateCMSFields(FieldList $fields) 26 | { 27 | if (Config::inst()->get(SeoObjectExtension::class, 'use_webmaster_tag')) { 28 | $fields->addFieldToTab( 29 | "Root.SEO", 30 | TextareaField::create( 31 | "GoogleWebmasterMetaTag", 32 | _t('SEO.SEOGoogleWebmasterMetaTag', 'Google webmaster meta tag') 33 | )->setRightTitle(_t( 34 | 'SEO.SEOGoogleWebmasterMetaTagRightTitle', 35 | "Full Google webmaster meta tag For example <meta name=\"google-site-verification\" content=\"hjhjhJHG12736JHGdfsdf\" />" 36 | )) 37 | ); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/js/seo.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | $.entwine('ss', function($){ 3 | $('.cms-edit-form textarea[name=MetaDescription]').entwine({ 4 | // Constructor: onmatch 5 | onkeyup : function() { 6 | set_preview_google_search_result(); 7 | }, 8 | onmatch: function() { 9 | set_preview_google_search_result(); 10 | } 11 | }); 12 | }); 13 | 14 | function set_preview_google_search_result() { 15 | 16 | var page_url_basehref = $('input[name="URLSegment"]').attr('data-prefix'), 17 | page_url_segment = $('input[name="URLSegment"]').val(), 18 | page_title = $('input[name="Title"').val(), 19 | page_metadata_title = $('input[name="MetaTitle"]').val(), 20 | page_metadata_description = $('textarea[name="MetaDescription"]').val(), 21 | siteconfig_title = $('#ss_siteconfig_title').html(); 22 | 23 | // build google search preview 24 | var google_search_title = (page_metadata_title ? page_metadata_title : page_title) + ' » ' + siteconfig_title; 25 | var google_search_url = page_url_basehref + page_url_segment; 26 | var google_search_description = page_metadata_description; 27 | 28 | if (google_search_description.length > 140) { 29 | google_search_description = google_search_description.substring(0, 140) + ' ...'; 30 | } 31 | 32 | var search_result_html = ''; 33 | search_result_html += '

' + google_search_title + '

'; 34 | search_result_html += '
' + google_search_url + '
'; 35 | search_result_html += '

' + google_search_description + '

'; 36 | 37 | $('#google_search_snippet').html(search_result_html); 38 | } 39 | 40 | })(jQuery); 41 | -------------------------------------------------------------------------------- /src/GoogleSuggestField.php: -------------------------------------------------------------------------------- 1 | getName()}"]').entwine({ 19 | // Constructor: onmatch 20 | onmatch : function() { 21 | 22 | $('input[name="{$this->getName()}"]').autocomplete({ 23 | source: function( request, response ) { 24 | $.ajax({ 25 | url: "//suggestqueries.google.com/complete/search", 26 | dataType: "jsonp", 27 | data: { 28 | client: 'firefox', 29 | q: request.term 30 | }, 31 | success: function( data ) { 32 | response( data[1] ); 33 | } 34 | }); 35 | }, 36 | minLength: 3 37 | }); 38 | 39 | }, 40 | }); 41 | }); 42 | 43 | })(jQuery); 44 | JS 45 | ); 46 | 47 | $this->addExtraClass('text'); 48 | 49 | return parent::Field($properties); 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client/css/seo.css: -------------------------------------------------------------------------------- 1 | #google_search_snippet { 2 | font-family: arial,sans-serif; 3 | width: 528px; 4 | background-color: #fff; 5 | padding: 4px 16px; 6 | margin-bottom: 16px; 7 | } 8 | 9 | #google_search_snippet h3 { 10 | font-style: normal; 11 | font-weight: bold; 12 | color: #1122CC; 13 | cursor: pointer; 14 | white-space: nowrap; 15 | font-size: medium; 16 | font-family: arial,sans-serif; 17 | text-decoration: underline; 18 | margin-bottom: 4px; 19 | overflow: hidden; 20 | } 21 | 22 | #google_search_snippet div.google_search_url { 23 | font-size: 14px !important; 24 | display: inline-block; 25 | color: #009933; 26 | font-style: normal; 27 | white-space: nowrap; 28 | font-family: arial,sans-serif; 29 | font-size: small; 30 | font-weight: normal; 31 | } 32 | 33 | /* hidden literal field for site title */ 34 | #ss_siteconfig_title { 35 | display: none; 36 | } 37 | 38 | #Root_SEO div#Options ul.ui-tabs-nav { 39 | float: left; 40 | } 41 | 42 | 43 | /* five-star css */ 44 | div#fivestar-widget { 45 | display: block; 46 | clear: both; 47 | } 48 | div#fivestar-widget div.star { 49 | float: left; 50 | height: 15px; 51 | overflow: hidden; 52 | width: 17px; 53 | text-indent: -999em; 54 | background: url("../images/star.png") no-repeat scroll 0 0 transparent; 55 | text-decoration: none; 56 | } 57 | div#fivestar-widget div.on { 58 | background-position: 0 -16px; 59 | } 60 | 61 | div#fivestar-widget div.on-half { 62 | background-position: 0 -32px; 63 | } 64 | 65 | 66 | h5.seo_score { 67 | margin-bottom: 4px; 68 | margin-top: 4px; 69 | } 70 | div.score_clear { 71 | clear: both; 72 | height: 10px; 73 | } 74 | 75 | ul#seo_score_tips { 76 | list-style: circle; 77 | margin: 14px; 78 | } 79 | ul#seo_score_tips li { 80 | color: #f00; 81 | margin-bottom: 4px; 82 | } 83 | 84 | 85 | .simple_pagesubject_yes { 86 | font-weight: bold; 87 | color: green; 88 | } 89 | .simple_pagesubject_no { 90 | font-weight: bold; 91 | color: red; 92 | } -------------------------------------------------------------------------------- /lang/de.yml: -------------------------------------------------------------------------------- 1 | de: 2 | SEO: 3 | SEOToggleTitle: 'Suchmaschinenoptimierung' 4 | SEOGoogleWebmasterCode: 'Google Webmaster Tools (GWT) Verificationscode' 5 | SEOGoogleSearchPreviewTitle: 'Vorschau Google-Suche' 6 | SEOMetaData: 'Metadaten für den Kopfbereich der Seite' 7 | SEOHelpAndScore: 'Hilfe und Seo-Score der Seite' 8 | SEOSocialNetworks: 'Soziale Netzwerke' 9 | SEOScoreTipPageSubjectDefined: 'Das Hauptkeyword (oder die Keyphrase) der Seite ist nicht definiert' 10 | SEOScoreTipPageSubjectInTitle: 'Das Hauptkeyword der Seite fehlt im Seitentitel (title)' 11 | SEOScoreTipPageSubjectInFirstParagraph: 'Das Hauptkeyword der Seite fehlt im ersten Absatz der Seite' 12 | SEOScoreTipPageSubjectInURL: 'Das Hauptkeyword fehlt in der URL der Seite' 13 | SEOScoreTipPageSubjectInMetaDescription: 'Das Hauptkeyword fehlt in der Seitenbeschreibung (descrition)' 14 | SEOScoreTipNumwordsContentOk: 'Der Inhalt der Seite ist zu gering. Erstellen Sie mindestens 250 Wörter Text, inhaltlich basierend auf dem hauptkeyword.' 15 | SEOScoreTipPageTitleLengthOk: 'Der Seitentitel ist zu kurz. Er sollte mindestens 40 Zeichen lang sein.' 16 | SEOScoreTipContentHasLinks: 'Der Inhalt der Seite verfügt über keine (ausgehenden) Links.' 17 | SEOScoreTipPageHasImages: 'Die Seite verfügt über keine Bilder.' 18 | SEOScoreTipContentHasSubtitles: 'Die Seitengliederung verfügt über keine Zwischenüberschriften (H2-H6)' 19 | SEOScore: 'Ihr SEO-Score' 20 | SEOScoreTips: 'SEO-Score Hilfe' 21 | SEOPageSubjectTitle: 'Subject of this page (required to view this page SEO score)' 22 | SEOYes: 'Ja' 23 | SEONo: 'Nein' 24 | SEOSubjectCheckFirstParagraph: 'Erster Absatz:' 25 | SEOSubjectCheckPageTitle: 'Seitentitel (title):' 26 | SEOSubjectCheckPageContent: 'Seiteninhalt:' 27 | SEOSubjectCheckPageURL: 'Seiten-URL:' 28 | SEOSubjectCheckPageMetaDescription: 'Seitenbeschreibung (description):' 29 | SEOSocialData: 'Soziale Daten' 30 | SEOHideSocialDataDescription: 'Soziale Daten vor HTML-Seiten verbergen?' 31 | SEOSocialType: 'Typ des sozialen Inhalts' 32 | SEOSocialImage: 'Bild zum Teilen in sozialen Medien' 33 | SEODefaultImage: 'Standardmäßig wird das Bild angezeigt, sofern verfügbar' -------------------------------------------------------------------------------- /templates/SimplePageSubjectTest.ss: -------------------------------------------------------------------------------- 1 |

<%t SEO.SEOSubjectCheckIntro 'Your page subject was found in:' %>

2 | 52 | -------------------------------------------------------------------------------- /lang/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | SEO: 3 | SEOToggleTitle: 'Optimización para motores de búsqueda' 4 | SEOGoogleWebmasterCode: 'Código de verificación de Google webmasters' 5 | SEOGoogleSearchPreviewTitle: 'Búsqueda de ejemplo en google' 6 | SEOMetaData: 'Meta datos' 7 | SEOHelpAndScore: 'Ayuda y puntaje SEO' 8 | SEOSocialNetworks: 'Redes sociales' 9 | SEOScoreTipPageSubjectDefined: 'Tema de la página no ha sido definida' 10 | SEOScoreTipPageSubjectInTitle: 'Tema de la página no está contenida en el título de esta página' 11 | SEOScoreTipPageSubjectInFirstParagraph: 'El tema de esta página no está presente en el primer párrafo del contenido de esta página' 12 | SEOScoreTipPageSubjectInURL: 'El tema de la página no está presente en la URL de esta página' 13 | SEOScoreTipPageSubjectInMetaDescription: 'El tema de esta página no está presente en la META descripción de esta página' 14 | SEOScoreTipNumwordsContentOk: 'El contenido de esta página es muy corto y no tiene suficientes palabras. Por favor crear contenido que posea al menos 250 palabras basadas en el tema de la página.' 15 | SEOScoreTipPageTitleLengthOk: 'El título de la página no es lo suficientemente largo y debe tener un largo de al menos 40 caracteres.' 16 | SEOScoreTipContentHasLinks: 'El contenido de esta págin no tiee ningún enlace saliente.' 17 | SEOScoreTipPageHasImages: 'El contenido de esta página no tiene ninguna imagen' 18 | SEOScoreTipContentHasSubtitles: 'El contenido de esta página no tiene ningún subtítulo' 19 | SEOScore: 'Puntaje SEO' 20 | SEOScoreTips: 'Tips para mejorar el puntaje SEO' 21 | SEOPageSubjectTitle: 'Tema de esta página (requerido para ver el puntaje SEO)' 22 | SEOYes: 'Si' 23 | SEONo: 'No' 24 | SEOSubjectCheckFirstParagraph: 'Primer Párrafo:' 25 | SEOSubjectCheckPageTitle: 'Título de la página:' 26 | SEOSubjectCheckPageContent: 'Contenido de la página:' 27 | SEOSubjectCheckPageURL: 'URL de la página:' 28 | SEOSubjectCheckPageMetaDescription: 'Descripción META de la página:' 29 | SEOSocialData: 'Datos sociales' 30 | SEOHideSocialDataDescription: '¿Ocultar datos sociales de páginas HTML?' 31 | SEOSocialType: 'Tipo de contenido social' 32 | SEOSocialImage: 'Imagen para compartir en las redes sociales' 33 | SEODefaultImage: 'El valor predeterminado es la imagen destacada, si está disponible.' 34 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | inherit: true 2 | 3 | checks: 4 | php: 5 | verify_property_names: true 6 | verify_argument_usable_as_reference: true 7 | verify_access_scope_valid: true 8 | useless_calls: true 9 | use_statement_alias_conflict: true 10 | variable_existence: true 11 | unused_variables: true 12 | unused_properties: true 13 | unused_parameters: true 14 | unused_methods: true 15 | unreachable_code: true 16 | too_many_arguments: true 17 | sql_injection_vulnerabilities: true 18 | simplify_boolean_return: true 19 | side_effects_or_types: true 20 | security_vulnerabilities: true 21 | return_doc_comments: true 22 | return_doc_comment_if_not_inferrable: true 23 | require_scope_for_properties: true 24 | require_scope_for_methods: true 25 | require_php_tag_first: true 26 | psr2_switch_declaration: true 27 | psr2_class_declaration: true 28 | property_assignments: true 29 | prefer_while_loop_over_for_loop: true 30 | precedence_mistakes: true 31 | precedence_in_conditions: true 32 | phpunit_assertions: true 33 | php5_style_constructor: true 34 | parse_doc_comments: true 35 | parameter_non_unique: true 36 | parameter_doc_comments: true 37 | param_doc_comment_if_not_inferrable: true 38 | optional_parameters_at_the_end: true 39 | one_class_per_file: true 40 | no_unnecessary_if: true 41 | no_trailing_whitespace: true 42 | no_property_on_interface: true 43 | no_non_implemented_abstract_methods: true 44 | no_error_suppression: true 45 | no_duplicate_arguments: true 46 | no_commented_out_code: true 47 | newline_at_end_of_file: true 48 | missing_arguments: true 49 | method_calls_on_non_object: true 50 | instanceof_class_exists: true 51 | foreach_traversable: true 52 | fix_line_ending: true 53 | fix_doc_comments: true 54 | duplication: true 55 | deprecated_code_usage: true 56 | deadlock_detection_in_loops: true 57 | code_rating: true 58 | closure_use_not_conflicting: true 59 | catch_class_exists: true 60 | blank_line_after_namespace_declaration: false 61 | avoid_multiple_statements_on_same_line: true 62 | avoid_duplicate_types: true 63 | avoid_conflicting_incrementers: true 64 | avoid_closing_tag: true 65 | assignment_of_null_return: true 66 | argument_type_checks: true 67 | 68 | filter: 69 | paths: [code/*, tests/*] 70 | -------------------------------------------------------------------------------- /lang/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | SEO: 3 | SEOToggleTitle: 'Search Engine Optimisation' 4 | SEOGoogleWebmasterCode: 'Google Webmaster verification code' 5 | SEOGoogleSearchPreviewTitle: 'Google Search preview' 6 | SEOMetaData: 'Meta data' 7 | SEOHelpAndScore: 'SEO Score and Tips' 8 | SEOSocialNetworks: 'Social Networks' 9 | SEOScoreTipPageSubjectDefined: 'Page subject is not defined for this page.' 10 | SEOScoreTipPageSubjectInTitle: 'Page subject is not in the title of this page.' 11 | SEOScoreTipPageSubjectInFirstParagraph: 'Page subject is not present in the first paragraph of this page''s content.' 12 | SEOScoreTipPageSubjectInURL: 'Page subject is not present in this page''s URL.' 13 | SEOScoreTipPageSubjectInMetaDescription: 'Page subject is not present in the meta description of this page.' 14 | SEOScoreTipNumwordsContentOk: 'The content of this page is too short and has too few words. Content of at least 250 words, based on the subject of this page, is recommended.' 15 | SEOScoreTipPageTitleLengthOk: 'The title of the page is not long enough and should have a length of at least 40 characters.' 16 | SEOScoreTipContentHasLinks: 'The content of this page does not have any (outgoing) links.' 17 | SEOScoreTipPageHasImages: 'The content of this page does not have any images.' 18 | SEOScoreTipContentHasSubtitles: 'The content of this page does not have any subtitles.' 19 | SEOScore: 'SEO Score' 20 | SEOScoreTips: 'SEO Score Tips' 21 | SEOPageSubjectTitle: 'Subject of this page (required to view this page''s SEO score)' 22 | SEOYes: 'Yes' 23 | SEONo: 'No' 24 | SEOSubjectCheckFirstParagraph: 'First paragraph:' 25 | SEOSubjectCheckPageTitle: 'Page title:' 26 | SEOSubjectCheckPageContent: 'Page content:' 27 | SEOSubjectCheckPageURL: 'Page URL:' 28 | SEOSubjectCheckPageMetaDescription: 'Page meta description:' 29 | SEOSubjectCheckIntro: 'Your page subject is found in:' 30 | SEOGoogleWebmasterMetaTag: 'Google webmaster meta tag' 31 | SEOGoogleWebmasterMetaTagRightTitle: 'Full Google webmaster meta tag For example <meta name="google-site-verification" content="hjhjhJHG12736JHGdfsdf" >' 32 | SEOScoreTipImagesHaveTitleTags: 'All images on this page do not have title tags' 33 | SEOSocialData: 'Social Data' 34 | SEOHideSocialDataDescription: 'Hide Social Data From Pages HTML?' 35 | SEOSocialType: 'Social Content Type' 36 | SEOSocialImage: 'Image to share on Social Media' 37 | SEODefaultImage: 'Defaults to featured image, if available' -------------------------------------------------------------------------------- /lang/nl.yml: -------------------------------------------------------------------------------- 1 | nl: 2 | SEO: 3 | SEOToggleTitle: 'Zoekmachine Optimalisatie' 4 | SEOGoogleWebmasterCode: 'Google webmaster verificatie code' 5 | SEOGoogleSearchPreviewTitle: 'Voorbeeld google search' 6 | SEOMetaData: 'Meta data' 7 | SEOHelpAndScore: 'Hulp en SEO Score' 8 | SEOSocialNetworks: 'Sociale Media' 9 | SEOScoreTipPageSubjectDefined: 'Pagina onderwerp is niet gedefinieerd voor deze pagina' 10 | SEOScoreTipPageSubjectInTitle: 'Pagina onderwerp staat niet in de titel van de pagina' 11 | SEOScoreTipPageSubjectInFirstParagraph: 'Pagina onderwerp komt niet voor in de eerste paragraaf van je pagina. Zorg ervoor dat het pagina onderwerp hierin staat.' 12 | SEOScoreTipPageSubjectInURL: 'Pagina onderwerp komt niet voor in de URL van je pagina' 13 | SEOScoreTipPageSubjectInMetaDescription: 'Pagina onderwerp komt niet voor in de Meta omschrijving van je pagina' 14 | SEOScoreTipNumwordsContentOk: 'De pagina inhoud bevat te weinig woorden, dit dient minumaal 250 woorden te zijn. Voeg meer inhoud toe, gebaseerd op het Pagina onderwerp.' 15 | SEOScoreTipPageTitleLengthOk: 'De titel van de pagina bevat te weinig tekens. Deze dient minimaal 40 tekens te bevatten.' 16 | SEOScoreTipContentHasLinks: 'De inoud van de pagina bevat geen (externe) links' 17 | SEOScoreTipPageHasImages: 'De inhoud van de pagina bevat geen afbeeldingen' 18 | SEOScoreTipContentHasSubtitles: 'De inhoud van de pagina bevat geen subtitels' 19 | SEOScore: 'SEO Score' 20 | SEOScoreTips: 'SEO Score Tips' 21 | SEOPageSubjectTitle: 'Onderwerp van deze pagina (vereist om SEO score te gebruiken)' 22 | SEOYes: 'Ja' 23 | SEONo: 'Nee' 24 | SEOSubjectCheckFirstParagraph: 'Eerste paragraaf:' 25 | SEOSubjectCheckPageTitle: 'Pagina titel:' 26 | SEOSubjectCheckPageContent: 'Pagina inhoud:' 27 | SEOSubjectCheckPageURL: 'Pagina URL:' 28 | SEOSubjectCheckPageMetaDescription: 'Pagina meta omschrijving:' 29 | SEOSubjectCheckIntro: 'Je pagina onderwerp is gevonden in:' 30 | SEOGoogleWebmasterMetaTag: 'Google webmaster meta tag' 31 | SEOGoogleWebmasterMetaTagRightTitle: 'Full Google webmaster meta tag. Bijvoorbeeld <meta name="google-site-verification" content="hjhjhJHG12736JHGdfsdf" >' 32 | SEOScoreTipImagesHaveTitleTags: 'Alle afbeeldingen op deze pagina hebben geen title tags' 33 | SEOSocialData: 'Sociale gegevens' 34 | SEOHideSocialDataDescription: 'Sociale gegevens verbergen in HTML?' 35 | SEOSocialType: 'Type sociale inhoud' 36 | SEOSocialImage: 'Afbeelding om te delen op sociale media' 37 | SEODefaultImage: 'Wordt standaard weergegeven op de afbeelding, indien beschikbaar' 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Silverstripe SEO Module 2 | 3 | ## Maintainer Contact 4 | 5 | * Bart van Irsel (Nickname: hubertusanton) 6 | * [Webium](http://www.webium.nl) 7 | 8 | 9 | ## Requirements 10 | 11 | * SilverStripe 5.* 12 | 13 | ## Documentation 14 | 15 | This module helps the administrator of the Silverstripe website in getting good results in search engines. 16 | A rating of the SEO of the current page helps the website editor creating good content around a subject 17 | of the page which can be defined using a google suggest field. 18 | 19 | The fields for meta data in pages will be moved to a SEO part by this module. 20 | This is done for giving a realtime preview on the google search result of the page. 21 | 22 | In seo.yml config file you can specify which classes will NOT use the module. 23 | By default every class extending Page will use the SEO module. 24 | 25 | ## Screenshots 26 | 27 | ![ScreenShot](https://raw.githubusercontent.com/hubertusanton/silverstripe-seo/master/1.png) 28 | ![ScreenShot](https://raw.githubusercontent.com/hubertusanton/silverstripe-seo/master/2.png) 29 | 30 | ## Installation 31 | Place the module dir in your website root and run /dev/build?flush=all 32 | 33 | ## TODO's for next versions 34 | 35 | * Create a google webmaster code config 36 | * Only check for outgoing links in content ommit links within site 37 | * Translations to other languages 38 | * Check for page subject usage in other pages 39 | * Check how many times the page subject has been used and give feedback to user 40 | * (Re)Calculate SEO Score in realtime with javascript without need to save first 41 | * Put html in cms defined in methods in template files 42 | * Check extra added db fields/ many_many DataObjects for SEO score and make this configurable 43 | 44 | ## License 45 | 46 | This module is published under BSD 2-clause license, although these are not in the actual classes, the license does apply: 47 | 48 | http://www.opensource.org/licenses/BSD-2-Clause 49 | 50 | Copyright (c) 2017, Bart van Irsel 51 | 52 | All rights reserved. 53 | 54 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 55 | 56 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 57 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 58 | 59 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 60 | -------------------------------------------------------------------------------- /src/SeoObjectExtension.php: -------------------------------------------------------------------------------- 1 | 'Website', 62 | 'article' => 'Article', 63 | 'book' => 'Book', 64 | 'profile' => 'Profile', 65 | 'music' => 'Music', 66 | 'video' => 'Video' 67 | ]; 68 | 69 | /** 70 | * Let the webmaster tag be edited by the CMS admin 71 | * 72 | * @config 73 | * @var boolean 74 | */ 75 | private static $use_webmaster_tag = true; 76 | 77 | private static $db = [ 78 | 'SEOPageSubject' => 'Varchar(256)', 79 | 'SEOSocialType' => 'Varchar', 80 | 'SEOHideSocialData' => 'Boolean' 81 | ]; 82 | 83 | private static $has_one = [ 84 | 'SEOSocialImage' => Image::class 85 | ]; 86 | 87 | private static $casting = [ 88 | 'SEOSocialTitle' => 'Varchar', 89 | 'SEOSocialLocale' => 'Varchar' 90 | ]; 91 | 92 | public $score_criteria = array( 93 | 'pagesubject_defined' => false, 94 | 'pagesubject_in_title' => false, 95 | 'pagesubject_in_firstparagraph' => false, 96 | 'pagesubject_in_url' => false, 97 | 'pagesubject_in_metadescription' => false, 98 | 'numwords_content_ok' => false, 99 | 'pagetitle_length_ok' => false, 100 | 'content_has_links' => false, 101 | 'page_has_images' => false, 102 | 'content_has_subtitles' => false, 103 | 'images_have_alt_tags' => false, 104 | 'images_have_title_tags' => false, 105 | ); 106 | 107 | public $seo_score = 0; 108 | 109 | public $seo_score_tips = ''; 110 | 111 | 112 | /** 113 | * getSEOScoreTips. 114 | * Get array of tips translated in current locale 115 | * 116 | * @param none 117 | * @return array $score_criteria_tips Associative array with translated tips 118 | */ 119 | public function getSEOScoreTips() { 120 | 121 | $score_criteria_tips = array( 122 | 'pagesubject_defined' => _t('SEO.SEOScoreTipPageSubjectDefined', 'Page subject is not defined for page'), 123 | 'pagesubject_in_title' => _t('SEO.SEOScoreTipPageSubjectInTitle', 'Page subject is not in the title of this page'), 124 | 'pagesubject_in_firstparagraph' => _t('SEO.SEOScoreTipPageSubjectInFirstParagraph', 'Page subject is not present in the first paragraph of the content of this page'), 125 | 'pagesubject_in_url' => _t('SEO.SEOScoreTipPageSubjectInURL', 'Page subject is not present in the URL of this page'), 126 | 'pagesubject_in_metadescription' => _t('SEO.SEOScoreTipPageSubjectInMetaDescription', 'Page subject is not present in the meta description of the page'), 127 | 'numwords_content_ok' => _t('SEO.SEOScoreTipNumwordsContentOk', 'The content of this page is too short and does not have enough words. Please create content of at least 300 words based on the Page subject.'), 128 | 'pagetitle_length_ok' => _t('SEO.SEOScoreTipPageTitleLengthOk', 'The title of the page is not long enough and should have a length of at least 40 characters.'), 129 | 'content_has_links' => _t('SEO.SEOScoreTipContentHasLinks', 'The content of this page does not have any (outgoing) links.'), 130 | 'page_has_images' => _t('SEO.SEOScoreTipPageHasImages', 'The content of this page does not have any images.'), 131 | 'content_has_subtitles' => _t('SEO.SEOScoreTipContentHasSubtitles', 'The content of this page does not have any subtitles'), 132 | 'images_have_alt_tags' => _t('SEO.SEOScoreTipImagesHaveAltTags', 'All images on this page do not have alt tags'), 133 | 'images_have_title_tags' => _t('SEO.SEOScoreTipImagesHaveTitleTags', 'All images on this page do not have title tags') 134 | ); 135 | 136 | return $score_criteria_tips; 137 | } 138 | 139 | /** 140 | * updateCMSFields. 141 | * Update Silverstripe CMS Fields for SEO Module 142 | * 143 | * @param FieldList 144 | * @return none 145 | */ 146 | public function updateCMSFields(FieldList $fields) 147 | { 148 | // exclude SEO tab from some pages 149 | $excluded = Config::inst()->get(self::class, 'excluded_page_types'); 150 | 151 | if ($excluded) { 152 | if (in_array($this->owner->getClassName(), $excluded)) { 153 | return; 154 | } 155 | } 156 | 157 | Requirements::css('hubertusanton/silverstripe-seo:client/css/seo.css'); 158 | Requirements::javascript('hubertusanton/silverstripe-seo:client/js/seo.js'); 159 | 160 | // better do this below in some init method? : 161 | $this->getSEOScoreCalculation(); 162 | $this->setSEOScoreTipsUL(); 163 | 164 | // lets create a new tab on top 165 | $fields->addFieldsToTab( 166 | 'Root.SEO', 167 | [ 168 | LiteralField::create('googlesearchsnippetintro', '

' . _t('SEO.SEOGoogleSearchPreviewTitle', 'Preview google search') . '

'), 169 | LiteralField::create('googlesearchsnippet', '
'), 170 | LiteralField::create('siteconfigtitle', '
' . $this->owner->getSiteConfig()->Title . '
'), 171 | ] 172 | ); 173 | 174 | // move Metadata field from Root.Main to SEO tab for visualising direct impact on search result 175 | $fields->removeFieldsFromTab( 176 | 'Root.Main', 177 | [ 178 | 'Metadata', 179 | 'SEOSocialType', 180 | 'SEOHideSocialData', 181 | 'SEOSocialImage' 182 | ] 183 | ); 184 | 185 | $fields->addFieldsToTab( 186 | 'Root.SEO', 187 | [ 188 | TextareaField::create("MetaDescription", $this->owner->fieldLabel('MetaDescription')) 189 | ->setRightTitle( 190 | _t( 191 | 'SiteTree.METADESCHELP', 192 | "Search engines use this content for displaying search results (although it will not influence their ranking)." 193 | ) 194 | ) 195 | ->addExtraClass('help'), 196 | GoogleSuggestField::create("SEOPageSubject", _t('SEO.SEOPageSubjectTitle', 'Subject of this page (required to view this page SEO score)')), 197 | LiteralField::create('', '

' . 198 | _t( 199 | 'SEO.SEOSaveNotice', 200 | "After making changes save this page to view the updated SEO score" 201 | ) . '

'), 202 | LiteralField::create('ScoreTitle', '

' . _t('SEO.SEOScore', 'SEO Score') . '

'), 203 | LiteralField::create('Score', $this->getHTMLStars()), 204 | LiteralField::create('ScoreClear', '
') 205 | ] 206 | ); 207 | 208 | if ($this->checkPageSubjectDefined()) { 209 | $fields->addFieldToTab( 210 | 'Root.SEO', 211 | LiteralField::create('SimplePageSubjectCheckValues', $this->getHTMLSimplePageSubjectTest()) 212 | ); 213 | } 214 | 215 | if ($this->seo_score < 12) { 216 | $fields->addFieldsToTab( 217 | 'Root.SEO', 218 | [ 219 | LiteralField::create('ScoreTipsTitle', '

' . _t('SEO.SEOScoreTips', 'SEO Score Tips') . '

'), 220 | LiteralField::create('ScoreTips', $this->seo_score_tips) 221 | ] 222 | ); 223 | } 224 | 225 | // Add Social settings 226 | $fields->addFieldToTab( 227 | 'Root.SEO', 228 | ToggleCompositeField::create( 229 | 'SEOSocialData', 230 | _t('SEO.SEOSocialData', "Social Data"), 231 | [ 232 | $this 233 | ->getOwner() 234 | ->dbObject('SEOHideSocialData') 235 | ->scaffoldFormField() 236 | ->setTitle(_t('SEO.SEOHideSocialDataDescription', 'Hide Social Data From Pages HTML?')), 237 | DropdownField::create( 238 | 'SEOSocialType', 239 | _t('SEO.SEOSocialType', 'Social Content Type') 240 | )->setSource($this->config()->og_types), 241 | UploadField::create( 242 | 'SEOSocialImage', 243 | _t('SEO.SEOSocialImage', 'Image to share on Social Media') 244 | )->setDescription(_t('SEO.SEODefaultImage', 'Defaults to featured image, if available')) 245 | ] 246 | ) 247 | ); 248 | } 249 | 250 | /** 251 | * getHTMLStars. 252 | * Get html of stars rating in CMS, maximum score is 12 253 | * threshold 2 254 | * 255 | * @param none 256 | * @return String $html 257 | */ 258 | public function getHTMLStars() { 259 | 260 | $treshold_score = $this->seo_score - 2 < 0 ? 0 : $this->seo_score - 2; 261 | 262 | $num_stars = intval(ceil($treshold_score) / 2); 263 | 264 | $num_nostars = 5 - $num_stars; 265 | 266 | $html = '
'; 267 | 268 | for ($i = 1; $i <= $num_stars; $i++) { 269 | $html .= '
'; 270 | } 271 | if ($treshold_score % 2) { 272 | $html .= '
'; 273 | $num_nostars--; 274 | } 275 | for ($i = 1; $i <= $num_nostars; $i++) { 276 | $html .= '
'; 277 | } 278 | 279 | 280 | $html .= '
'; 281 | return $html; 282 | } 283 | 284 | /** 285 | * Get the current title for this page (to load into social tags) 286 | * First try to get the MetaTitle (if the field is available), if 287 | * not, fall back to title 288 | * 289 | * @return string 290 | */ 291 | public function getSEOSocialTitle() 292 | { 293 | // Try to use meta title field (if available) 294 | if (!empty($this->getOwner()->MetaTitle)) { 295 | return $this->getOwner()->MetaTitle; 296 | } 297 | 298 | return $this->getOwner()->Title; 299 | } 300 | 301 | /** 302 | * Get the current site locale. 303 | * 304 | * @return string 305 | */ 306 | public function getSEOSocialLocale() 307 | { 308 | return i18n::get_locale(); 309 | } 310 | 311 | /** 312 | * Attempt to find a suitable social image to use if one is not set. 313 | * By default try to see if this is a blog post and add the "Featured Image" 314 | * 315 | * @return Image 316 | */ 317 | public function getSEOPreferedSocialImage() 318 | { 319 | $owner = $this->getOwner(); 320 | $social_image = $owner->SEOSocialImage(); 321 | 322 | if (!$social_image->exists() && $owner->hasMethod('FeaturedImage') && $owner->FeaturedImage()->exists()) { 323 | return $owner->FeaturedImage(); 324 | } 325 | 326 | // Return the default expected result 327 | return $social_image; 328 | } 329 | 330 | /** 331 | * Hooks into MetaTags SiteTree method and adds additional 332 | * meta data for Sharing of this page on Social Media 333 | * 334 | * @return null 335 | */ 336 | public function MetaTags(&$tags) 337 | { 338 | $tags .= $this->getOwner()->renderWith('Hubertusanton\\SilverStripeSeo\\Includes\\SocialTags'); 339 | 340 | if (Config::inst()->get('SeoObjectExtension', 'use_webmaster_tag')) { 341 | $siteConfig = SiteConfig::current_site_config(); 342 | $tags .= $siteConfig->GoogleWebmasterMetaTag . "\n"; 343 | } 344 | } 345 | 346 | /** 347 | * Return a breadcrumb trail to this page. Excludes "hidden" pages 348 | * (with ShowInMenus=0). Adds extra microdata compared to 349 | * 350 | * @param int $maxDepth The maximum depth to traverse. 351 | * @param boolean $unlinked Do not make page names links 352 | * @param string $stopAtPageType ClassName of a page to stop the upwards traversal. 353 | * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0 354 | * @return string The breadcrumb trail. 355 | */ 356 | public function SeoBreadcrumbs($separator = '»', $addhome = true, $maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false) { 357 | $page = $this->owner; 358 | $pages = array(); 359 | 360 | while( 361 | $page 362 | && (!$maxDepth || count($pages) < $maxDepth) 363 | && (!$stopAtPageType || $page->ClassName != $stopAtPageType) 364 | ) { 365 | if($showHidden || $page->ShowInMenus || ($page->ID == $this->owner->ID)) { 366 | $pages[] = $page; 367 | } 368 | 369 | $page = $page->Parent; 370 | } 371 | // add homepage; 372 | if($addhome){ 373 | $pages[] = SiteTree::get_by_link(RootURLController::get_homepage_link()); 374 | } 375 | 376 | $template = new SSViewer('SeoBreadcrumbsTemplate'); 377 | 378 | return $template->process($this->owner->customise(new ArrayData(array( 379 | 'BreadcrumbSeparator' => $separator, 380 | 'AddHome' => $addhome, 381 | 'Pages' => new ArrayList(array_reverse($pages)) 382 | )))); 383 | } 384 | 385 | 386 | /** 387 | * getHTMLSimplePageSubjectTest. 388 | * Get html of tips for the Page Subject 389 | * 390 | * @param none 391 | * @return String $html 392 | */ 393 | public function getHTMLSimplePageSubjectTest() { 394 | 395 | return $this->owner->renderWith('SimplePageSubjectTest'); 396 | 397 | } 398 | 399 | /** 400 | * getSEOScoreCalculation. 401 | * Do SEO score calculation and set class Array score_criteria 12 corresponding assoc values 402 | * Also set class Integer seo_score with score 0-12 based on values which are true in score_criteria array 403 | * Do SEO score calculation and set class Array score_criteria 11 corresponding assoc values 404 | * Also set class Integer seo_score with score 0-12 based on values which are true in score_criteria array 405 | * 406 | * @param none 407 | * @return none, set class array score_criteria tips boolean 408 | */ 409 | public function getSEOScoreCalculation() { 410 | 411 | $this->score_criteria['pagesubject_defined'] = $this->checkPageSubjectDefined(); 412 | $this->score_criteria['pagesubject_in_title'] = $this->checkPageSubjectInTitle(); 413 | $this->score_criteria['pagesubject_in_firstparagraph'] = $this->checkPageSubjectInFirstParagraph(); 414 | $this->score_criteria['pagesubject_in_url'] = $this->checkPageSubjectInUrl(); 415 | $this->score_criteria['pagesubject_in_metadescription'] = $this->checkPageSubjectInMetaDescription(); 416 | $this->score_criteria['numwords_content_ok'] = $this->checkNumWordsContent(); 417 | $this->score_criteria['pagetitle_length_ok'] = $this->checkPageTitleLength(); 418 | $this->score_criteria['content_has_links'] = $this->checkContentHasLinks(); 419 | $this->score_criteria['page_has_images'] = $this->checkPageHasImages(); 420 | $this->score_criteria['content_has_subtitles'] = $this->checkContentHasSubtitles(); 421 | $this->score_criteria['images_have_alt_tags'] = $this->checkImageAltTags(); 422 | $this->score_criteria['images_have_title_tags'] = $this->checkImageTitleTags(); 423 | 424 | 425 | $this->seo_score = intval(array_sum($this->score_criteria)); 426 | } 427 | 428 | /** 429 | * setSEOScoreTipsUL. 430 | * Set SEO Score tips ul > li for SEO tips literal field, based on score_criteria 431 | * 432 | * @param none 433 | * @return none, set class string seo_score_tips with tips html 434 | */ 435 | public function setSEOScoreTipsUL() { 436 | 437 | $tips = $this->getSEOScoreTips(); 438 | $this->seo_score_tips = ''; 445 | 446 | } 447 | 448 | /** 449 | * checkContentHasSubtitles. 450 | * check if page Content has a h2's in it 451 | * 452 | * @param HTMLText $html String 453 | * @return DOMDocument Object 454 | */ 455 | private function createDOMDocumentFromHTML($html = null) { 456 | 457 | if ($html != null) { 458 | libxml_use_internal_errors(true); 459 | $dom = new DOMDocument; 460 | $dom->loadHTML($html); 461 | libxml_clear_errors(); 462 | libxml_use_internal_errors(false); 463 | return $dom; 464 | } 465 | } 466 | 467 | 468 | /** 469 | * checkPageSubjectInImageAlt. 470 | * Checks if image alt tags contain page subject 471 | * 472 | * @param none 473 | * @return boolean 474 | */ 475 | public function checkPageSubjectInImageAltTags() { 476 | 477 | $html = $this->getPageContent(); 478 | 479 | // for newly created page 480 | if ($html == '') { 481 | return false; 482 | } 483 | 484 | $dom = $this->createDOMDocumentFromHTML($html); 485 | 486 | $images = $dom->getElementsByTagName('img'); 487 | 488 | foreach($images as $image){ 489 | if($image->hasAttribute('alt') && $image->getAttribute('alt') != ''){ 490 | if (preg_match('/' . preg_quote($this->owner->SEOPageSubject, '/') . '/i', $image->getAttribute('alt'))) { 491 | return true; 492 | } 493 | } 494 | } 495 | 496 | return false; 497 | } 498 | 499 | 500 | /** 501 | * checkImageAltTags. 502 | * Checks if images in content have alt tags 503 | * 504 | * @param none 505 | * @return boolean 506 | */ 507 | private function checkImageAltTags() { 508 | 509 | $html = $this->getPageContent(); 510 | 511 | // for newly created page 512 | if ($html == '') { 513 | return false; 514 | } 515 | 516 | $dom = $this->createDOMDocumentFromHTML($html); 517 | 518 | $images = $dom->getElementsByTagName('img'); 519 | 520 | $imagesWithAltTags = 0; 521 | foreach($images as $image){ 522 | if($image->hasAttribute('alt') && $image->getAttribute('alt') != ''){ 523 | $imagesWithAltTags++; 524 | } 525 | } 526 | if($imagesWithAltTags == $images->length){ 527 | return true; 528 | } 529 | 530 | return false; 531 | } 532 | 533 | 534 | 535 | /** 536 | * checkImageTitleTags. 537 | * Checks if images in content have title tags 538 | * 539 | * @param none 540 | * @return boolean 541 | */ 542 | private function checkImageTitleTags() { 543 | 544 | $html = $this->getPageContent(); 545 | 546 | // for newly created page 547 | if ($html == '') { 548 | return false; 549 | } 550 | 551 | $dom = $this->createDOMDocumentFromHTML($html); 552 | 553 | $images = $dom->getElementsByTagName('img'); 554 | 555 | $imagesWithTitleTags = 0; 556 | foreach($images as $image){ 557 | if($image->hasAttribute('title') && $image->getAttribute('title') != ''){ 558 | //echo $image->getAttribute('title') . '
'; 559 | $imagesWithTitleTags++; 560 | } 561 | } 562 | 563 | if($imagesWithTitleTags == $images->length){ 564 | return true; 565 | } 566 | 567 | return false; 568 | } 569 | 570 | /** 571 | * checkPageSubjectDefined. 572 | * Checks if SEOPageSubject is defined 573 | * 574 | * @param none 575 | * @return boolean 576 | */ 577 | private function checkPageSubjectDefined() { 578 | return (trim($this->owner->SEOPageSubject != '')) ? true : false; 579 | } 580 | 581 | /** 582 | * checkPageSubjectInTitle. 583 | * Checks if defined PageSubject is present in the Page Title 584 | * 585 | * @param none 586 | * @return boolean 587 | */ 588 | public function checkPageSubjectInTitle() { 589 | if ($this->checkPageSubjectDefined()) { 590 | if (preg_match('/' . preg_quote($this->owner->SEOPageSubject, '/') . '/i', $this->owner->MetaTitle ?? '')) { 591 | return true; 592 | } elseif (preg_match('/' . preg_quote($this->owner->SEOPageSubject, '/') . '/i', $this->owner->Title ?? '')) { 593 | return true; 594 | } else { 595 | return false; 596 | } 597 | } 598 | return false; 599 | } 600 | 601 | /** 602 | * checkPageSubjectInContent. 603 | * Checks if defined PageSubject is present in the Page Content 604 | * 605 | * @param none 606 | * @return boolean 607 | */ 608 | public function checkPageSubjectInContent() { 609 | if ($this->checkPageSubjectDefined()) { 610 | if (preg_match('/' . preg_quote($this->owner->SEOPageSubject, '/') . '/i', $this->getPageContent())) { 611 | return true; 612 | } 613 | else { 614 | return false; 615 | } 616 | } 617 | return false; 618 | } 619 | 620 | /** 621 | * checkPageSubjectInFirstParagraph. 622 | * Checks if defined PageSubject is present in the Page Content's First Paragraph 623 | * 624 | * @param none 625 | * @return boolean 626 | */ 627 | public function checkPageSubjectInFirstParagraph() { 628 | if ($this->checkPageSubjectDefined()) { 629 | $first_paragraph = $this->owner->dbObject('Content')->FirstParagraph(); 630 | 631 | if (trim($first_paragraph != '')) { 632 | if (preg_match('/' . preg_quote($this->owner->SEOPageSubject, '/') . '/i', $first_paragraph)) { 633 | return true; 634 | } 635 | else { 636 | return false; 637 | } 638 | } 639 | } 640 | 641 | return false; 642 | } 643 | 644 | /** 645 | * checkPageSubjectInUrl. 646 | * Checks if defined PageSubject is present in the Page URLSegment 647 | * 648 | * @param none 649 | * @return boolean 650 | */ 651 | public function checkPageSubjectInUrl() { 652 | if ($this->checkPageSubjectDefined()) { 653 | 654 | $url_segment = $this->owner->URLSegment; 655 | $pagesubject_url_segment = $this->owner->generateURLSegment($this->owner->SEOPageSubject); 656 | 657 | if (preg_match('/' . preg_quote($pagesubject_url_segment, '/') . '/i', $url_segment)) { 658 | return true; 659 | } 660 | else { 661 | return false; 662 | } 663 | } 664 | return false; 665 | 666 | } 667 | 668 | /** 669 | * checkPageSubjectInMetaDescription. 670 | * Checks if defined PageSubject is present in the Page MetaDescription 671 | * 672 | * @param none 673 | * @return boolean 674 | */ 675 | public function checkPageSubjectInMetaDescription() { 676 | if ($this->checkPageSubjectDefined()) { 677 | 678 | if (preg_match('/' . preg_quote($this->owner->SEOPageSubject, '/') . '/i', $this->owner->MetaDescription)) { 679 | return true; 680 | } 681 | else { 682 | return false; 683 | } 684 | } 685 | return false; 686 | 687 | } 688 | 689 | /** 690 | * checkNumWordsContent. 691 | * Checks if the number of words of the Page Content is 250 692 | * 693 | * @param none 694 | * @return boolean 695 | */ 696 | private function checkNumWordsContent() { 697 | return ($this->getNumWordsContent() > 250) ? true : false; 698 | } 699 | 700 | /** 701 | * checkPageTitleLength. 702 | * check if length of Title and SiteConfig.Title has a minimal of 40 chars 703 | * 704 | * @param none 705 | * @return boolean 706 | */ 707 | private function checkPageTitleLength() { 708 | $site_title_length = strlen($this->owner->getSiteConfig()->Title ?? ''); 709 | // 3 is length of divider, this could all be done better ... 710 | return (($this->getNumCharsTitle() + 3 + $site_title_length) >= 40) ? true : false; 711 | } 712 | 713 | /** 714 | * checkContentHasLinks. 715 | * check if page Content has a href's in it 716 | * 717 | * @param none 718 | * @return boolean 719 | */ 720 | private function checkContentHasLinks() { 721 | 722 | $html = $this->getPageContent(); 723 | 724 | // for newly created page 725 | if ($html == '') { 726 | return false; 727 | } 728 | 729 | $dom = $this->createDOMDocumentFromHTML($html); 730 | 731 | $elements = $dom->getElementsByTagName('a'); 732 | return ($elements->length) ? true : false; 733 | 734 | } 735 | 736 | /** 737 | * checkPageHasImages. 738 | * check if page Content has a img's in it 739 | * 740 | * @param none 741 | * @return boolean 742 | */ 743 | private function checkPageHasImages() { 744 | 745 | $html = $this->getPageContent(); 746 | 747 | // for newly created page 748 | if ($html == '') { 749 | return false; 750 | } 751 | 752 | $dom = $this->createDOMDocumentFromHTML($html); 753 | 754 | $elements = $dom->getElementsByTagName('img'); 755 | return ($elements->length) ? true : false; 756 | 757 | } 758 | 759 | 760 | 761 | 762 | /** 763 | * checkContentHasSubtitles. 764 | * check if page Content has a h2's in it 765 | * 766 | * @param none 767 | * @return boolean 768 | */ 769 | private function checkContentHasSubtitles() { 770 | 771 | $html = $this->getPageContent(); 772 | 773 | // for newly created page 774 | if ($html == '') { 775 | return false; 776 | } 777 | 778 | $dom = $this->createDOMDocumentFromHTML($html); 779 | $elements = $dom->getElementsByTagName('h2'); 780 | 781 | return ($elements->length) ? true : false; 782 | 783 | } 784 | 785 | /** 786 | * getNumWordsContent. 787 | * get the number of words in the Page Content 788 | * 789 | * @param none 790 | * @return Integer Number of words in content 791 | */ 792 | 793 | public function getNumWordsContent() { 794 | return str_word_count((Convert::xml2raw($this->getPageContent()))); 795 | } 796 | 797 | /** 798 | * getNumCharsTitle. 799 | * get the number of characters in the Page Title 800 | * 801 | * @param none 802 | * @return Integer Number of chars of the title 803 | */ 804 | public function getNumCharsTitle() { 805 | return strlen($this->owner->Title ?? ''); 806 | } 807 | 808 | /** 809 | * getPageContent 810 | * function to get html content of page which SEO score is based on 811 | * (we use the same info as gets back from $Layout in template) 812 | * 813 | */ 814 | public function getPageContent() 815 | { 816 | static $cache = ''; 817 | 818 | if ($cache === '') { 819 | $session = []; 820 | 821 | if ($request = Injector::inst()->get(HTTPRequest::class)) { 822 | if (Controller::curr()) { 823 | $request = Controller::curr()->getRequest(); 824 | if ($request) { 825 | $session = $request->getSession(); 826 | } 827 | } 828 | 829 | $response = Director::test($this->owner->Link(), [], $session); 830 | 831 | if (!$response->isError()) { 832 | $cache = $response->getBody(); 833 | } 834 | } 835 | } 836 | 837 | return $cache; 838 | 839 | } 840 | } 841 | --------------------------------------------------------------------------------