diff --git a/CHANGELOG.md b/CHANGELOG.md index d20220d4a22..a47778da6ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ * Support using `.env` and `.env.local.php` files (see #768). * Do not install the tests with "prefer-dist" (see #762). * Simplify registering custom fragment types (see #776). - * Add a SERP preview wherever page meta data can be edited. + * Add a Google search results preview wherever page meta data can be edited. * Dynamically add the robots.txt and favicon.ico files per root page (see #717). * Moved folderUrl setting to the root page (see #706). * Do not generate request tokens via ESI if not needed (see #710). diff --git a/calendar-bundle/src/Resources/contao/dca/tl_calendar_events.php b/calendar-bundle/src/Resources/contao/dca/tl_calendar_events.php index 93352356947..dfcb45c9b86 100644 --- a/calendar-bundle/src/Resources/contao/dca/tl_calendar_events.php +++ b/calendar-bundle/src/Resources/contao/dca/tl_calendar_events.php @@ -259,7 +259,7 @@ 'label' => &$GLOBALS['TL_LANG']['MSC']['serpPreview'], 'exclude' => true, 'inputType' => 'serpPreview', - 'eval' => array('serpPreview'=>array('title'=>array('pageTitle', 'title'), 'description'=>array('description', 'teaser'))), + 'eval' => array('url_callback'=>array('tl_calendar_events', 'getSerpUrl'), 'titleFields'=>array('pageTitle', 'title'), 'descriptionFields'=>array('description', 'teaser')), 'sql' => null ), 'location' => array @@ -737,6 +737,18 @@ public function setEmptyEndTime($varValue, Contao\DataContainer $dc) return $varValue; } + /** + * Return the SERP URL + * + * @param Contao\CalendarEventsModel $model + * + * @return string + */ + public function getSerpUrl(Contao\CalendarEventsModel $model) + { + return Contao\Events::generateEventUrl($model, true); + } + /** * Add the type of input field * diff --git a/core-bundle/src/Resources/contao/dca/tl_page.php b/core-bundle/src/Resources/contao/dca/tl_page.php index 17c75645695..d50fa17e3b3 100644 --- a/core-bundle/src/Resources/contao/dca/tl_page.php +++ b/core-bundle/src/Resources/contao/dca/tl_page.php @@ -269,7 +269,7 @@ 'label' => &$GLOBALS['TL_LANG']['MSC']['serpPreview'], 'exclude' => true, 'inputType' => 'serpPreview', - 'eval' => array('serpPreview'=>array('title'=>array('pageTitle', 'title'))), + 'eval' => array('url_callback'=>array('tl_page', 'getSerpUrl'), 'titleFields'=>array('pageTitle', 'title')), 'sql' => null ), 'redirect' => array @@ -989,6 +989,18 @@ public function checkRootType($varValue, Contao\DataContainer $dc) return $varValue; } + /** + * Return the SERP URL + * + * @param Contao\PageModel $model + * + * @return string + */ + public function getSerpUrl(Contao\PageModel $model) + { + return $model->getAbsoluteUrl(); + } + /** * Show a warning if there is no language fallback page */ diff --git a/core-bundle/src/Resources/contao/languages/en/default.xlf b/core-bundle/src/Resources/contao/languages/en/default.xlf index 930339105ed..1939e24f992 100644 --- a/core-bundle/src/Resources/contao/languages/en/default.xlf +++ b/core-bundle/src/Resources/contao/languages/en/default.xlf @@ -1848,10 +1848,10 @@ The internal CSS editor has been deprecated and will be removed in one of the next Contao versions! Please consider exporting your existing style sheets and re-adding them to the page layout as external style sheets. - SERP preview + Google search results preview - Here you can preview the meta data in the search results. Some search engines might show longer texts or crop at a different position. + Here you can preview the meta data in the Google search results. Other search engines might show longer texts or crop at a different position. Byte diff --git a/core-bundle/src/Resources/contao/widgets/SerpPreview.php b/core-bundle/src/Resources/contao/widgets/SerpPreview.php index 35ba838f900..d587311c683 100644 --- a/core-bundle/src/Resources/contao/widgets/SerpPreview.php +++ b/core-bundle/src/Resources/contao/widgets/SerpPreview.php @@ -11,7 +11,10 @@ namespace Contao; /** - * @property array $serpPreview + * @property array $titleFields + * @property array $descriptionFields + * @property string $aliasField + * @property callable $url_callback */ class SerpPreview extends Widget { @@ -26,7 +29,7 @@ class SerpPreview extends Widget public function generate() { /** @var Model $class */ - $class = $this->serpPreview['class'] ?? Model::getClassFromTable($this->strTable); + $class = Model::getClassFromTable($this->strTable); $model = $class::findByPk($this->activeRecord->id); if (!$model instanceof Model) @@ -37,15 +40,30 @@ public function generate() $id = $model->id; $title = StringUtil::substr($this->getTitle($model), 64); $description = StringUtil::substr($this->getDescription($model), 160); + $alias = $this->getAlias($model); + + // Get the URL with a %s placeholder for the alias or ID $url = $this->getUrl($model); - list($baseUrl) = explode($model->alias ?: $model->id, $url); - $urlSuffix = System::getContainer()->getParameter('contao.url_suffix'); + list($baseUrl, $urlSuffix) = explode('%s', $url); + + // Use the base URL for the index page + if ($model instanceof PageModel && $alias == 'index') + { + $url = $baseUrl; + } + else + { + $url = sprintf($url, $alias ?: $model->id); + } + + // Get the input field suffix (edit multiple mode) $suffix = substr($this->objDca->inputName, \strlen($this->objDca->field)); - $titleField = $this->getTitleField() . $suffix; - $titleFallbackField = $this->getTitleFallbackField() . $suffix; - $aliasField = $this->getAliasField() . $suffix; - $descriptionField = $this->getDescriptionField() . $suffix; - $descriptionFallbackField = $this->getDescriptionFallbackField() . $suffix; + + $titleField = $this->getTitleField($suffix); + $titleFallbackField = $this->getTitleFallbackField($suffix); + $aliasField = $this->getAliasField($suffix); + $descriptionField = $this->getDescriptionField($suffix); + $descriptionFallbackField = $this->getDescriptionFallbackField($suffix); return << @@ -72,115 +90,116 @@ public function generate() private function getTitle(Model $model) { - if (!isset($this->serpPreview['title'])) + if (!isset($this->titleFields)) { return $model->title; } - if (\is_array($this->serpPreview['title'])) - { - return $model->{$this->serpPreview['title'][0]} ?: $model->{$this->serpPreview['title'][1]}; - } - - return $model->{$this->serpPreview['title']}; + return $model->{$this->titleFields[0]} ?: $model->{$this->titleFields[1]}; } private function getDescription(Model $model) { - if (!isset($this->serpPreview['description'])) + if (!isset($this->descriptionFields)) { return $model->description; } - if (\is_array($this->serpPreview['description'])) + return $model->{$this->descriptionFields[0]} ?: $model->{$this->descriptionFields[1]}; + } + + private function getAlias(Model $model) + { + if (!isset($this->aliasField)) { - return $model->{$this->serpPreview['description'][0]} ?: $model->{$this->serpPreview['description'][1]}; + return $model->alias; } - return $model->{$this->serpPreview['description']}; + return $model->{$this->aliasField}; } + /** + * @todo Use the router to generate the URL in a future version (see #831) + */ private function getUrl(Model $model) { - if (isset($this->serpPreview['url'])) + if (!isset($this->url_callback)) { - return $this->serpPreview['url']; + throw new \LogicException('No url_callback given'); } - // FIXME: use the router to generate the URL (see #831) - switch (true) - { - case $model instanceof PageModel: - return $model->getAbsoluteUrl(); - - case $model instanceof NewsModel: - return News::generateNewsUrl($model, false, true); + $alias = $this->getAlias($model); + $placeholder = bin2hex(random_bytes(10)); - case $model instanceof CalendarEventsModel: - return Events::generateEventUrl($model, true); + // Pass a detached clone with the alias set to the placeholder + $tempModel = clone $model; + $tempModel->origAlias = $tempModel->$alias; + $tempModel->$alias = $placeholder; + $tempModel->preventSaving(false); - default: - throw new \RuntimeException(sprintf('Unsupported model class "%s"', \get_class($model))); + if (\is_array($this->url_callback)) + { + $url = System::importStatic($this->url_callback[0])->{$this->url_callback[1]}($tempModel); } - } - - private function getTitleField() - { - if (!isset($this->serpPreview['title'])) + elseif (\is_callable($this->url_callback)) { - return 'ctrl_title'; + $url = \call_user_func($this->url_callback, $tempModel); } - - if (\is_array($this->serpPreview['title'])) + else { - return 'ctrl_' . $this->serpPreview['title'][0]; + throw new \LogicException('Please provide the url_callback as callable'); } - return 'ctrl_' . $this->serpPreview['title']; + return str_replace($placeholder, '%s', $url); } - private function getTitleFallbackField() + private function getTitleField($suffix) { - if (!isset($this->serpPreview['title']) || !\is_array($this->serpPreview['title'])) + if (!isset($this->titleFields[0])) { - return ''; + return 'ctrl_title' . $suffix; } - return 'ctrl_' . $this->serpPreview['title'][1]; + return 'ctrl_' . $this->titleFields[0] . $suffix; } - private function getAliasField() + private function getTitleFallbackField($suffix) { - if (!isset($this->serpPreview['alias'])) + if (!isset($this->titleFields[1])) { - return 'ctrl_alias'; + return ''; } - return 'ctrl_' . $this->serpPreview['alias']; + return 'ctrl_' . $this->titleFields[1] . $suffix; } - private function getDescriptionField() + private function getDescriptionField($suffix) { - if (!isset($this->serpPreview['description'])) + if (!isset($this->descriptionFields[0])) { - return 'ctrl_description'; + return 'ctrl_description' . $suffix; } - if (\is_array($this->serpPreview['description'])) + return 'ctrl_' . $this->descriptionFields[0] . $suffix; + } + + private function getDescriptionFallbackField($suffix) + { + if (!isset($this->descriptionFields[1])) { - return 'ctrl_' . $this->serpPreview['description'][0]; + return ''; } - return 'ctrl_' . $this->serpPreview['description']; + return 'ctrl_' . $this->descriptionFields[1] . $suffix; } - private function getDescriptionFallbackField() + private function getAliasField($suffix) { - if (!isset($this->serpPreview['description']) || !\is_array($this->serpPreview['description'])) + if (!isset($this->aliasField)) { - return ''; + return 'ctrl_alias' . $suffix; } - return 'ctrl_' . $this->serpPreview['description'][1]; + return 'ctrl_' . $this->aliasField . $suffix; } } diff --git a/news-bundle/src/Resources/contao/dca/tl_news.php b/news-bundle/src/Resources/contao/dca/tl_news.php index fa2fe28ee36..efead5496db 100644 --- a/news-bundle/src/Resources/contao/dca/tl_news.php +++ b/news-bundle/src/Resources/contao/dca/tl_news.php @@ -239,7 +239,7 @@ 'label' => &$GLOBALS['TL_LANG']['MSC']['serpPreview'], 'exclude' => true, 'inputType' => 'serpPreview', - 'eval' => array('serpPreview'=>array('title'=>array('pageTitle', 'headline'), 'description'=>array('description', 'teaser'))), + 'eval' => array('url_callback'=>array('tl_news', 'getSerpUrl'), 'titleFields'=>array('pageTitle', 'headline'), 'descriptionFields'=>array('description', 'teaser')), 'sql' => null ), 'subheadline' => array @@ -675,6 +675,18 @@ public function loadTime($value) return strtotime('1970-01-01 ' . date('H:i:s', $value)); } + /** + * Return the SERP URL + * + * @param Contao\NewsModel $model + * + * @return string + */ + public function getSerpUrl(Contao\NewsModel $model) + { + return Contao\News::generateNewsUrl($model, false, true); + } + /** * List a news article *