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 | 
28 | 
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') . '