use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
+use Drupal\taxonomy\TermInterface;
+use Drupal\Component\Utility\Html;
/**
* Implements hook_help().
$output .= '<li>' . t('To override the meta tags for individual entities, e.g. for individual nodes, add the "Metatag" field via the field settings for that entity or bundle type.') . '</li>';
$output .= '</ol>';
return $output;
- break;
// The main configuration page.
case 'entity.metatag_defaults.collection':
$output .= '<p>' . t('If the top-level configuration is not specific enough, additional default meta tag configurations can be added for a specific entity type or entity bundle, e.g. for a specific content type.') . '</p>';
$output .= '<p>' . t('Meta tags can be further refined on a per-entity basis, e.g. for individual nodes, by adding the "Metatag" field to that entity type through its normal field settings pages.') . '</p>';
return $output;
- break;
// The 'add default meta tags' configuration page.
case 'entity.metatag_defaults.add_form':
$output = '<p>' . t('Use the following form to override the global default meta tags for a specific entity type or entity bundle. In practical terms, this allows the meta tags to be customized for a specific content type or taxonomy vocabulary, so that its content will have different meta tags <em>default values</em> than others.') . '</p>';
$output .= '<p>' . t('As a reminder, if the "Metatag" field is added to the entity type through its normal field settings, the meta tags can be further refined on a per entity basis; this allows each node to have its meta tags customized on an individual basis.') . '</p>';
return $output;
- break;
}
}
if (is_null($metatag_attachments)) {
// Load the meta tags from the route.
$metatag_attachments = metatag_get_tags_from_route();
- if (!$metatag_attachments) {
- return NULL;
+ }
+ if (!$metatag_attachments) {
+ return NULL;
+ }
+
+ // If any Metatag items were found, append them.
+ if (!empty($metatag_attachments['#attached']['html_head'])) {
+ if (empty($attachments['#attached'])) {
+ $attachments['#attached'] = [];
+ }
+ if (empty($attachments['#attached']['html_head'])) {
+ $attachments['#attached']['html_head'] = [];
}
- // If any Metatag items were found, append them.
- if (!empty($metatag_attachments['#attached']['html_head'])) {
- if (empty($attachments['#attached'])) {
- $attachments['#attached'] = [];
- }
- if (empty($attachments['#attached']['html_head'])) {
- $attachments['#attached']['html_head'] = [];
- }
- foreach ($metatag_attachments['#attached']['html_head'] as $item) {
- $attachments['#attached']['html_head'][] = $item;
+ $head_links = [];
+ foreach ($metatag_attachments['#attached']['html_head'] as $item) {
+ $attachments['#attached']['html_head'][] = $item;
+
+ // Also add a HTTP header "Link:" for canonical URLs and shortlinks.
+ // See HtmlResponseAttachmentsProcessor::processHtmlHeadLink() for the
+ // implementation of the functionality in core.
+ if (in_array($item[1], ['canonical_url', 'shortlink'])) {
+ $attributes = $item[0]['#attributes'];
+
+ $href = '<' . Html::escape($attributes['href']) . '>';
+ unset($attributes['href']);
+ if ($param = drupal_http_header_attributes($attributes)) {
+ $href .= ';' . $param;
+ }
+ $head_links[] = $href;
}
}
+
+ // If any HTTP Header items were found, add them too.
+ if (!empty($head_links)) {
+ $attachments['#attached']['http_header'][] = [
+ 'Link',
+ implode(', ', $head_links),
+ FALSE,
+ ];
+ }
+ }
+}
+
+/**
+ * Implements hook_module_implements_alter().
+ */
+function metatag_module_implements_alter(&$implementations, $hook) {
+ if ($hook == 'page_attachments_alter') {
+ // Move metatag_page_attachments_alter() to the end of the list. This is so
+ // the canonical and shortlink tags can be removed that are added by
+ // taxonomy_page_attachments_alter().
+ // @todo Remove once https://www.drupal.org/node/2282029 is fixed.
+ $group = $implementations['metatag'];
+ unset($implementations['metatag']);
+ $implementations['metatag'] = $group;
+ }
+}
+
+/**
+ * Implements hook_page_attachments_alter().
+ */
+function metatag_page_attachments_alter(array &$attachments) {
+ $route_match = \Drupal::routeMatch();
+ // Can be removed once https://www.drupal.org/node/2282029 is fixed.
+ if ($route_match->getRouteName() == 'entity.taxonomy_term.canonical' && ($term = $route_match->getParameter('taxonomy_term')) && $term instanceof TermInterface) {
+ _metatag_remove_duplicate_entity_tags($attachments);
}
}
* Implements hook_entity_view_alter().
*/
function metatag_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
- // Some entities are built with a link rel="canonical" and/or link rel="shortlink" tag attached.
+ // If this is a 403 or 404 page then don't output these meta tags.
+ // @todo Make the default meta tags load properly so this is unnecessary.
+ if ($display->getOriginalId() == 'node.403.default' || $display->getOriginalId() == 'node.404.default') {
+ $build['#attached']['html_head_link'] = [];
+ return;
+ }
+
+ _metatag_remove_duplicate_entity_tags($build);
+}
+
+/**
+ * Remove duplicate entity tags from a build.
+ *
+ * @param array $build
+ * The build.
+ */
+function _metatag_remove_duplicate_entity_tags(array &$build) {
+ // Some entities are built with a link rel="canonical" and/or link
+ // rel="shortlink" tag attached.
// If metatag provides them, remove the ones built with the entity.
if (isset($build['#attached']['html_head_link'])) {
- $metatag_attachments = drupal_static('metatag_attachments');
+ $metatag_attachments = &drupal_static('metatag_attachments');
if (is_null($metatag_attachments)) {
// Load the meta tags from the route.
$metatag_attachments = metatag_get_tags_from_route();
}
- // Check to see if the page currently outputs a canonical and/or shortlink tag.
+ // Check to see if the page currently outputs a canonical and/or shortlink
+ // tag.
if (isset($metatag_attachments['#attached']['html_head'])) {
foreach ($metatag_attachments['#attached']['html_head'] as $metatag_item) {
- if (in_array($metatag_item[1], array('canonical_url', 'shortlink'))) {
+ if (in_array($metatag_item[1], ['canonical_url', 'shortlink'])) {
// Metatag provides rel="canonical" and/or rel="shortlink" tags.
foreach ($build['#attached']['html_head_link'] as $key => $item) {
- if (isset($item[0]['rel']) && in_array($item[0]['rel'], array('canonical', 'shortlink'))) {
+ if (isset($item[0]['rel']) && in_array($item[0]['rel'], ['canonical', 'shortlink'])) {
// Remove the link rel="canonical" or link rel="shortlink" tag
// from the entity's build array.
unset($build['#attached']['html_head_link'][$key]);
/**
* Returns the entity of the current route.
*
- * @return EntityInterface
+ * @return Drupal\Core\Entity\EntityInterface
* The entity or NULL if this is not an entity route.
*/
function metatag_get_route_entity() {
// Look for a canonical entity view page, e.g. node/{nid}, user/{uid}, etc.
$matches = [];
- preg_match('/entity\.(.*)\.canonical/', $route_name, $matches);
+ preg_match('/entity\.(.*)\.(latest[_-]version|canonical)/', $route_name, $matches);
if (!empty($matches[1])) {
$entity_type = $matches[1];
return $route_match->getParameter($entity_type);
}
- // Look for a rest entity view page, e.g. node/{nid}?_format=json, etc.
+ // Look for a rest entity view page, e.g. "node/{nid}?_format=json", etc.
$matches = [];
- // ie: rest.entity.node.GET.json
+ // Matches e.g. "rest.entity.node.GET.json".
preg_match('/rest\.entity\.(.*)\.(.*)\.(.*)/', $route_name, $matches);
if (!empty($matches[1])) {
$entity_type = $matches[1];
return $route_match->getParameter($entity_type);
}
- // Look for entity object 'add' pages, e.g. node/add/{bundle}.
+ // Look for entity object 'add' pages, e.g. "node/add/{bundle}".
$route_name_matches = [];
preg_match('/(entity\.)?(.*)\.add(_form)?/', $route_name, $route_name_matches);
if (!empty($route_name_matches[2])) {
}
}
- // Look for entity object 'edit' pages, e.g. node/{entity_id}/edit.
+ // Look for entity object 'edit' pages, e.g. "node/{entity_id}/edit".
$route_name_matches = [];
preg_match('/entity\.(.*)\.edit_form/', $route_name, $route_name_matches);
if (!empty($route_name_matches[1])) {
}
// Look for entity object 'add content translation' pages, e.g.
- // node/{nid}/translations/add/{source_lang}/{translation_lang}
+ // "node/{nid}/translations/add/{source_lang}/{translation_lang}".
$route_name_matches = [];
preg_match('/(entity\.)?(.*)\.content_translation_add/', $route_name, $route_name_matches);
if (!empty($route_name_matches[2])) {
return NULL;
}
- $attachments = drupal_static('metatag_attachments');
+ $attachments = &drupal_static('metatag_attachments');
if (is_null($attachments)) {
$attachments = metatag_get_tags_from_route();
}
$variables['head_title'] = [];
$variables['head_title']['title'] = html_entity_decode($attachment[0]['#attributes']['content'], ENT_QUOTES);
// Original:
- // $variables['head_title_array']['title'] = $attachment[0]['#attributes']['content'];
- // $variables['head_title'] = implode(' | ', $variables['head_title_array']);
+ // $variables['head_title_array']['title'] =
+ // $attachment[0]['#attributes']['content'];
+ // $variables['head_title'] = implode(' | ',
+ // $variables['head_title_array']);
break;
}
}
* Load the meta tags by processing the route parameters.
*
* @return mixed
- * Array of metatags or NULL.
+ * Array of meta tags or NULL.
*/
-function metatag_get_tags_from_route() {
+function metatag_get_tags_from_route($entity = NULL) {
$metatag_manager = \Drupal::service('metatag.manager');
// First, get defaults.
- $metatags = metatag_get_default_tags();
+ $metatags = metatag_get_default_tags($entity);
if (!$metatags) {
return NULL;
}
// Then, set tag overrides for this particular entity.
- $entity = metatag_get_route_entity();
+ if (!$entity) {
+ $entity = metatag_get_route_entity();
+ }
+
if (!empty($entity) && $entity instanceof ContentEntityInterface) {
+ // If content entity does not have an ID the page is likely an "Add" page,
+ // so do not generate meta tags for entity which has not been created yet.
+ if (!$entity->id()) {
+ return NULL;
+ }
+
foreach ($metatag_manager->tagsFromEntity($entity) as $tag => $data) {
$metatags[$tag] = $data;
}
* @return mixed
* Array of tags or NULL;
*/
-function metatag_get_default_tags() {
+function metatag_get_default_tags($entity = NULL) {
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $global_metatag_manager */
$global_metatag_manager = \Drupal::entityTypeManager()->getStorage('metatag_defaults');
// First we load global defaults.
// Next check if there is this page is an entity that has meta tags.
else {
- $entity = metatag_get_route_entity();
+ if (!$entity) {
+ $entity = metatag_get_route_entity();
+ }
- if (!empty($entity)) {
+ if (!empty($entity) && $entity instanceof ContentEntityInterface) {
$entity_metatags = $global_metatag_manager->load($entity->getEntityTypeId());
if ($entity_metatags != NULL) {
// Merge with global defaults.
return $metatags->get('tags');
}
+
+/**
+ * Implements hook_entity_base_field_info().
+ */
+function metatag_entity_base_field_info(EntityTypeInterface $entity_type) {
+ $fields = [];
+ $base_table = $entity_type->getBaseTable();
+ $canonical_template_exists = $entity_type->hasLinkTemplate('canonical');
+ // Certain classes are just not supported.
+ $original_class = $entity_type->getOriginalClass();
+ $classes_to_skip = [
+ 'Drupal\comment\Entity\Comment',
+ ];
+
+ // If the entity type doesn't have a base table, has no link template then
+ // there's no point in supporting it.
+ if (!empty($base_table) && $canonical_template_exists && !in_array($original_class, $classes_to_skip)) {
+ $fields['metatag'] = BaseFieldDefinition::create('map')
+ ->setLabel(t('Metatags'))
+ ->setDescription(t('The meta tags for the entity.'))
+ ->setClass('\Drupal\metatag\Plugin\Field\MetatagEntityFieldItemList')
+ ->setQueryable(FALSE)
+ ->setComputed(TRUE)
+ ->setTargetEntityTypeId($entity_type->id());
+ }
+
+ return $fields;
+}
+
+/**
+ * Implements hook_entity_diff_options().
+ */
+function metatag_entity_diff_options($entity_type) {
+ if (metatag_entity_supports_metatags($entity_type)) {
+ $options = [
+ 'metatag' => t('Metatags'),
+ ];
+ return $options;
+ }
+}
+
+/**
+ * Implements hook_entity_diff().
+ */
+function metatag_entity_diff($old_entity, $new_entity, $context) {
+ $result = [];
+ $entity_type = $context['entity_type'];
+ $options = variable_get('diff_additional_options_' . $entity_type, []);
+ if (!empty($options['metatag']) && metatag_entity_supports_metatags($entity_type)) {
+ // Find meta tags that are set on either the new or old entity.
+ $tags = [];
+ foreach (['old' => $old_entity, 'new' => $new_entity] as $entity_key => $entity) {
+ $language = metatag_entity_get_language($entity_type, $entity);
+ if (isset($entity->metatags[$language])) {
+ foreach ($entity->metatags[$language] as $key => $value) {
+ $tags[$key][$entity_key] = $value['value'];
+ }
+ }
+ }
+
+ $init_weight = 100;
+ foreach ($tags as $key => $values) {
+ $id = ucwords('Meta ' . $key);
+ // @todo Find the default values and show these if not set.
+ $result[$id] = [
+ '#name' => $id,
+ '#old' => [empty($values['old']) ? '' : $values['old']],
+ '#new' => [empty($values['new']) ? '' : $values['new']],
+ '#weight' => $init_weight++,
+ '#settings' => [
+ 'show_header' => TRUE,
+ ],
+ ];
+ }
+ }
+ return $result;
+}
+
+/**
+ * Turn the meta tags for an entity into a human readable structure.
+ *
+ * @param object $entity
+ * The entity object.
+ *
+ * @return array
+ * All of the meta tags in a nested structure.
+ */
+function metatag_generate_entity_metatags($entity) {
+ $values = [];
+ $raw = metatag_get_tags_from_route($entity);
+ if (!empty($raw['#attached']['html_head'])) {
+ foreach ($raw['#attached']['html_head'] as $tag) {
+ $values[$tag[1]] = $tag[0];
+ }
+ }
+ return $values;
+}