Source of file SeoPageExtension.php
Size: 24,200 Bytes - Last Modified: 2021-12-24T06:43:33+00:00
/var/www/docs.ssmods.com/process/src/src/Model/Extension/SeoPageExtension.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775 | <?php namespace CyberDuck\SEO\Model\Extension; use Page; use CyberDuck\SEO\Model\SeoHeadTag; use CyberDuck\SEO\Forms\MetaPreviewField; use CyberDuck\SEO\Admin\SEOAdmin; use SilverStripe\Assets\Image; use SilverStripe\AssetAdmin\Forms\UploadField; use SilverStripe\Blog\Model\BlogPost; use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Control\Controller; use SilverStripe\Control\Director; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Convert; use SilverStripe\ErrorPage\ErrorPage; use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor; use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter; use SilverStripe\Forms\HeaderField; use SilverStripe\Forms\TextField; use SilverStripe\Forms\TextareaField; use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\NumericField; use SilverStripe\i18n\i18n; use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\PaginatedList; use SilverStripe\Security\Permission; use SilverStripe\SiteConfig\SiteConfig; /** * @package silverstripe-seo * @license MIT License https://github.com/cyber-duck/silverstripe-seo/blob/master/LICENSE * @author <andrewm@cyber-duck.co.uk> * * SeoPageExtension * * Core extension to add detailed SEO configuration. * Attaches by default to the Page object. * Maps all properties in HeadTags.ss to methods within this class. * e.g $PageMetaTitle => getPageMetaTitle() * The mapped methods handle a class property and return a value based on conditions * within the configuration such as returning a default value when no value is set. * Whether attaching the extension to existing or new page no manual setting of * properties in the CMS should be required if you want to go with a standard config. * * Standard config is: * Canonical => The current full page URL * Robots => 'index,follow' * Priority => '0.5' * ChangeFrequency => 'weekly' * SitemapHide => false * HideSocial => false * OGtype => 'website' * OGlocale => The current website locale * TwitterCard => 'summary' * * Class properties in the $db array here should not be called directly, rather * the class methods should be called to take advantage of SubSite detection, Blog * related DataObject detection and other features. You can subclass, override, or * use YML config to compliment this class and create your own detailed Meta strategies. **/ class SeoPageExtension extends DataExtension { /** * Our page fields * * @since version 1.0.0 * * @config array $db **/ private static $db = [ 'MetaTitle' => 'Varchar(512)', 'Canonical' => 'Varchar(512)', 'Robots' => 'Varchar(100)', 'Priority' => 'Decimal(3,2)', 'ChangeFrequency' => 'Varchar(20)', 'SitemapHide' => 'Boolean', 'HideSocial' => 'Boolean', 'OGtype' => 'Varchar(100)', 'OGlocale' => 'Varchar(10)', 'TwitterCard' => 'Varchar(100)' ]; /** * Social image and other has_one relations * * @since version 1.0.0 * * @config array $has_one **/ private static $has_one = [ 'SocialImage' => Image::class ]; /** * Has many extra Meta tags * * @since version 1.0.0 * * @config array $many_many **/ private static $many_many = [ 'HeadTags' => SeoHeadTag::class, 'SitemapImages' => Image::class ]; /** * Sitemap defaults * * @since version 1.0.0 * * @config array $defaults **/ private static $defaults = [ 'Robots' => 'index,follow', 'Priority' => 0.50, 'ChangeFrequency' => 'weekly', 'OGtype' => 'website', 'TwitterCard' => 'summary' ]; /** * A PaginatedList instance used for rel Meta tags * * @since version 2.0.0 * * @var PaginatedList $pagination **/ private $pagination; /** * Adds our SEO Meta fields to the page field list. The tab is divided into * logical sections controlling various aspects of page SEO. * * @since version 1.0.0 * * @param FieldList $fields The fields object * * @return FieldList **/ public function updateCMSFields(FieldList $fields) { $fields->removeByName('HeadTags'); $fields->removeByName('SitemapImages'); // META TAB // Meta $fields->addFieldToTab('Root.MetaTags', MetaPreviewField::create($this->owner)); $title = TextField::create('MetaTitle'); $description = TextareaField::create('MetaDescription'); if(class_exists(BlogPost::class)) { if($this->owner instanceof BlogPost) { if($this->owner->Parent()->DefaultPostMetaTitle == 1) { $title->setAttribute('placeholder', 'Using page title'); } if($this->owner->Parent()->DefaultPostMetaDescription == 1) { $description->setAttribute('placeholder', 'Using page summary'); } } } $fields->addFieldToTab('Root.MetaTags', $title); $fields->addFieldToTab('Root.MetaTags', $description); // Indexing $fields->addFieldToTab('Root.MetaTags', HeaderField::create(false, 'Indexing', 2)); $canonical = TextField::create('Canonical'); if(!$this->owner->Canonical) { $canonical->setAttribute('placeholder', 'Using page URL'); } $fields->addFieldToTab('Root.MetaTags', $canonical); $robots = DropdownField::create('Robots', 'Robots') ->setSource($this->getRobotsIndexingRules()) ->setEmptyString('- please select - '); if(!$this->owner->Robots) { $robots->setDescription('Using default "index,follow" rule'); } $fields->addFieldToTab('Root.MetaTags', $robots); // Social Sharing $fields->addFieldToTab('Root.MetaTags', HeaderField::create(false, 'Social Sharing', 2)); $fields->addFieldToTab('Root.MetaTags', CheckboxField::create('HideSocial', 'Hide Social Meta?')); $og = DropdownField::create('OGtype', 'Open Graph Type') ->setSource($this->getOGtypes()) ->setEmptyString('- please select - '); if(!$this->owner->OGtype) { $og->setDescription('Using default "website" type'); } $fields->addFieldToTab('Root.MetaTags', $og); $og = DropdownField::create('OGlocale', 'Open Graph Locale') ->setSource($this->getOGlocales()) ->setEmptyString('- please select - '); if(!$this->owner->OGlocale) { $locale = str_replace('-', '_', i18n::get_locale()); $og->setDescription(sprintf('Using default locale from application "%s"', $locale)); } $fields->addFieldToTab('Root.MetaTags', $og); $card = DropdownField::create('TwitterCard', 'Twitter Card') ->setSource($this->getTwitterCardTypes()) ->setEmptyString('- please select - '); if(!$this->owner->TwitterCard) { $card->setDescription('Using default twitter card "summary"'); } $fields->addFieldToTab('Root.MetaTags', $card); $uploader = UploadField::create('SocialImage') ->setFolderName(Config::inst()->get('SocialImage', 'image_folder')) ->setAllowedFileCategories('image', 'image/supported') ->setDescription(sprintf("Minimum size: %sw x %sh pixels.<br/>Maximum file size: %sMB<br/>Recommended Aspect Ratio: %s<br/>Used in og:image and twitter:image meta when social image not set on page / model", Config::inst()->get('CyberDuck\SEO\SocialImage', 'min_file_width'), Config::inst()->get('CyberDuck\SEO\SocialImage', 'min_file_height'), Config::inst()->get('CyberDuck\SEO\SocialImage', 'max_file_size'), Config::inst()->get('CyberDuck\SEO\SocialImage', 'file_aspect_ratio'), )); $uploader->getValidator()->setAllowedMaxFileSize(['*' => (int)Config::inst()->get('CyberDuck\SEO\SocialImage', 'max_file_size') * 1024 * 1024]); if(class_exists(BlogPost::class)) { if($this->owner instanceof BlogPost) { if($this->owner->Parent()->UseFeaturedAsSocialImage == 1) { $uploader->setDescription('Using the page featured image'); } } } $fields->addFieldToTab('Root.MetaTags', $uploader); // Extra Meta Tags $grid = GridField::create('HeadTags', 'Other Meta Tags', $this->owner->HeadTags(), GridFieldConfig_RelationEditor::create()); $grid->getConfig()->removeComponentsByType(GridFieldAddExistingAutocompleter::class); $fields->addFieldToTab('Root.MetaTags', $grid); // SITEMAP TAB // Sitemap $fields->addFieldToTab('Root.Sitemap', HeaderField::create(false, 'Sitemap', 2)); $fields->addFieldToTab('Root.Sitemap', CheckboxField::create('SitemapHide', 'Hide in sitemap? (XML and HTML)')); $fields->addFieldToTab('Root.Sitemap', NumericField::create('Priority')->setScale(1) ->setDescription('0.1, 0.2, 0.3, ..., 0.9, 1.0.<br >1.0 is your highest priorty, the most important page. Often the homepage.')); $fields->addFieldToTab('Root.Sitemap', DropdownField::create('ChangeFrequency', 'Change Frequency') ->setSource($this->getSitemapChangeFrequency()) ->setEmptyString('- please select - ')); $uploader = UploadField::create('SitemapImages') ->setIsMultiUpload(true) ->setFolderName('SitemapImages') ->setAllowedFileCategories('image', 'image/supported'); $fields->addFieldToTab('Root.Sitemap', $uploader); return $fields; } /** * Change the grid summary field structure is currently in SEO admin * * @param object $fields The current summary fields * * @since version 1.0.0 * * @return void **/ public function updateSummaryFields(&$fields) { if(Controller::curr() instanceof SEOAdmin) { Config::inst()->remove($this->owner->class, 'summary_fields'); Config::modify()->set($this->owner->class, 'summary_fields', $this->getSummaryFields()); $fields = Config::inst()->get($this->owner->class, 'summary_fields'); } } /** * Returns an array of summary fields used in the SEO Admin section of the CMS * * @since version 1.0.2 * * @return array **/ public function getSummaryFields() { return [ 'ID' => 'ID', 'PublishedIcon' => 'Published', 'Title' => 'Title', 'PageRobots' => 'Robots', 'PageOgType' => 'OGtype', 'PageOgLocale' => 'OGlocale', 'PageTwitterCard' => 'TwitterCard', 'Priority' => 'Priority', 'ChangeFrequency' => 'Change Freq' ]; } /** * Returns an array of sitemap change frequencies used in a sitemap.xml file * * @since version 1.0.0 * * @return array **/ public function getSitemapChangeFrequency() { return [ 'always' => 'Always', 'hourly' => 'Hourly', 'daily' => 'Daily', 'weekly' => 'Weekly', 'monthly' => 'Monthly', 'yearly' => 'Yearly', 'never' => 'Never' ]; } /** * Returns an array of robots crawling rules used in a robots Meta tag * * @since version 1.0.0 * * @return array **/ public function getRobotsIndexingRules() { return [ 'index,follow' => 'index,follow', 'noindex,nofollow' => 'noindex,nofollow', 'noindex,follow' => 'noindex,follow', 'index,nofollow' => 'index,nofollow' ]; } /** * Return an array of Facebook Open Graph Types used in Meta tags * * @since version 1.0.0 * * @return array **/ public function getOGtypes() { return [ 'website' => 'Website', 'article' => 'Article', 'book' => 'Book', 'profile' => 'Profile', 'music' => 'Music', 'video' => 'Video' ]; } /** * Return an array of Facebook Open Graph locales used in Meta tags * * @since version 1.0.0 * * @return array **/ public function getOGlocales() { return [ 'en_GB' => 'English - United Kingdom', 'en_US' => 'English - United States', 'da_DK' => 'Danish - Denmark', 'hr_HR' => 'Croatian - Croatia', 'nl_NL' => 'Dutch - Netherlands', 'fr_FR' => 'French - France', 'de_DE' => 'German - Germany', 'el_GR' => 'Greek - Greece', 'hu_HU' => 'Hungarian - Hungary', 'is_IS' => 'Icelandic - Iceland', 'id_ID' => 'Indonesian - Indonesia', 'it_IT' => 'Italian - Italy', 'ja_JP' => 'Japanese - Japan', 'ko_KR' => 'Korean - Korea', 'lv_LV' => 'Latvian - Latvia', 'lt_LT' => 'Lithuanian - Lithuania', 'mk_MK' => 'Macedonian - Macedonia', 'no_NO' => 'Norwegian - Norway', 'fa_IN' => 'Persian - India', 'fa_IR' => 'Persian - Iran', 'pl_PL' => 'Polish - Poland', 'pt_PT' => 'Portuguese - Portugal', 'ro_RO' => 'Romanian - Romania', 'ru_RU' => 'Russian - Russia', 'sk_SK' => 'Slovak - Slovakia', 'sl_SI' => 'Slovenian - Slovenia', 'es_ES' => 'Spanish - Spain', 'sv_SE' => 'Swedish - Sweden', 'tr_TR' => 'Turkish - Turkey', 'uk_UA' => 'Ukrainian - Ukraine', 'vi_VN' => 'Vietnamese - Vietnam' ]; } /** * Returns an array of Twitter card types used in Meta tags * * @since version 1.0.0 * * @return array **/ public function getTwitterCardTypes() { return [ 'summary' => 'Summary', 'summary_large_image' => 'Summary Large Image', 'photo' => 'Photo', 'gallery' => 'Gallery', 'app' => 'App', 'product' => 'Product' ]; } /** * Get the current page Meta title * * @since version 2.0.0 * * @return string **/ public function getPageMetaTitle() { if($this->owner->MetaTitle) { return $this->owner->MetaTitle; } if(class_exists(BlogPost::class)) { if($this->owner instanceof BlogPost) { if($this->owner->Parent()->DefaultPostMetaTitle == 1) { return $this->owner->Title; } } } if(SiteConfig::current_site_config()->UseTitleAsMetaTitle) { return $this->owner->Title; } } /** * Get the current page Meta description * * @since version 2.0.0 * * @return string **/ public function getPageMetaDescription() { if($this->owner->MetaDescription) { return $this->owner->MetaDescription; } if(class_exists(BlogPost::class)) { if($this->owner instanceof BlogPost) { if($this->owner->Parent()->DefaultPostMetaDescription == 1) { return strip_tags($this->owner->Summary); } } } } /** * Get the current page canonical tag URL * * @since version 2.0.0 * * @return string **/ public function getPageCanonical($query = null) { if($this->pagination) { if($this->pagination->getPageStart() > 0) { $query = '?'.$this->pagination->getPaginationGetVar().'='.$this->pagination->getPageStart(); } } if($this->owner->Canonical) { return $this->owner->Canonical.$query; } return $this->getPageURL().$query; } /** * Get the current page Meta robots rules * * @since version 2.0.0 * * @return string **/ public function getPageRobots() { if($this->owner->Robots) { return $this->owner->Robots; } return 'index,follow'; } /** * Get the current page URL // todo getAbsoluteURL()? * * @since version 2.0.0 * * @return string **/ public function getPageURL() { return Director::absoluteBaseURL().substr($this->owner->Link(), 1); } /** * Get the current page Meta open graph type * * @since version 2.0.0 * * @return string **/ public function getPageOgType() { if($this->owner->OGtype) { return $this->owner->OGtype; } return 'website'; } /** * Get the current page Meta open graph locale * * @since version 2.0.0 * * @return string **/ public function getPageOgLocale() { if($this->owner->OGlocale) { return $this->owner->OGlocale; } return str_replace('-', '_', i18n::get_locale()); } /** * Get the current page Meta Twitter card type * * @since version 2.0.0 * * @return string **/ public function getPageTwitterCard() { if($this->owner->TwitterCard) { return $this->owner->TwitterCard; } return 'summary'; } /** * Get the current page Meta social sharing image * * @since version 2.0.0 * * @return string **/ public function getPageSocialImage() { if($this->owner->SocialImageID > 0) { return $this->owner->SocialImage(); } if(class_exists(BlogPost::class)) { if($this->owner instanceof BlogPost) { if($this->owner->Parent()->UseFeaturedAsSocialImage == true) { return $this->owner->FeaturedImage(); } } } } /** * Get the current site Facebook app ID * * @since version 2.0.0 * * @return string **/ public function getSiteFacebookAppID() { return SiteConfig::current_site_config()->FacebookAppID; } /** * Get the current site open graph site name * * @since version 2.0.0 * * @return string **/ public function getSiteOgSiteName() { if(SiteConfig::current_site_config()->OGSiteName) { return SiteConfig::current_site_config()->OGSiteName; } return SiteConfig::current_site_config()->Title; } /** * Get the current site Twitter handle * * @since version 2.0.0 * * @return string **/ public function getSiteTwitterHandle() { return '@'.SiteConfig::current_site_config()->TwitterHandle; } /** * Get the current site Twitter creator handle * * @since version 2.0.0 * * @return string **/ public function getSiteCreatorTwitterHandle() { return '@'.SiteConfig::current_site_config()->CreatorTwitterHandle; } /** * Get the current page Meta charset value * * @since version 2.0.0 * * @return string **/ public function getPageCharset() { return Config::inst()->get('ContentNegotiator', 'encoding'); } /** * Returns true when the current page is a CMS preview * * @since version 2.0.0 * * @return boolean **/ public function isCMSPreviewPage() { return Permission::check('CMS_ACCESS_CMSMain') && in_array('CMSPreviewable', class_implements($this)) && !$this instanceof ErrorPage && $this->owner->ID > 0; } /** * Get the current page ID * * @since version 2.0.0 * * @return int **/ public function getCMSPageID() { return $this->owner->ID; } /** * Get the current page edit link * * @since version 2.0.0 * * @return string **/ public function getCMSPageEditLink() { return Controller::curr()->CMSEditLink(); } /** * Get the current page published status icon * * @since version 2.0.0 * * @return string **/ public function getPublishedIcon() { if($this->owner instanceof Page) { $status = $this->owner->isPublished() ? 'accept' : 'delete'; } else { $status = 'accept'; } return DBField::create_field('HTMLText', sprintf('<span class="ui-button-icon-primary ui-icon btn-icon-%s"></span>', $status)); } /** * Get the LastEdited object property as an ISO foramtted date for XML sitemap * * @since version 4.0.0 * * @return string **/ public function getSitemapDate() { return date('c', strtotime($this->owner->LastEdited)); } /** * Sets a Paginated list object which the prev and next rel tags will be * calculated off. This method validates the current $_GET param used for * pagination and will return a 404 response if the $_GET var is outside * of the expected range. e.g start=100 but only 99 items in the list * * @since version 2.0.0 * * @param PaginatedList $list Paginated list object * @param array $params Array of $_GET params to allow in the URL // todo * * @return string|404 response **/ public function setPaginationTags(PaginatedList $list, $params = []) // @todo allowed { $controller = Controller::curr(); if($controller->getRequest()->getVar($list->getPaginationGetVar()) !== NULL) { if((int) $list->getPageStart() === 0) { return $controller->httpError(404); } if($list->CurrentPage() > $list->TotalPages()){ return $controller->httpError(404); } if($list->getPageStart() % $list->getPageLength() !== 0){ return $controller->httpError(404); } if(!preg_match('/^[0-9]+$/', $list->getPageStart())){ return $controller->httpError(404); } } $this->pagination = $list; } /** * Get the current page prev pagination link * * @since version 2.0.0 * * @return string **/ public function getPaginationPrevTag() { if($this->pagination) { if($this->pagination->TotalPages() > 1 && $this->pagination->NotFirstPage()) { if((int) $this->pagination->CurrentPage() === 2) { return $this->getPageURL(); } else { $start = $this->pagination->getPageStart() - $this->pagination->getPageLength(); return $this->getPageURL().'?'.$this->pagination->getPaginationGetVar().'='.$start; } } } } /** * Get the current page next pagination link * * @since version 2.0.0 * * @return string **/ public function getPaginationNextTag() { if($this->pagination) { if($this->pagination->TotalPages() > 1 && $this->pagination->NotLastPage()) { $start = $this->pagination->getPageStart() + $this->pagination->getPageLength(); return $this->getPageURL().'?'.$this->pagination->getPaginationGetVar().'='.$start; } } } } |