├── _config
└── .gitignore
├── .gitattributes
├── images
└── preview.png
├── .editorconfig
├── composer.json
├── README.md
├── LICENSE
├── client
├── css
│ └── seo-editor.css
└── javascript
│ └── seo-editor.js
├── src
├── Forms
│ ├── SEOEditorCSVLoader.php
│ └── GridField
│ │ ├── SEOEditorMetaTitleColumn.php
│ │ └── SEOEditorMetaDescriptionColumn.php
└── ModelAdmin
│ └── SEOEditorAdmin.php
└── .scrutinizer.yml
/_config/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | /.gitignore export-ignore
2 |
--------------------------------------------------------------------------------
/images/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/isobar-nz/silverstripe-seo-editor/HEAD/images/preview.png
--------------------------------------------------------------------------------
/.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 | [*.js]
14 | indent_size = 2
15 | indent_style = space
16 |
17 | [{*.yml,package.json}]
18 | indent_size = 2
19 |
20 | # The indent size used in the package.json file cannot be changed:
21 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "littlegiant/silverstripe-seo-editor",
3 | "type": "silverstripe-vendormodule",
4 | "description": "SEO Editor Administration for SilverStripe",
5 | "keywords": ["silverstripe", "seo", "editing", "metatitle", "metadescription"],
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Stevie Mayhew",
10 | "email": "stevie.mayhew@littlegiant.co.nz"
11 | }
12 | ],
13 | "require": {
14 | "silverstripe/cms": "^4.0",
15 | "kinglozzer/metatitle": "^2.0"
16 | },
17 | "extra": {
18 | "expose": [
19 | "client/css",
20 | "client/javascript"
21 | ]
22 | },
23 | "autoload": {
24 | "psr-4": {
25 | "LittleGiant\\SEOEditor\\": "src/"
26 | }
27 | },
28 | "prefer-stable": true,
29 | "minimum-stability": "dev"
30 | }
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SilverStripe SEO Editor
2 |
3 | SEO Editor interface to allow easy editing of SEO MetaTitle and MetaDescriptions for pages within a ModelAdmin
4 | interface.
5 |
6 | Edit inline, or download a CSV export and import changes.
7 |
8 | 
9 |
10 | ## Installation
11 |
12 | Installation via composer
13 |
14 | ```bash
15 | $ composer require littlegiant/silverstripe-seo-editor
16 | ```
17 |
18 | ## License
19 |
20 | SilverStripe SEO is released under the MIT license
21 |
22 | ## Contributing
23 |
24 | ### Code guidelines
25 |
26 | This project follows the standards defined in:
27 |
28 | * [PSR-0](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md)
29 | * [PSR-1](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md)
30 | * [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Little Giant
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/client/css/seo-editor.css:
--------------------------------------------------------------------------------
1 | .cms table.grid-field__table tbody td.col-Title {
2 | max-width: 8rem;
3 | }
4 |
5 | /**
6 | * @desc Remove the hover background color from the grid-field items, as they're distracting.
7 | */
8 | .cms tbody tr:hover,
9 | .cms .table tbody tr.even:hover,
10 | .cms .table tbody tr:hover {
11 | background-color: transparent;
12 | }
13 |
14 | /**
15 | * @desc Add a border, and box-shadow to the inputs in an error state.
16 | */
17 | .cms .seo-editor-error input,
18 | .cms .seo-editor-error input:hover,
19 | .cms .seo-editor-error input:focus,
20 | .cms .seo-editor-error textarea,
21 | .cms .seo-editor-error textarea:hover,
22 | .cms .seo-editor-error textarea:focus {
23 | border-color: #dc3545;
24 | box-shadow: 0 0 0 0.3em rgba(220, 53, 69, .5);
25 | }
26 |
27 | .seo-editor-errors {
28 | font-size: smaller;
29 | margin-top: 1em;
30 | color: #dc3545;
31 | }
32 |
33 | /* Hide the messages until the parent has the appropriate class */
34 | .seo-editor-message {
35 | display: none;
36 | }
37 |
38 | .seo-editor-error-too-short .seo-editor-message-too-short {
39 | display: block;
40 | }
41 |
42 | .seo-editor-error-too-long .seo-editor-message-too-long {
43 | display: block;
44 | }
45 |
46 | .seo-editor-error-duplicate .seo-editor-message-duplicate {
47 | display: block;
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/src/Forms/SEOEditorCSVLoader.php:
--------------------------------------------------------------------------------
1 | 'ID',
20 | ];
21 |
22 | /**
23 | * Update the columns needed when importing from CSV
24 | *
25 | * @param array $record
26 | * @param array $columnMap
27 | * @param BulkLoader_Result $results
28 | * @param bool $preview
29 | * @return bool|int
30 | */
31 | public function processRecord($record, $columnMap, &$results, $preview = false)
32 | {
33 | $page = $this->findExistingObject($record, $columnMap);
34 |
35 | if (!$page || !$page->exists()) {
36 | return false;
37 | }
38 |
39 | foreach ($record as $fieldName => $val) {
40 | if ($fieldName == 'MetaTitle' || $fieldName == 'MetaDescription') {
41 | $sqlValue = Convert::raw2sql($val);
42 | DB::query("UPDATE SiteTree SET {$fieldName} = '{$sqlValue}' WHERE ID = {$page->ID}");
43 | if ($page->isPublished()) {
44 | DB::query("UPDATE SiteTree_Live SET {$fieldName} = '{$sqlValue}' WHERE ID = {$page->ID}");
45 | }
46 | }
47 | }
48 |
49 | return $page->ID;
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/client/javascript/seo-editor.js:
--------------------------------------------------------------------------------
1 | (function ($) {
2 | $.entwine('ss', function ($) {
3 | $('.ss-seo-editor .ss-gridfield-item input, .ss-seo-editor .ss-gridfield-item textarea').entwine({
4 |
5 | onchange: function () {
6 | // kill the popup for form changes
7 | $('.cms-edit-form').removeClass('changed');
8 |
9 | var $this = $(this);
10 | var id = $this.closest('tr').attr('data-id');
11 | var url = $this.closest('.ss-gridfield').attr('data-url') + "/update" + $this.attr('data-name') + "/" + id;
12 | var data = encodeURIComponent($this.attr('name')) + '=' + encodeURIComponent($(this).val());
13 |
14 | $.noticeAdd({
15 | text: 'Saving changes',
16 | type: 'notice',
17 | stayTime: 5000,
18 | inEffect: {left: '0', opacity: 'show'},
19 | });
20 |
21 | $.post(
22 | url,
23 | data,
24 | function (data, textStatus) {
25 | $.noticeAdd({
26 | text: data.message,
27 | type: data.type,
28 | stayTime: 5000,
29 | inEffect: {left: '0', opacity: 'show'},
30 | });
31 |
32 | $this.closest('td').removeClass();
33 | if (data.errors.length) {
34 | $this.closest('td').addClass('seo-editor-error');
35 | data.errors.forEach(function (error) {
36 | $this.closest('td').addClass(error)
37 | });
38 | } else {
39 | $this.closest('td').addClass('seo-editor-valid');
40 | }
41 | },
42 | 'json'
43 | );
44 | }
45 | });
46 | });
47 | }(jQuery));
48 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/src/Forms/GridField/SEOEditorMetaTitleColumn.php:
--------------------------------------------------------------------------------
1 | getDisplayFields()}
31 | * @see {@link GridFieldDataColumns}.
32 | *
33 | * @param GridField $gridField
34 | * @param array - List reference of all column names.
35 | */
36 | public function augmentColumns($gridField, &$columns)
37 | {
38 | $columns[] = 'MetaTitle';
39 | }
40 |
41 | /**
42 | * Names of all columns which are affected by this component.
43 | *
44 | * @param GridField $gridField
45 | * @return array
46 | */
47 | public function getColumnsHandled($gridField)
48 | {
49 | return [
50 | 'MetaTitle',
51 | ];
52 | }
53 |
54 | /**
55 | * Attributes for the element containing the content returned by {@link getColumnContent()}.
56 | *
57 | * @param GridField $gridField
58 | * @param DataObject $record displayed in this row
59 | * @param string $columnName
60 | * @return array
61 | */
62 | public function getColumnAttributes($gridField, $record, $columnName)
63 | {
64 | $errors = $this->getErrors($record);
65 |
66 | return [
67 | 'class' => count($errors)
68 | ? 'seo-editor-error ' . implode(' ', $errors)
69 | : 'seo-editor-valid',
70 | ];
71 | }
72 |
73 | /**
74 | * HTML for the column, content of the
element.
75 | *
76 | * @param GridField $gridField
77 | * @param DataObject $record - Record displayed in this row
78 | * @param string $columnName
79 | * @return string - HTML for the column. Return NULL to skip.
80 | */
81 | public function getColumnContent($gridField, $record, $columnName)
82 | {
83 | $field = new TextField('MetaTitle');
84 | $value = $gridField->getDataFieldValue($record, $columnName);
85 | $value = $this->formatValue($gridField, $record, $columnName, $value);
86 | $field->setName($this->getFieldName($field->getName(), $gridField, $record));
87 | $field->setValue($value);
88 | $field->setAttribute('data-name', 'MetaTitle');
89 |
90 | return $field->Field() . $this->getErrorMessages();
91 | }
92 |
93 | /**
94 | * Additional metadata about the column which can be used by other components,
95 | * e.g. to set a title for a search column header.
96 | *
97 | * @param GridField $gridField
98 | * @param string $column
99 | * @return array - Map of arbitrary metadata identifiers to their values.
100 | */
101 | public function getColumnMetadata($gridField, $column)
102 | {
103 | return [
104 | 'title' => 'MetaTitle',
105 | ];
106 | }
107 |
108 | /**
109 | * Get the errors which are specific to MetaTitle
110 | *
111 | * @param DataObject $record
112 | * @return array
113 | */
114 | public function getErrors(DataObject $record)
115 | {
116 | $errors = [];
117 |
118 | if (strlen($record->MetaTitle) < 10) {
119 | $errors[] = 'seo-editor-error-too-short';
120 | }
121 | if (strlen($record->MetaTitle) > 55) {
122 | $errors[] = 'seo-editor-error-too-long';
123 | }
124 | if (strlen(SiteTree::get()->filter('MetaTitle', $record->MetaTitle)->count() > 1)) {
125 | $errors[] = 'seo-editor-error-duplicate';
126 | }
127 |
128 | return $errors;
129 | }
130 |
131 | /**
132 | * Return all the error messages
133 | *
134 | * @return string
135 | */
136 | public function getErrorMessages()
137 | {
138 | return ' ' .
139 | 'This title is too short. It should be greater than 10 characters long.' .
140 | 'This title is too long. It should be less than 55 characters long.' .
141 | 'This title is a duplicate. It should be unique.' .
142 | ' ';
143 | }
144 |
145 | /**
146 | * Add a class to the gridfield
147 | *
148 | * @param $gridField
149 | * @return array|void
150 | */
151 | public function getHTMLFragments($gridField)
152 | {
153 | $gridField->addExtraClass('ss-seo-editor');
154 | }
155 |
156 | /**
157 | * @param $name
158 | * @param GridField $gridField
159 | * @param DataObjectInterface $record
160 | * @return string
161 | */
162 | protected function getFieldName($name, GridField $gridField, DataObjectInterface $record)
163 | {
164 | return sprintf(
165 | '%s[%s][%s]', $gridField->getName(), $record->ID, $name
166 | );
167 | }
168 |
169 | /**
170 | * Return URLs to be handled by this grid field, in an array the same form as $url_handlers.
171 | * Handler methods will be called on the component, rather than the grid field.
172 | *
173 | * @param $gridField
174 | * @return array
175 | */
176 | public function getURLHandlers($gridField)
177 | {
178 | return [
179 | 'updateMetaTitle/$ID' => 'handleAction',
180 | ];
181 | }
182 |
183 | /**
184 | * @param $gridField
185 | * @param $request
186 | * @return string
187 | */
188 | public function handleAction($gridField, $request)
189 | {
190 | $data = $request->postVar($gridField->getName());
191 |
192 | foreach ($data as $id => $params) {
193 | $page = $gridField->getList()->byId((int)$id);
194 |
195 | foreach ($params as $fieldName => $val) {
196 | $sqlValue = Convert::raw2sql($val);
197 | $page->$fieldName = $sqlValue;
198 | DB::query("UPDATE SiteTree SET {$fieldName} = '{$sqlValue}' WHERE ID = {$page->ID}");
199 | if ($page->isPublished()) {
200 | DB::query("UPDATE SiteTree_Live SET {$fieldName} = '{$sqlValue}' WHERE ID = {$page->ID}");
201 | }
202 |
203 | return json_encode([
204 | 'type' => 'success',
205 | 'message' => $fieldName . ' saved',
206 | 'errors' => $this->getErrors($page),
207 | ]);
208 | }
209 | }
210 |
211 | return json_encode([
212 | 'type' => 'error',
213 | 'message' => 'An error occurred while saving',
214 | ]);
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/src/Forms/GridField/SEOEditorMetaDescriptionColumn.php:
--------------------------------------------------------------------------------
1 | getDisplayFields()}
31 | * @see {@link GridFieldDataColumns}.
32 | *
33 | * @param GridField $gridField
34 | * @param array - List reference of all column names.
35 | */
36 | public function augmentColumns($gridField, &$columns)
37 | {
38 | $columns[] = 'MetaDescription';
39 | }
40 |
41 | /**
42 | * Names of all columns which are affected by this component.
43 | *
44 | * @param GridField $gridField
45 | * @return array
46 | */
47 | public function getColumnsHandled($gridField)
48 | {
49 | return [
50 | 'MetaDescription',
51 | ];
52 | }
53 |
54 | /**
55 | * Attributes for the element containing the content returned by {@link getColumnContent()}.
56 | *
57 | * @param GridField $gridField
58 | * @param DataObject $record displayed in this row
59 | * @param string $columnName
60 | * @return array
61 | */
62 | public function getColumnAttributes($gridField, $record, $columnName)
63 | {
64 | $errors = $this->getErrors($record);
65 |
66 | return [
67 | 'class' => count($errors)
68 | ? 'seo-editor-error ' . implode(' ', $errors)
69 | : 'seo-editor-valid',
70 | ];
71 | }
72 |
73 |
74 | /**
75 | * HTML for the column, content of the | element.
76 | *
77 | * @param GridField $gridField
78 | * @param DataObject $record - Record displayed in this row
79 | * @param string $columnName
80 | * @return string - HTML for the column. Return NULL to skip.
81 | */
82 | public function getColumnContent($gridField, $record, $columnName)
83 | {
84 | $field = new TextareaField('MetaDescription');
85 | $value = $gridField->getDataFieldValue($record, $columnName);
86 | $value = $this->formatValue($gridField, $record, $columnName, $value);
87 | $field->setName($this->getFieldName($field->getName(), $gridField, $record));
88 | $field->setValue($value);
89 | $field->setAttribute('data-name', 'MetaDescription');
90 |
91 | return $field->Field() . $this->getErrorMessages();
92 | }
93 |
94 | /**
95 | * Additional metadata about the column which can be used by other components,
96 | * e.g. to set a title for a search column header.
97 | *
98 | * @param GridField $gridField
99 | * @param string $column
100 | * @return array - Map of arbitrary metadata identifiers to their values.
101 | */
102 | public function getColumnMetadata($gridField, $column)
103 | {
104 | return [
105 | 'title' => 'MetaDescription',
106 | ];
107 | }
108 |
109 | /**
110 | * Get the errors which are specific to MetaDescription
111 | *
112 | * @param DataObject $record
113 | * @return array
114 | */
115 | public function getErrors(DataObject $record)
116 | {
117 | $errors = [];
118 |
119 | if (strlen($record->MetaDescription) < 10) {
120 | $errors[] = 'seo-editor-error-too-short';
121 | }
122 | if (strlen($record->MetaDescription) > 160) {
123 | $errors[] = 'seo-editor-error-too-long';
124 | }
125 | if (strlen(SiteTree::get()->filter('MetaDescription', $record->MetaDescription)->count() > 1)) {
126 | $errors[] = 'seo-editor-error-duplicate';
127 | }
128 |
129 | return $errors;
130 | }
131 |
132 | /**
133 | * Return all the error messages
134 | *
135 | * @return string
136 | */
137 | public function getErrorMessages()
138 | {
139 | return ' ' .
140 | 'This meta description is too short. It should be greater than 10 characters long.' .
141 | 'This meta description is too long. It should be less than 160 characters long.' .
142 | 'This meta description is a duplicate. It should be unique.' .
143 | ' ';
144 | }
145 |
146 | /**
147 | * Add a class to the gridfield
148 | *
149 | * @param $gridField
150 | * @return array|void
151 | */
152 | public function getHTMLFragments($gridField)
153 | {
154 | $gridField->addExtraClass('ss-seo-editor');
155 | }
156 |
157 | /**
158 | * @param $name
159 | * @param GridField $gridField
160 | * @param DataObjectInterface $record
161 | * @return string
162 | */
163 | protected function getFieldName($name, GridField $gridField, DataObjectInterface $record)
164 | {
165 | return sprintf(
166 | '%s[%s][%s]', $gridField->getName(), $record->ID, $name
167 | );
168 | }
169 |
170 |
171 | /**
172 | * Return URLs to be handled by this grid field, in an array the same form as $url_handlers.
173 | * Handler methods will be called on the component, rather than the grid field.
174 | *
175 | * @param $gridField
176 | * @return array
177 | */
178 | public function getURLHandlers($gridField)
179 | {
180 | return [
181 | 'updateMetaDescription/$ID' => 'handleAction',
182 | ];
183 | }
184 |
185 | /**
186 | * @param $gridField
187 | * @param $request
188 | * @return string
189 | */
190 | public function handleAction($gridField, $request)
191 | {
192 | $data = $request->postVar($gridField->getName());
193 |
194 | foreach ($data as $id => $params) {
195 | $page = $gridField->getList()->byId((int)$id);
196 |
197 | foreach ($params as $fieldName => $val) {
198 | $sqlValue = Convert::raw2sql($val);
199 | $page->$fieldName = $sqlValue;
200 | DB::query("UPDATE SiteTree SET {$fieldName} = '{$sqlValue}' WHERE ID = {$page->ID}");
201 | if ($page->isPublished()) {
202 | DB::query("UPDATE SiteTree_Live SET {$fieldName} = '{$sqlValue}' WHERE ID = {$page->ID}");
203 | }
204 |
205 | return json_encode([
206 | 'type' => 'success',
207 | 'message' => $fieldName . ' saved',
208 | 'errors' => $this->getErrors($page),
209 | ]);
210 | }
211 | }
212 |
213 | return json_encode([
214 | 'type' => 'error',
215 | 'message' => 'An error occurred while saving',
216 | ]);
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/src/ModelAdmin/SEOEditorAdmin.php:
--------------------------------------------------------------------------------
1 | SEOEditorCSVLoader::class
59 | ];
60 |
61 | /**
62 | * @var array
63 | */
64 | private static $allowed_actions = [
65 | 'ImportForm'
66 | ];
67 |
68 | /**
69 | * @return HTTPResponse|string|void
70 | */
71 | public function init()
72 | {
73 | parent::init();
74 | Requirements::css('littlegiant/silverstripe-seo-editor: client/css/seo-editor.css');
75 | Requirements::javascript('littlegiant/silverstripe-seo-editor: client/javascript/seo-editor.js');
76 | }
77 |
78 | /**
79 | * @return SearchContext
80 | */
81 | public function getSearchContext()
82 | {
83 | $context = parent::getSearchContext();
84 |
85 | $fields = FieldList::create(
86 | TextField::create('Title', 'Title'),
87 | TextField::create('MetaTitle', 'MetaTitle'),
88 | TextField::create('MetaDescription', 'MetaDescription'),
89 | CheckboxField::create('DuplicatesOnly', 'Duplicates Only'),
90 | CheckboxField::create('RemoveEmptyMetaTitles', 'Remove Empty MetaTitles'),
91 | CheckboxField::create('RemoveEmptyMetaDescriptions', 'Remove Empty MetaDescriptions')
92 | );
93 |
94 | $context->setFields($fields);
95 | $filters = [
96 | 'Title' => new PartialMatchFilter('Title'),
97 | 'MetaTitle' => new PartialMatchFilter('MetaTitle'),
98 | 'MetaDescription' => new PartialMatchFilter('MetaDescription')
99 | ];
100 |
101 | $context->setFilters($filters);
102 |
103 | // Namespace fields, for easier detection if a search is present
104 | foreach ($context->getFields() as $field) $field->setName(sprintf('q[%s]', $field->getName()));
105 | foreach ($context->getFilters() as $filter) $filter->setFullName(sprintf('q[%s]', $filter->getFullName()));
106 |
107 | return $context;
108 | }
109 |
110 | /**
111 | * @param null $id
112 | * @param null $fields
113 | * @return Form
114 | */
115 | public function getEditForm($id = null, $fields = null)
116 | {
117 | $form = parent::getEditForm($id, $fields);
118 |
119 | $grid = $form->Fields()->dataFieldByName('SilverStripe-CMS-Model-SiteTree');
120 | $gridField = $grid->getConfig();
121 | if ($gridField) {
122 | $gridField->removeComponentsByType(GridFieldAddNewButton::class);
123 | $gridField->removeComponentsByType(GridFieldPrintButton::class);
124 | $gridField->removeComponentsByType(GridFieldEditButton::class);
125 | $gridField->removeComponentsByType(GridFieldExportButton::class);
126 | $gridField->removeComponentsByType(GridFieldDeleteAction::class);
127 |
128 | $gridField->getComponentByType(GridFieldDataColumns::class)->setDisplayFields(
129 | [
130 | 'ID' => 'ID',
131 | 'Title' => 'Title',
132 | ]
133 | );
134 |
135 | $gridField->addComponent(
136 | new GridFieldExportButton(
137 | 'before',
138 | [
139 | 'ID' => 'ID',
140 | 'Title' => 'Title',
141 | 'MetaTitle' => 'MetaTitle',
142 | 'MetaDescription' => 'MetaDescription'
143 | ]
144 | )
145 | );
146 |
147 | $gridField->addComponent(new SEOEditorMetaTitleColumn());
148 | $gridField->addComponent(new SEOEditorMetaDescriptionColumn());
149 | }
150 |
151 | return $form;
152 | }
153 |
154 | /**
155 | * @return Form|bool
156 | */
157 | public function ImportForm()
158 | {
159 | $form = parent::ImportForm();
160 | $modelName = $this->modelClass;
161 |
162 | if ($form) {
163 | $form->Fields()->removeByName("SpecFor{$modelName}");
164 | $form->Fields()->removeByName("EmptyBeforeImport");
165 | }
166 |
167 | return $form;
168 | }
169 |
170 | /**
171 | * Get the list for the GridField
172 | *
173 | * @return SS_List
174 | */
175 | public function getList()
176 | {
177 | $list = parent::getList();
178 | $params = $this->request->requestVar('q');
179 |
180 | if (isset($params['RemoveEmptyMetaTitles']) && $params['RemoveEmptyMetaTitles']) {
181 | $list = $this->removeEmptyAttributes($list, 'MetaTitle');
182 | }
183 |
184 | if (isset($params['RemoveEmptyMetaDescriptions']) && $params['RemoveEmptyMetaDescriptions']) {
185 | $list = $this->removeEmptyAttributes($list, 'MetaDescription');
186 | }
187 |
188 | $list = $this->markDuplicates($list);
189 |
190 | if (isset($params['DuplicatesOnly']) && $params['DuplicatesOnly']) {
191 | $list = $list->filter('IsDuplicate', true);
192 | }
193 |
194 | $list = $list->sort('ID');
195 |
196 | return $list;
197 | }
198 |
199 | /**
200 | * Mark duplicate attributes
201 | *
202 | * @param SS_List $list
203 | * @return SS_List
204 | */
205 | private function markDuplicates($list)
206 | {
207 | $duplicates = $this->findDuplicates($list, 'MetaTitle')->map('ID', 'ID')->toArray();
208 | $duplicateList = new ArrayList();
209 |
210 | foreach ($list as $item) {
211 | if (in_array($item->ID, $duplicates)) {
212 | $item->IsDuplicate = true;
213 | $duplicateList->push($item);
214 | }
215 | }
216 |
217 | $duplicates = $this->findDuplicates($list, 'MetaDescription')->map('ID', 'ID')->toArray();
218 | foreach ($list as $item) {
219 | if (in_array($item->ID, $duplicates)) {
220 | $item->IsDuplicate = true;
221 | if (!$list->byID($item->ID)) {
222 | $duplicateList->push($item);
223 | }
224 | }
225 | }
226 |
227 | $duplicateList->merge($list);
228 | $duplicateList->removeDuplicates();
229 | return $duplicateList;
230 | }
231 |
232 | /**
233 | * Find duplicate attributes within a list
234 | *
235 | * @param SS_List $list
236 | * @param string $type
237 | * @return SS_List
238 | */
239 | private function findDuplicates(SS_List $list, $type)
240 | {
241 | $pageAttributes = $list->map('ID', $type)->toArray();
242 |
243 | $potentialDuplicateAttributes = array_unique(
244 | array_diff_assoc(
245 | $pageAttributes,
246 | array_unique($pageAttributes)
247 | )
248 | );
249 | $duplicateAttributes = array_filter($pageAttributes, function ($value) use ($potentialDuplicateAttributes) {
250 | return in_array($value, $potentialDuplicateAttributes);
251 | });
252 |
253 | if (!count($duplicateAttributes)) {
254 | return $list;
255 | }
256 |
257 | return $list->filter([
258 | 'ID' => array_keys($duplicateAttributes),
259 | ]);
260 | }
261 |
262 | /**
263 | * Remove pages with empty attributes
264 | *
265 | * @param SS_List $list
266 | * @param string $type
267 | * @return SS_List
268 | */
269 | private function removeEmptyAttributes(SS_List $list, $type)
270 | {
271 | $pageAttributes = $list->map('ID', $type)->toArray();
272 |
273 | $emptyAttributess = array_map(function ($value) {
274 | return $value == '';
275 | }, $pageAttributes);
276 |
277 | if (!count($emptyAttributess)) {
278 | return $list;
279 | }
280 |
281 | return $list->filter([
282 | 'ID:not' => array_keys(
283 | array_filter($emptyAttributess, function ($value) {
284 | return $value == 1;
285 | })
286 | )
287 | ]);
288 | }
289 |
290 |
291 | }
292 |
--------------------------------------------------------------------------------
|