├── README.md
└── MarkupSEO.module
/README.md:
--------------------------------------------------------------------------------
1 | MarkupSEO
2 | =========
3 |
4 | The all-in-one SEO solution for ProcessWire.
5 | More over here: https://processwire.com/talk/topic/8007-markupseo-the-all-in-one-seo-solution-for-processwire/
6 |
7 |
8 | ### Todo
9 |
10 | - Character count
11 |
--------------------------------------------------------------------------------
/MarkupSEO.module:
--------------------------------------------------------------------------------
1 | __('SEO'),
15 | 'version' => '0.8.7',
16 | 'summary' => __('The all-in-one SEO solution for ProcessWire.'),
17 | 'autoload' => true,
18 | 'requires' => array('ProcessWire>=2.4.0', 'PHP>=5.3.8')
19 | );
20 | }
21 |
22 |
23 | /**
24 | * Default configuration
25 | *
26 | */
27 | static public function getDefaultConfig() {
28 | return array(
29 | 'sitename' => '',
30 | 'author' => '',
31 | 'title' => '',
32 | 'titleSmart' => 'title',
33 | 'keywords' => '',
34 | 'keywordsSmart' => '',
35 | 'description' => '',
36 | 'descriptionSmart' => '',
37 | 'image' => '',
38 | 'imageSmart' => '',
39 | 'titleFormat' => '',
40 | 'canonical' => '',
41 | 'canonicalProtocol' => 'auto',
42 | 'robots' => '',
43 | 'custom' => '',
44 | 'includeGenerator' => 1,
45 | 'includeOpenGraph' => 1,
46 | 'includeTwitter' => 1,
47 | 'twitterUsername' => '',
48 | 'useParents' => 0,
49 | 'method' => 'auto',
50 | 'addWhitespace' => 1,
51 | 'includeTemplates' => array(),
52 | 'usePermission' => 0,
53 | 'googleAnalytics' => '',
54 | 'googleAnalyticsAnonymizeIP' => false,
55 | 'piwikAnalyticsUrl' => '',
56 | 'piwikAnalyticsIDSite' => '',
57 | 'hardLimit' => 0,
58 | 'titleLimit' => '60',
59 | 'descriptionLimit' => '160'
60 | );
61 | }
62 |
63 | static public function getDefaultFields() {
64 | return array(
65 | 'seo_tab',
66 | 'seo_title',
67 | 'seo_keywords',
68 | 'seo_description',
69 | 'seo_image',
70 | 'seo_robots',
71 | 'seo_custom',
72 | 'seo_canonical',
73 | 'seo_tab_END'
74 | );
75 | }
76 |
77 |
78 | /**
79 | * Populate default configuration (will be overwritten after constructor with user's own configuration)
80 | *
81 | */
82 | public function __construct() {
83 | foreach(self::getDefaultConfig() as $key => $value) {
84 | $this->$key = $value;
85 | }
86 | }
87 |
88 |
89 | /**
90 | * Initializing the hooks
91 | *
92 | */
93 | public function init() {
94 | // frontend hooks
95 | $this->addHookAfter("Page::render", $this, 'hookMethodAuto');
96 | }
97 |
98 | public function ready() {
99 | // backend hooks (Fix by @peterfoeng)
100 | if(@$this->page->process == 'ProcessPageEdit') {
101 | $editedPage = wire('pages')->get($this->config->input->get->id);
102 |
103 | if(!($editedPage instanceof NullPage)) {
104 | if(in_array($editedPage->template->name, $this->includeTemplates)) {
105 | $this->addHookAfter("ProcessPageEdit::buildFormContent", $this, 'hookCustomizeSeoTab');
106 | }
107 | }
108 | }
109 |
110 |
111 | // frontend hooks
112 | if($this->page->template != 'admin' && in_array($this->page->template->name, $this->includeTemplates)) {
113 | $this->addHookProperty("Page::seo", $this, 'hookFrontendPage');
114 | $this->addHookProperty("Config::seo", $this, 'hookFrontendConfig');
115 | }
116 | }
117 |
118 |
119 | /**
120 | * The hooking functions
121 | *
122 | */
123 | public function hookMethodAuto(HookEvent $event) {
124 | if($this->method != 'auto' || $this->page->template == 'admin' || !in_array($this->page->template->name, $this->includeTemplates)) return;
125 |
126 | // inject rendered meta tags into page
127 | $dataRendered = $this->page->seo->render;
128 | $event->return = str_ireplace("", $dataRendered.'', $event->return);
129 | }
130 |
131 |
132 | public function hookCustomizeSeoTab(HookEvent $e) {
133 | $page = wire('pages')->get($this->config->input->get->id);
134 | $configData = wire('modules')->getModuleConfigData($this);
135 |
136 | $titleField = (empty($configData['useParents']) or $configData['useParents'] == true) ? array('seo_title') : $configData['titleSmart'];
137 | $title = '';
138 | foreach ($titleField as $field) {
139 | if (!empty($page->$field)) {
140 | $title = $page->$field;
141 | break;
142 | }
143 | }
144 |
145 | if(!$e->return->get('seo_tab')) return;
146 |
147 | // Add google preview
148 | $field = $this->modules->get("InputfieldMarkup");
149 | $field->label = $this->_("Google Preview");
150 | $field->description = $this->_('Updates while you type in a title or description.');
151 | $field->value = $this->javascriptCounter($configData['hardLimit'], $configData['titleLimit'], $configData['descriptionLimit']).
152 | $this->javascriptAutocomplete().
153 | $this->javascriptGooglePreview().
154 | $this->getGooglePreview($title, $page->seo_canonical, $page->seo_description); // add javascript, too
155 | $e->return->insertAfter($field, $e->return->get('seo_tab'));
156 |
157 | }
158 |
159 |
160 | /**
161 | * Generates a google styled preview for the SEO Tab
162 | *
163 | */
164 | private function getGooglePreview($title, $url, $description) {
165 | $page = wire('pages')->get($this->config->input->get->id);
166 |
167 | $html = '
'.($title ? $title : 'Title').' ';
168 | $html .= ''.($url ? $url : $page->httpUrl).' ';
169 | $html .= ''.($description ? substr($description, 0, 155) : 'This is just a short description.').'.
';
170 | $html .= '';
177 |
178 | return $html;
179 | }
180 |
181 |
182 | /**
183 | * Returns an object including all the data (mixed config and page)
184 | *
185 | */
186 | public function hookFrontendPage(HookEvent $event) {
187 | // get page seo data
188 | $page = wire('pages')->get($event->object->id);
189 | $pageData = array();
190 | foreach($page->fields as $field) {
191 | if(preg_match("%^seo_(.*)%Uis", $field->name) && $field->name != 'seo_tab' && $field->name != 'seo_tab_END') {
192 | $pageData[str_replace('seo_', '', $field->name)] = $page->get($field->name);
193 | }
194 | }
195 |
196 | // get config seo data
197 | $configData = wire('modules')->getModuleConfigData($this);
198 |
199 | // override styles for multisite module, if it's installed
200 | if ($multiSite = $this->modules->getModule('Multisite', array('noPermissionCheck' => true, 'noInit' => true))) {
201 | if ($this->config->MultisiteDomains && array_key_exists($multiSite->domain, $this->config->MultisiteDomains) && array_key_exists('markupSEO', $this->config->MultisiteDomains[$multiSite->domain])) {
202 | $configDataOverrides = $this->config->MultisiteDomains[$multiSite->domain]['markupSEO']; // get special site data
203 | $configData = array_merge($configData, $configDataOverrides); // merge module config data with config data for special site
204 |
205 | // override data in module scope, otherwise the one from module settings will be used
206 | foreach(self::getDefaultConfig() as $key => $value) {
207 | if (array_key_exists($key, $configData)) $this->$key = $configData[$key];
208 | }
209 | }
210 | }
211 |
212 | foreach($pageData as $fieldKey => $fieldValue) {
213 | // if the field has content we can continue
214 | if($fieldValue != '') continue;
215 |
216 | // otherwise we try do add default content
217 | if($configData['useParents']) {
218 | // use parent data
219 | $pageData[$fieldKey] = $this->getParentValue($page, $fieldKey);
220 | } else {
221 | // use smart data or default data
222 | switch($fieldKey) {
223 | case 'title':
224 | if($configData['title']) {
225 | $pageData['title'] = $configData['title'];
226 | } elseif($configData['titleSmart'] && $page->get(implode('|', $configData['titleSmart'])) != '') {
227 | $pageData['title'] = strip_tags($page->get(implode('|', $configData['titleSmart'])));
228 | }
229 | break;
230 | case 'keywords':
231 | if($configData['keywords']) {
232 | $pageData['keywords'] = $configData['keywords'];
233 | } elseif($configData['keywordsSmart'] && $page->get(implode('|', $configData['keywordsSmart'])) != '') {
234 | $pageData['keywords'] = strip_tags($page->get(implode('|', $configData['keywordsSmart'])));
235 | }
236 | break;
237 | case 'description':
238 | if($configData['description']) {
239 | $pageData['description'] = $configData['description'];
240 | } elseif($configData['descriptionSmart'] && $page->get(implode('|', $configData['descriptionSmart'])) != '') {
241 | $pageData['description'] = strip_tags($page->get(implode('|', $configData['descriptionSmart'])));
242 | }
243 | break;
244 | case 'image':
245 | if($configData['imageSmart'] && count($page->get(implode('|', $configData['imageSmart']))) > 0) {
246 | $imageFields = $page->get(implode('|', $configData['imageSmart']));
247 | try {
248 | $pageData['image'] = $page->get(implode('|', $configData['imageSmart']))->first()->httpUrl;
249 | } catch (Exception $e) {
250 | $pageData['image'] = $page->get(implode('|', $configData['imageSmart']))->httpUrl;
251 | }
252 | }
253 | if(!$pageData['image'] && $configData['image']) {
254 | $pageData['image'] = $configData['image'];
255 | }
256 | break;
257 | }
258 |
259 | }
260 |
261 |
262 | }
263 |
264 |
265 | // add generator
266 | if($configData['includeGenerator']) $pageData['generator'] = 'ProcessWire '.wire('config')->version;
267 |
268 | // add author
269 | if($configData['author']) $pageData['author'] = $configData['author'];
270 |
271 | // add robots
272 | if($configData['robots'] && empty($pageData['robots'])) $pageData['robots'] = implode(', ', $configData['robots']);
273 |
274 | // add opengraph and canonical
275 | if(!$pageData['canonical']) {
276 | if($configData['canonicalProtocol'] == 'auto') {
277 | if(wire('config')->https == true) $configData['canonicalProtocol'] = 'https';
278 | }
279 |
280 | if($configData['canonicalProtocol'] == 'https') {
281 | $pageData['canonical'] = preg_replace('%^https?%i', 'https', $page->httpUrl);
282 | } else {
283 | $pageData['canonical'] = $page->httpUrl;
284 | }
285 |
286 | }
287 |
288 |
289 | if($configData['includeOpenGraph']) {
290 | $pageData['og:site_name'] = $configData['sitename'];
291 | $pageData['og:title'] = $pageData['title'];
292 | $pageData['og:url'] = $pageData['canonical'];
293 | $pageData['og:description'] = $pageData['description'];
294 | $pageData['og:type'] = 'website'; // TODO: Add more options
295 | $pageData['og:image'] = $pageData['image'];
296 | }
297 |
298 | // add twitter
299 | if($configData['includeTwitter']) {
300 | $pageData['twitter:card'] = 'summary'; // TODO: Add more options
301 | $pageData['twitter:site'] = '@'.$configData['twitterUsername'];
302 | $pageData['twitter:title'] = $pageData['title'];
303 | $pageData['twitter:url'] = $pageData['canonical'];
304 | $pageData['twitter:description'] = $pageData['description'];
305 | $pageData['twitter:image'] = $pageData['image'];
306 | }
307 |
308 |
309 | //add custom
310 | $pageData['custom'] = (array)$this->parseCustom($pageData['custom']);
311 | $configData['custom'] = (array)$this->parseCustom($configData['custom']);
312 |
313 | $pageData['custom'] = array_merge($configData['custom'], $pageData['custom']);
314 |
315 | foreach($pageData['custom'] as $key => $value) {
316 | $pageData[$key] = $value;
317 | }
318 |
319 | // add google analytics
320 | $googleAnalytics = '';
321 | if($configData['googleAnalytics']) {
322 | $googleAnalytics = '
323 |
324 |
340 | ';
341 | }
342 |
343 | $piwikAnalytics = '';
344 | if($configData['piwikAnalyticsUrl']) {
345 | $piwikAnalytics = '
346 |
356 | ';
357 | }
358 |
359 |
360 | // add "render"
361 | $rendered = '';
362 | foreach($pageData as $name => $content) {
363 |
364 | switch($name) {
365 | case 'custom':
366 | break;
367 | case 'title':
368 | if($this->titleFormat == '') break;
369 | $rendered .= ''.$this->parseTitle($page, $content).' '.PHP_EOL;
370 | break;
371 | case 'canonical':
372 | $rendered .= ' '.PHP_EOL;
373 | break;
374 | default:
375 | if (strstr($name, 'og:')) {
376 | $rendered .= ' '.PHP_EOL;
377 | } else {
378 | $rendered .= ' '.PHP_EOL;
379 | }
380 | break;
381 | }
382 |
383 | }
384 |
385 | // replace whitespaces and add analytics code
386 | $rendered .= preg_replace('/^\s+|\s+$/m', '', $googleAnalytics).PHP_EOL;
387 | $rendered .= preg_replace('/^\s+|\s+$/m', '', $piwikAnalytics).PHP_EOL;
388 |
389 | if($this->addWhitespace) {
390 | $renderedTmp = '';
391 | foreach(explode(PHP_EOL, $rendered) as $line) {
392 | $renderedTmp .= "\t".$line.PHP_EOL;
393 | }
394 | $rendered = $renderedTmp;
395 | }
396 |
397 | $pageData['render'] = $pageData['rendered'] = $rendered;
398 | $event->return = (object)$pageData;
399 | }
400 |
401 |
402 |
403 | private function getParentValue($page, $what = '') {
404 | if($page->id == 1) return '';
405 | $parent = $page->parent();
406 | if($parent->get('seo_'.$what) != '') return $parent->get('seo_'.$what);
407 | return $this->getParentValue($parent, $what);
408 | }
409 |
410 |
411 | private function parseTitle($page, $title) {
412 | $tags = array(
413 | 'title' => $title,
414 | 'sitename' => $this->sitename
415 | );
416 |
417 | $return = $this->titleFormat;
418 | foreach($tags as $tag => $value) {
419 | $return = str_replace("{".$tag."}", $value, $return);
420 | }
421 |
422 | return $return;
423 | }
424 |
425 |
426 | private function parseCustom($custom) {
427 | if(trim($custom) == '') return;
428 |
429 | $return = '';
430 | $lines = explode(PHP_EOL, $custom);
431 | foreach($lines as $line) {
432 | list($key, $value) = explode(':=', $line);
433 | $key = preg_replace('%[^A-Za-z0-9\-\.\:\_]+%', '', str_replace(' ', '-', trim($key)));
434 | $value = trim(wire('sanitizer')->text(html_entity_decode($value)));
435 | $return[$key] = $value;
436 | }
437 |
438 | return $return;
439 | }
440 |
441 |
442 | /**
443 | * Returns an object including all the data (only config/defaults)
444 | *
445 | */
446 | public function hookFrontendConfig(HookEvent $event) {
447 | $moduleData = wire('modules')->getModuleConfigData($this);
448 | $moduleData['custom'] = (array)$this->parseCustom($moduleData['custom']);
449 | $moduleData['robots'] = is_array($moduleData['robots']) ? implode(', ', $moduleData['robots']) : $moduleData['robots'];
450 |
451 | $moduleData = array_merge($moduleData, $moduleData['custom']);
452 |
453 | $event->return = (object)$moduleData;
454 | }
455 |
456 |
457 |
458 |
459 | /**
460 | * Create the modules setting page
461 | *
462 | */
463 | static public function getModuleConfigInputfields(array $data) {
464 | // since this is a static function, we can't use $this->modules, so get them from the global wire() function
465 | $modules = wire('modules');
466 | $input = wire('input');
467 | $fields = wire('fields');
468 | $tmpTemplates = wire('templates');
469 | foreach($tmpTemplates as $template) { // exclude system fields
470 | if($template->flags & Template::flagSystem) continue;
471 | $templates[] = $template;
472 | }
473 |
474 | // merge default config settings (custom values overwrite defaults)
475 | $defaults = self::getDefaultConfig();
476 | $data = array_merge($defaults, $data);
477 |
478 |
479 |
480 |
481 |
482 | // Add/remove seo fields from templates
483 | if($input->post->submit_save_module) {
484 |
485 | $includedTemplates = (array)$input->post->includeTemplates;
486 |
487 | foreach($templates as $template) {
488 | if(in_array($template->name, $includedTemplates)) {
489 | if($template->hasField('seo_tab')) {
490 | continue;
491 | } else {
492 | // add seo fields
493 | $seoFields = self::getDefaultFields();
494 | unset($seoFields[count($seoFields)-1]); // unset closing seo_tab_END
495 |
496 | foreach($fields as $seoField) {
497 | if(preg_match("%^seo_(.*)%Uis", $seoField->name) && !in_array($seoField->name, self::getDefaultFields())) {
498 | array_push($seoFields, $seoField->name);
499 | }
500 | }
501 |
502 | array_push($seoFields, 'seo_tab_END'); // add closing again
503 |
504 | //add fields to template
505 | foreach($seoFields as $templateField) {
506 | $template->fields->add($fields->get($templateField));
507 | }
508 | $template->fields->save();
509 | }
510 | } else {
511 | if($template->hasField('seo_tab')) {
512 | // remove seo fields
513 | foreach($template->fields as $templateField) {
514 | if(in_array($templateField->name, self::getDefaultFields())) {
515 | $template->fields->remove($templateField);
516 | }
517 | }
518 | $template->fields->save();
519 | } else {
520 | continue;
521 | }
522 | }
523 | }
524 |
525 | }
526 |
527 |
528 |
529 |
530 | // this is a container for fields, basically like a fieldset
531 | $form = new InputfieldWrapper();
532 |
533 | // Included fields
534 | $field = $modules->get("InputfieldAsmSelect");
535 | $field->name = "includeTemplates";
536 | $field->label = __("Templates with SEO tab");
537 | $field->description = __("Choose the templates which should get a SEO tab.");
538 | foreach($templates as $template) $field->addOption($template->name);
539 | $field->value = $data['includeTemplates'];
540 | $field->notes = __('Be careful with this field. If you remove an entry all of it\'s "seo_*" fields get deleted (including the data).');
541 | $form->add($field);
542 |
543 |
544 | // Author
545 | $field = $modules->get("InputfieldText");
546 | $field->name = "author";
547 | $field->label = __("Author");
548 | $field->description = "";
549 | $field->value = $data['author'];
550 | $form->add($field);
551 |
552 | // Site Name
553 | $field = $modules->get("InputfieldText");
554 | $field->name = "sitename";
555 | $field->label = __("Site Name");
556 | $field->description = "";
557 | $field->value = $data['sitename'];
558 | $form->add($field);
559 |
560 |
561 | $fieldset = $modules->get("InputfieldFieldset");
562 | $fieldset->label = "Advanced";
563 | $fieldset->collapsed = Inputfield::collapsedNo;
564 | $form->add($fieldset);
565 |
566 | $field = $modules->get("InputfieldCheckbox");
567 | $field->name = "useParents";
568 | $field->label = __("Use parent's values if empty?");
569 | $field->description = __("Parent's values will be used as default if you don't define page specific meta data and leave the fields below blank and don't choose smart fields.");
570 | $field->checked = $data['useParents'];
571 | $fieldset->add($field);
572 |
573 | // Default Title
574 | $field = $modules->get("InputfieldText");
575 | $field->name = "title";
576 | $field->label = __("Title");
577 | $field->description = __("A good length for a title is 60 characters.");
578 | $field->value = $data['title'];
579 | $field->columnWidth = '50%';
580 | $field->showIf = 'useParents=0';
581 | $field->set('class', 'seo_autocomplete');
582 | $fieldset->add($field);
583 |
584 | $field = $modules->get("InputfieldAsmSelect");
585 | $field->name = "titleSmart";
586 | $field->label = __("Smart Title");
587 | $field->description = __("We will use those fields (in this particular order) if you don't fill in the title field");
588 | foreach($fields as $selectField) $field->addOption($selectField->name);
589 | $field->value = $data['titleSmart'];
590 | $field->columnWidth = '50%';
591 | $field->showIf = 'useParents=0';
592 | $fieldset->add($field);
593 |
594 | // Default Keywords
595 | $field = $modules->get("InputfieldText");
596 | $field->name = "keywords";
597 | $field->label = __("Keywords");
598 | $field->description = "";
599 | $field->value = $data['keywords'];
600 | $field->columnWidth = '50%';
601 | $field->showIf = 'useParents=0';
602 | $fieldset->add($field);
603 |
604 | $field = $modules->get("InputfieldAsmSelect");
605 | $field->name = "keywordsSmart";
606 | $field->label = __("Smart Keywords");
607 | $field->description = __("We will use those fields (in this particular order) if you don't fill in the keywords field");
608 | foreach($fields as $selectField) $field->addOption($selectField->name);
609 | $field->value = $data['keywordsSmart'];
610 | $field->columnWidth = '50%';
611 | $field->showIf = 'useParents=0';
612 | $fieldset->add($field);
613 |
614 | // Default Description
615 | $field = $modules->get("InputfieldText");
616 | $field->name = "description";
617 | $field->label = __("Description");
618 | $field->description = __("A good length for a description is 160 characters.");
619 | $field->value = $data['description'];
620 | $field->columnWidth = '50%';
621 | $field->showIf = 'useParents=0';
622 | $field->set('class', 'seo_autocomplete');
623 | $fieldset->add($field);
624 |
625 | $field = $modules->get("InputfieldAsmSelect");
626 | $field->name = "descriptionSmart";
627 | $field->label = __("Smart Description");
628 | $field->description = __("We will use those fields (in this particular order) if you don't fill in the description field");
629 | foreach($fields as $selectField) $field->addOption($selectField->name);
630 | $field->value = $data['descriptionSmart'];
631 | $field->columnWidth = '50%';
632 | $field->showIf = 'useParents=0';
633 | $fieldset->add($field);
634 |
635 | // Default Image
636 | $field = $modules->get("InputfieldText");
637 | $field->name = "image";
638 | $field->label = __("Image");
639 | $field->description = "";
640 | $field->value = $data['image'];
641 | $field->columnWidth = '50%';
642 | $field->showIf = 'useParents=0';
643 | $fieldset->add($field);
644 |
645 | $field = $modules->get("InputfieldAsmSelect");
646 | $field->name = "imageSmart";
647 | $field->label = __("Smart Image");
648 | $field->description = __("We will use the first image from the specified image field.");
649 | foreach($fields->find('type=FieldtypeImage|FieldtypeCroppableImage|FieldtypeImageFocusArea') as $selectField) $field->addOption($selectField->name);
650 | $field->value = $data['imageSmart'];
651 | $field->columnWidth = '50%';
652 | $field->showIf = 'useParents=0';
653 | $fieldset->add($field);
654 |
655 |
656 | // title format
657 | $field = $modules->get("InputfieldText");
658 | $field->name = "titleFormat";
659 | $field->label = __("Title Format");
660 | $field->description = __("Use this field to adjust the title format. If left empty the tag won't be included.");
661 | $field->value = $data['titleFormat'];
662 | $field->columnWidth = '50%';
663 | $field->notes = __('You can use: {title}, {sitename}');
664 | $fieldset->add($field);
665 |
666 |
667 | // https or http format
668 | $field = $modules->get("InputfieldSelect");
669 | $field->name = "canonicalProtocol";
670 | $field->label = __("Protocol for canonical links");
671 | $field->description = __("Choose if you always want to use \"https\" or \"http\" or if you want automatic detection.");
672 | $field->notes = __('Automatic detection will check if $config->https is set to true.');
673 | $field->value = $data['canonicalProtocol'];
674 | $field->addOption('auto', 'Automatically');
675 | $field->addOption('http', 'http');
676 | $field->addOption('https', 'https');
677 | $field->required = true;
678 | $field->columnWidth = '50%';
679 | $fieldset->add($field);
680 |
681 |
682 | // Custom
683 | $field = $modules->get("InputfieldTextarea");
684 | $field->name = "custom";
685 | $field->label = __("Custom");
686 | $field->description = __("If you want to add other meta tags, you can do it here.");
687 | $field->value = $data['custom'];
688 | $field->notes = __('Please use this schema: name := content. One tag per line. Special characters are only allowed in the content part and get converted to HTML.');
689 | $fieldset->add($field);
690 |
691 |
692 | // Robots
693 | $field = $modules->get("InputfieldAsmSelect");
694 | $field->name = "robots";
695 | $field->label = __("Robots");
696 | $field->description = __("The robots settings will tell search engine which data they are allowed to include/index.");
697 | $field->addOption('index');
698 | $field->addOption('follow');
699 | $field->addOption('archive');
700 | $field->addOption('noindex');
701 | $field->addOption('nofollow');
702 | $field->addOption('noarchive');
703 | $field->addOption('nosnippet');
704 | $field->addOption('noodp');
705 | $field->addOption('noydir');
706 | $field->value = $data['robots'];
707 | $fieldset->add($field);
708 |
709 | // Limits
710 | $field = $modules->get("InputfieldCheckbox");
711 | $field->name = "hardLimit";
712 | $field->label = __("Enforce hard limits?");
713 | $field->description = __('This toggles the hard limit for any defined limits. If checked it prevents more than the defined characters to be entered.');
714 | $field->checked = $data['hardLimit'];
715 | $field->columnWidth = '33%';
716 | $fieldset->add($field);
717 |
718 | $field = $modules->get("InputfieldInteger");
719 | $field->name = "titleLimit";
720 | $field->label = __("SEO title character limit");
721 | $field->description = __('The character limit for the SEO title, recommended and default is 60.');
722 | $field->value = $data['titleLimit'];
723 | $field->attr('min', '1');
724 | $field->columnWidth = '34%';
725 | $fieldset->add($field);
726 |
727 | $field = $modules->get("InputfieldInteger");
728 | $field->name = "descriptionLimit";
729 | $field->label = __("SEO description character limit");
730 | $field->description = __('The character limit for the SEO title, recommended and default is 160.');
731 | $field->value = $data['descriptionLimit'];
732 | $field->attr('min', '1');
733 | $field->columnWidth = '33%';
734 | $fieldset->add($field);
735 |
736 |
737 | // Include stuff
738 | $field = $modules->get("InputfieldCheckbox");
739 | $field->name = "includeGenerator";
740 | $field->label = __("Include Generator?");
741 | $field->description = __('This will include a meta tag called "generator" to show that this site was created with "ProcessWire 2.x.x".');
742 | $field->checked = $data['includeGenerator'];
743 | $field->columnWidth = '33%';
744 | $fieldset->add($field);
745 |
746 | $field = $modules->get("InputfieldCheckbox");
747 | $field->name = "includeOpenGraph";
748 | $field->label = __("Include (Basic) Open Graph?");
749 | $field->description = __('The Open Graph meta tags are prefered by Facebook and several other sites.');
750 | $field->checked = $data['includeOpenGraph'];
751 | $field->columnWidth = '34%';
752 | $fieldset->add($field);
753 |
754 | $field = $modules->get("InputfieldCheckbox");
755 | $field->name = "includeTwitter";
756 | $field->label = __("Include (Basic) Twitter Cards?");
757 | $field->description = __('This will help Twitter to extract the right data from your site.');
758 | $field->checked = $data['includeTwitter'];
759 | $field->columnWidth = '33%';
760 | $fieldset->add($field);
761 |
762 | $field = $modules->get("InputfieldText");
763 | $field->name = "twitterUsername";
764 | $field->label = __("Twitter Username");
765 | $field->description = __('Your Twitter username (without "@") is needed for the "include Twitter" option.');
766 | $field->value = $data['twitterUsername'];
767 | $field->columnWidth = '100%';
768 | $field->showIf = 'includeTwitter=1';
769 | $fieldset->add($field);
770 |
771 | // Choose Method
772 | $field = $modules->get("InputfieldRadios");
773 | $field->name = "method";
774 | $field->label = __("Method");
775 | $field->description = __("Do you want to get the generated code included automatically in the part of your site?");
776 | $field->addOption('auto', __('Automatically'));
777 | $field->addOption('manual', __('Manually'));
778 | $field->value = $data['method'];
779 | $field->columnWidth = '50%';
780 | $fieldset->add($field);
781 |
782 | // Add Whitespace
783 | $field = $modules->get("InputfieldCheckbox");
784 | $field->name = "addWhitespace";
785 | $field->label = __("Add whitespace before tags?");
786 | $field->description = __('This will add a little white space (one tab indent) before your meta tags in the "rendered" version. Perfectly if you use the automatically insert method.');
787 | $field->checked = $data['addWhitespace'];
788 | $field->columnWidth = '50%';
789 | $fieldset->add($field);
790 |
791 |
792 | $fieldset = $modules->get("InputfieldFieldset");
793 | $fieldset->label = "Tracking";
794 | $fieldset->collapsed = Inputfield::collapsedNo;
795 | $form->add($fieldset);
796 |
797 | // Analytics code
798 | $field = $modules->get("InputfieldText");
799 | $field->name = "googleAnalytics";
800 | $field->label = __("Google Analytics Code");
801 | $field->description = __("Google Analytics code will be embedded if this field is populated.");
802 | $field->notes = __('How to find your code: https://support.google.com/analytics/answer/1008080. It should look like: UA-XXXXXXX-X.');
803 | $field->value = $data['googleAnalytics'];
804 | $field->columnWidth = '50%';
805 | $fieldset->add($field);
806 |
807 | // Anonymize IP
808 | $field = $modules->get("InputfieldCheckbox");
809 | $field->name = "googleAnalyticsAnonymizeIP";
810 | $field->label = __("Google Analytics: Anonymize IPs?");
811 | $field->description = __('In some countrys (like Germany) anonymizing the visitors IP in Google Analytics is an obligatory setting for legal reasons.');
812 | $field->checked = $data['googleAnalyticsAnonymizeIP'];
813 | $field->columnWidth = '50%';
814 | $fieldset->add($field);
815 |
816 |
817 | $field = $modules->get("InputfieldText");
818 | $field->name = "piwikAnalyticsUrl";
819 | $field->label = __("Piwik Analytics URL");
820 | $field->description = __("Piwik code will be embedded if both Piwik fields are populated.");
821 | $field->value = $data['piwikAnalyticsUrl'];
822 | $field->notes = __('Url without http:// or https://');
823 | $field->columnWidth = '50%';
824 | $fieldset->add($field);
825 |
826 | $field = $modules->get("InputfieldText");
827 | $field->name = "piwikAnalyticsIDSite";
828 | $field->label = __("Piwik Analytics IDSite");
829 | $field->description = __("Piwik code will be embedded if both Piwik fields are populated.");
830 | $field->value = $data['piwikAnalyticsIDSite'];
831 | $field->columnWidth = '50%';
832 | $fieldset->add($field);
833 |
834 |
835 | $fieldset = $modules->get("InputfieldFieldset");
836 | $fieldset->label = "More...";
837 | $fieldset->collapsed = Inputfield::collapsedNo;
838 | $form->add($fieldset);
839 |
840 | $field = $modules->get("InputfieldMarkup");
841 | $field->label = __("Multilanguage");
842 | $line[] = __('If your site uses multiple languages you can use SEO fields in multiple languages to. To archive this, you have to change the fieldtypes of the SEO fields manually:');
843 | $line[] = __('1. Go to "Setup > Fields". There are a lot of fields in the schema "seo_*". Now click on the field you want to have in multiple languages.');
844 | $line[] = __('2. Change the "Type" e.g. from "Text" to "TextLanguage" and click "Save". That\'s it.');
845 | $field->value = implode(' ', $line);
846 | $fieldset->add($field);
847 |
848 |
849 | $field = $modules->get("InputfieldMarkup");
850 | $field->label = __("Recommendations");
851 | $string = __('For an even better SEO experience there are a couple of other modules I can recommend:');
852 | $field->value = ''.$string.'
853 | '.self::javascriptAutocomplete(); // Add javascript
860 |
861 | $fieldset->add($field);
862 |
863 |
864 | $field = $modules->get("InputfieldMarkup");
865 | $field->label = __("Support development");
866 | $string = __('This is and stays a free, open-source module. If you like it and want to support its development you can use this button:');
867 | $field->value = ''.$string.'
868 | ';
869 |
870 | $fieldset->add($field);
871 |
872 | return $form;
873 | }
874 |
875 | static private function javascriptCounter($hardLimit, $titleLimit, $descriptionLimit) {
876 | $string = __('characters used');
877 | $code = "
878 | $(document).ready(function(){
879 | $('#Inputfield_seo_title').data('seolimit', ".$titleLimit.");
880 | $('#Inputfield_seo_description').data('seolimit', ".$descriptionLimit.");
881 |
882 | if(".($hardLimit ?: 0).") {
883 | $('#Inputfield_seo_title').attr('maxlength', ".$titleLimit.");
884 | $('#Inputfield_seo_description').attr('maxlength', ".$descriptionLimit.");
885 | }
886 |
887 | $('#Inputfield_seo_title, #Inputfield_seo_description').each(function(){
888 | $(this).before('' + $(this).val().length + ' /' + $(this).data('seolimit') + ' ".$string.".
');
889 |
890 | $(this).on('change load propertychange keyup input paste', function(el){
891 | $(this).siblings('.counter').children('.remainingChars').html($(this).val().length);
892 | });
893 | });
894 | });
895 | ";
896 | return '';
897 | }
898 |
899 | static private function javascriptAutocomplete() {
900 | $code = "
901 | $(document).ready(function(){
902 | $('#Inputfield_seo_title, #Inputfield_seo_description, .seo_autocomplete').autocomplete({
903 | minLength: 2,
904 | source: function(request, response) {
905 | var suggestions = $.ajax({url:'https://suggestqueries.google.com/complete/search',dataType:'jsonp',data:{q:request.term,cp:1,gs_id:6,xhr:'t',client:'youtube'}}).done(function(data){
906 | console.log(data[1]);
907 | response($.map(data[1], function(item) {
908 | return {
909 | label: item[0],
910 | value: item[0]
911 | }
912 | }));
913 | });
914 | }
915 | }).keydown(function(event) {
916 | if(event.keyCode == 13) {
917 | // prevents enter from submitting the form
918 | event.preventDefault();
919 | return false;
920 | }
921 | });
922 |
923 | });
924 | ";
925 | return '';
926 | }
927 |
928 | private function javascriptGooglePreview() {
929 | $configData = wire('modules')->getModuleConfigData($this);
930 |
931 | $titleSmart = ($configData['useParents'] == true) ? array('seo_title') : $configData['titleSmart'];
932 | $smartFieldFormat = function($fieldName) {
933 | return "input[name={$fieldName}]";
934 | };
935 | $titleFieldsSelectors = implode(',',array_map($smartFieldFormat,$titleSmart));
936 | $titleFieldsNames = "'" . implode('\',\'',$titleSmart) ."'";
937 |
938 | $code = "
939 | $(document).ready(function(){
940 |
941 | $('{$titleFieldsSelectors},input[name=seo_title]').keyup(function(){
942 | $.each([$titleFieldsNames],function(index, name) {
943 | value = $('input[name='+ name +']').val();
944 | if (value != '') {
945 | $('.SEO_google_title').html(value);
946 | return false;
947 | } else if (index == ($(this).length - 1)) {
948 | $('.SEO_google_title').html('".__('Title')."');
949 | }
950 | });
951 | });
952 |
953 | $('#Inputfield_seo_description').keyup(function(){
954 | $('.SEO_google_description').html(((\$(this).val()) ? \$(this).val() : 'This is just a short description.'));
955 | });
956 |
957 | $('#Inputfield_seo_canonical').keyup(function(){
958 | $('.SEO_google_link').html(((\$(this).val()) ? \$(this).val() : '".wire('pages')->get(wire('input')->get->id)->httpUrl."'));
959 | });
960 |
961 | });
962 | ";
963 | return '';
964 | }
965 |
966 |
967 |
968 |
969 |
970 |
971 | /**
972 | * Install and uninstall functions
973 | *
974 | */
975 |
976 | public function ___install() {
977 |
978 | $fields = wire('fields');
979 |
980 | // Tab stuff
981 |
982 | if(!$fields->get('seo_tab')) {
983 | $field = new Field;
984 | $field->type = $this->modules->get('FieldtypeFieldsetTabOpen');
985 | $field->name = "seo_tab";
986 | $field->label = $this->_('SEO');
987 | $field->tags = 'seo';
988 | $field->save();
989 | }
990 |
991 | if(!$fields->get('seo_tab_END')) {
992 | $field = new Field;
993 | $field->type = $this->modules->get('FieldtypeFieldsetClose');
994 | $field->name = "seo_tab_END";
995 | $field->label = $this->_('Close an open fieldset');
996 | $field->tags = 'seo';
997 | $field->save();
998 | }
999 |
1000 |
1001 | // title, keywords, description, image, canonical, robots, custom
1002 |
1003 | if(!$fields->get('seo_title')) {
1004 | $field = new Field;
1005 | $field->type = $this->modules->get("FieldtypeText");
1006 | $field->name = "seo_title";
1007 | $field->label = $this->_("Title");
1008 | $field->description = $this->_("A good length for a title is 60 characters.");
1009 | $field->tags = 'seo';
1010 | $field->save();
1011 | }
1012 |
1013 |
1014 | if(!$fields->get('seo_keywords')) {
1015 | $field = new Field;
1016 | $field->type = $this->modules->get("FieldtypeText");
1017 | $field->name = "seo_keywords";
1018 | $field->label = $this->_("Keywords");
1019 | $field->description = "";
1020 | $field->tags = 'seo';
1021 | $field->save();
1022 | }
1023 |
1024 |
1025 | if(!$fields->get('seo_description')) {
1026 | $field = new Field;
1027 | $field->type = $this->modules->get("FieldtypeText");
1028 | $field->name = "seo_description";
1029 | $field->label = $this->_("Description");
1030 | $field->description = $this->_("A good length for a description is 160 characters.");
1031 | $field->tags = 'seo';
1032 | $field->save();
1033 | }
1034 |
1035 |
1036 | if(!$fields->get('seo_image')) {
1037 | $field = new Field;
1038 | $field->type = $this->modules->get("FieldtypeText");
1039 | $field->name = "seo_image";
1040 | $field->label = $this->_("Image");
1041 | $field->description = $this->_('Please enter the URL (including "http://...") to a preview image.');
1042 | $field->tags = 'seo';
1043 | $field->save();
1044 | }
1045 |
1046 |
1047 | if(!$fields->get('seo_canonical')) {
1048 | $field = new Field;
1049 | $field->type = $this->modules->get("FieldtypeText");
1050 | $field->name = "seo_canonical";
1051 | $field->label = $this->_("Canonical Link");
1052 | $field->notes = $this->_('The URL should include "http://...".');
1053 | $field->tags = 'seo';
1054 | $field->save();
1055 | }
1056 |
1057 |
1058 | if(!$fields->get('seo_custom')) {
1059 | $field = new Field;
1060 | $field->type = $this->modules->get("FieldtypeTextarea");
1061 | $field->name = "seo_custom";
1062 | $field->label = $this->_("Custom");
1063 | $field->description = $this->_("If you want to add other meta tags, you can do it here.");
1064 | $field->notes = $this->_('Please use this schema: name := content. One tag per line. Special characters are only allowed in the content part and get converted to HTML.');
1065 | $field->tags = 'seo';
1066 | $field->save();
1067 | }
1068 |
1069 | if(!$fields->get('seo_robots')) {
1070 | $field = new Field;
1071 | $field->type = wire('modules')->get("FieldtypeText");
1072 | $field->name = "seo_robots";
1073 | $field->label = $this->_("Robots");
1074 | $field->description = $this->_("The robots settings will tell search engine which data they are allowed to include/index.");
1075 | $field->notes = $this->_('This overwrites the module\'s global setting for this page.');
1076 | $field->tags = 'seo';
1077 | $field->save();
1078 | }
1079 | }
1080 |
1081 |
1082 | public function ___uninstall() {
1083 | $fields = wire('fields');
1084 | $templates = wire('templates');
1085 |
1086 | foreach(self::getDefaultFields() as $field) {
1087 | foreach($templates as $template) {
1088 | if(!$template->hasField($field)) continue;
1089 | $template->fields->remove($field);
1090 | $template->fields->save();
1091 | }
1092 | $fields->delete($fields->get($field));
1093 | }
1094 |
1095 | }
1096 | }
1097 |
--------------------------------------------------------------------------------