├── 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 <head> 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('<br>', $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 = '<p>'.$string.'</p> 853 | <ul> 854 | <li><a target="_blank" href="http://modules.processwire.com/modules/markup-sitemap-xml/">Sitemap</a> (MarkupSitemapXML)</li> 855 | <li><a target="_blank" href="http://modules.processwire.com/modules/page-path-history/">Page Path History</a> (PagePathHistory)</li> 856 | <li><a target="_blank" href="http://modules.processwire.com/modules/search-engine-referrer-tracker/">Search Engine Referrer Tracker</a> (SearchEngineReferrerTracker)</li> 857 | <li><a target="_blank" href="http://modules.processwire.com/modules/process-redirects/">Redirects</a> (ProcessRedirects)</li> 858 | <li><a target="_blank" href="http://modules.processwire.com/modules/all-in-one-minify/">AIOM+</a> (AllInOneMinify)</li> 859 | </ul>'.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 = '<p>'.$string.'</p> 868 | <a target="_blank" href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8RTGCB7NCWE2J"><img src="https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!"></a>'; 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('<p class=\"counter notes\"><span class=\"remainingChars\">' + $(this).val().length + '</span>/' + $(this).data('seolimit') + ' ".$string.".</p>'); 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 '<script type="text/javascript">'.$code.'</script>'; 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 '<script type="text/javascript">'.$code.'</script>'; 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 '<script type="text/javascript">'.$code.'</script>'; 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 | --------------------------------------------------------------------------------