diff --git a/amp.admin.inc b/amp.admin.inc index 782db2a..702eeaa 100644 --- a/amp.admin.inc +++ b/amp.admin.inc @@ -15,6 +15,18 @@ function amp_admin_form($form, &$form_state) { $form = array(); + if (!module_exists('token')) { + // Provide message in case somebody has upgraded AMP module but has not + // installed Token. + drupal_set_message(t('The AMP module requires the Token module as a dependency. Please download and install Token to prevent errors with AMP.', array('@module' => 'https://www.drupal.org/project/token')), 'warning'); + } + + if (!module_exists('ctools')) { + // Provide message in case somebody has upgraded AMP module but has not + // installed ctools. + drupal_set_message(t('The AMP module requires the ctools module as a dependency. Please download and install ctools to prevent errors with AMP.', array('@module' => 'https://www.drupal.org/project/ctools')), 'warning'); + } + if (module_exists('field_ui')) { $form['amp_content_amp_status'] = array( '#title' => t('AMP Status by Content Type'), @@ -270,3 +282,116 @@ function amp_get_formatted_status_list() { } return $node_status_list; } + +/** + * Form constructor for the AMP metadata form. + */ +function amp_admin_metadata_form($form, &$form_state) { + if (!module_exists('token')) { + // Provide message in case somebody has upgraded AMP module but has not + // installed Token. + drupal_set_message(t('The AMP module requires the Token module as a dependency. Please download and install Token to prevent errors with AMP.', array('@module' => 'https://www.drupal.org/project/token')), 'warning'); + } + + if (!module_exists('ctools')) { + // Provide message in case somebody has upgraded AMP module but has not + // installed ctools. + drupal_set_message(t('The AMP module requires the ctools module as a dependency. Please download and install ctools to prevent errors with AMP.', array('@module' => 'https://www.drupal.org/project/ctools')), 'warning'); + } + + + $form['amp_metadata_organization'] = array( + '#type' => 'fieldset', + '#title' => t('Organization information'), + ); + + $form['amp_metadata_organization']['description'] = array( + '#type' => 'item', + '#description' => t('Provide information about your organization for use in search metadata.'), + ); + + $form['amp_metadata_organization']['amp_metadata_token_tree'] = array( + '#theme' => 'token_tree', + '#token_types' => array('site'), + '#dialog' => TRUE, + ); + + $form['amp_metadata_organization']['amp_metadata_organization_name'] = array( + '#type' => 'textfield', + '#title' => t('Organization name'), + '#description' => t(' +

Name of the publisher of the content. Typically, this can be the same as the site name.

+

Suggested token: [site:name]

'), + '#required' => TRUE, + '#attributes' => ['placeholder' => '[site:name]'], + '#default_value' => variable_get('amp_metadata_organization_name', NULL) + ); + + $form['amp_metadata_organization']['amp_metadata_organization_logo'] = array( + '#type' => 'managed_file', + '#title' => t('Organization logo'), + '#description' => t(' +

Upload a logo for your organization.

+

This logo must have a height of 60px and a width less than 600px. SVG logos are not allowed: please provide a JPG, JPEG, GIF or PNG.

+

See the AMP logo guidelines.

', array('@logo_guidelines' => 'https://developers.google.com/search/docs/data-types/articles#amp-logo-guidelines')), + '#upload_location' => 'public://', + '#upload_validators' => array( + 'file_validate_extensions' => array('png jpg'), + ), + '#required' => TRUE, + '#default_value' => variable_get('amp_metadata_organization_logo', FALSE), + ); + + $enabled_types = amp_get_enabled_types(); + if (!empty($enabled_types)) { + $node_types = node_type_get_names(); + $enabled_type_list = array(); + foreach ($enabled_types as $type) { + $enabled_type_list[] = $node_types[$type] . t(': Edit AMP metadata settings', array( + '@configure' => '/admin/structure/types/manage/' . $type . '?destination=/admin/config/content/amp/metadata', + )); + } + + $form['amp_metadata_content_types'] = array( + '#title' => 'Content information from AMP-enabled content types', + '#theme' => 'item_list', + '#items' => $enabled_type_list, + ); + } + else { + $form['amp_metadata_content_types'] = array( + '#markup' => t('No content types are currently enabled for AMP. Enable them here.', array( + '@configure' => '/admin/config/content/amp', + )), + ); + } + + $form['#submit'] = array('amp_admin_metadata_form_submit'); + return system_settings_form($form); +} + +/** + * Submit handler for the amp_admin_metadata_form. + * Sets the logo file to permanent. + */ +function amp_admin_metadata_form_submit($form, &$form_state) { + $old_fid = variable_get('amp_metadata_organization_logo', 0); + if ($form_state['values']['amp_metadata_organization_logo'] != $old_fid) { + + $file = file_load($form_state['values']['amp_metadata_organization_logo']); + + // If this is a new file... + if (!$file->status) { + $file->status = FILE_STATUS_PERMANENT; + file_save($file); + file_usage_add($file, 'amp', 'logo', 1); + } + + // Delete the old file + $old_file = $old_fid ? file_load($old_fid) : FALSE; + if ($old_file) { + file_usage_delete($old_file, 'amp', 'logo', 1); + file_delete($old_file); + } + } +} diff --git a/amp.info b/amp.info index f0bab35..849af03 100644 --- a/amp.info +++ b/amp.info @@ -3,4 +3,6 @@ description = Google AMP integration core = 7.x configure = admin/config/content/amp php = 5.5 +dependencies[] = token:token +dependencies[] = ctools:ctools files[] = amp.test diff --git a/amp.install b/amp.install index 7f162de..2a0acda 100644 --- a/amp.install +++ b/amp.install @@ -20,6 +20,22 @@ function amp_requirements($phase) { 'severity' => REQUIREMENT_ERROR, ); } + if (!module_exists('token')) { + $requirements['amp_token'] = array( + 'title' => t('Token module required for AMP'), + 'value' => t('Not installed'), + 'description' => t('The AMP module requires the Token module as a dependency. Please download and install Token to prevent errors with AMP.', array('@module' => 'https://www.drupal.org/project/token')), + 'severity' => REQUIREMENT_ERROR, + ); + } + if (!module_exists('ctools')) { + $requirements['amp_ctools'] = array( + 'title' => t('ctools module required for AMP'), + 'value' => t('Not installed'), + 'description' => t('The AMP module requires the ctools module as a dependency. Please download and install ctools to prevent errors with AMP.', array('@module' => 'https://www.drupal.org/project/ctools')), + 'severity' => REQUIREMENT_ERROR, + ); + } $themes = list_themes(); if (empty($themes['amptheme']) || empty($themes['amptheme']->status)) { $requirements['amptheme'] = array( diff --git a/amp.module b/amp.module index dfbdab6..5729d6c 100644 --- a/amp.module +++ b/amp.module @@ -46,6 +46,22 @@ function amp_menu() { 'type' => MENU_NORMAL_ITEM, 'file' => 'amp.admin.inc', ); + + $items['admin/config/content/amp/config'] = array( + 'title' => 'AMP Configuration', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'file' => 'amp.admin.inc', + ); + + $items['admin/config/content/amp/metadata'] = array( + 'title' => 'AMP Metadata', + 'description' => 'Configure AMP Metadata', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('amp_admin_metadata_form'), + 'access arguments' => array('administer site configuration'), + 'type' => MENU_LOCAL_TASK, + 'file' => 'amp.admin.inc', + ); return $items; } @@ -424,6 +440,125 @@ function amp_node_form_submit_warnfix(&$form, $form_state) { } /** + * Implements hook_form_FORM_ID_alter(). + */ +function amp_form_node_type_form_alter(&$form, &$form_state, $form_id) { + $enabled_types = amp_get_enabled_types(); + if (in_array($form['#node_type']->type, $enabled_types)) { + $form['amp_metadata'] = array( + '#type' => 'fieldset', + '#title' => t('AMP Metadata'), + '#group' => 'additional_settings', + '#weight' => 100, + ); + + $form['amp_metadata']['description'] = array( + '#type' => 'item', + '#title' => 'Content information', + '#description' => t('Provide information about your content for use in the Top Stories carousel in Google Search.'), + ); + + + $form['amp_metadata']['amp_metadata_options'] = array( + '#type' => 'container', + '#tree' => 'true', + ); + + $amp_metadata_fields = _amp_get_metadata_type_fields(); + + $settings = variable_get('amp_metadata_options_' . $form['#node_type']->type); + + foreach ($amp_metadata_fields as $name => $field) { + $form_options['amp_metadata_config_' . $name] = array( + '#type' => 'textfield', + '#title' => t('@title', array('@title' => ucfirst($name))), + '#default_value' => isset($settings['amp_metadata_config_' . $name]) ? $settings['amp_metadata_config_' . $name] : $field['value'], + ); + switch ($name) { + case 'schemaType': + $schema_type_options = array( + 'Article' => 'Article', + 'NewsArticle' => 'NewsArticle', + 'BlogPosting' => 'BlogPosting', + ); + $form_options['amp_metadata_config_' . $name]['#title'] = 'AMP schema type'; + $form_options['amp_metadata_config_' . $name]['#type'] = 'select'; + $form_options['amp_metadata_config_' . $name]['#options'] = $schema_type_options; + $form_options['amp_metadata_config_' . $name]['#description'] = t('The type of schema to use on AMP pages'); + break; + case 'headline': + $form_options['amp_metadata_config_' . $name]['#title'] = 'Article headline'; + $form_options['amp_metadata_config_' . $name]['#description'] = t(' +

A short headline for an AMP article, using fewer than 110 characters and no HTML markup.

+

Use tokens to provide the correct headline for each article page. Suggested token: [node:title].' + ); + $form_options['amp_metadata_config_' . $name]['#attributes'] = ['placeholder' => '[node:title]']; + break; + case 'datePublished': + $form_options['amp_metadata_config_' . $name]['#title'] = 'Date published'; + $form_options['amp_metadata_config_' . $name]['#description'] = t(' +

The date an article was published.

+

Use tokens to provide the correct published date for each article. Suggested token: [node:created].' + ); + $form_options['amp_metadata_config_' . $name]['#attributes'] = ['placeholder' => '[node:created]']; + break; + case 'dateModified': + $form_options['amp_metadata_config_' . $name]['#title'] = 'Date modified'; + $form_options['amp_metadata_config_' . $name]['#description'] = t(' +

The date an article was most recently modified date.

+

Use tokens to provide the correct modification date for each article. Suggested token: [node:changed].' + ); + $form_options['amp_metadata_config_' . $name]['#attributes'] = ['placeholder' => '[node:changed]']; + break; + case 'author': + $form_options['amp_metadata_config_' . $name]['#title'] = 'Author'; + $form_options['amp_metadata_config_' . $name]['#description'] = t(' +

The name of the author to use on AMP pages.

+

Use tokens to provide the correct author for each article. Token output should be text only, no HTML markup. Suggested token: [node:author:name].' + ); + $form['amp_metadata_config_' . $name]['#attributes'] = ['placeholder' => '[node:author:name]']; + break; + case 'description': + $form_options['amp_metadata_config_' . $name]['#title'] = 'Article description'; + $form_options['amp_metadata_config_' . $name]['#description'] = t(' +

A short description of an AMP article, using fewer than 150 characters and no HTML markup.

+

Use tokens to provide the correct description for each article. Suggested token: [node:summary].' + ); + $form_options['amp_metadata_config_' . $name]['#attributes'] = ['placeholder' => '[node:summary]']; + break; + case 'image': + $form_options['amp_metadata_config_' . $name]['#title'] = 'Article image for carousel'; + $form_options['amp_metadata_config_' . $name]['#description'] = t(' +

An article image to appear in the Top Stories carousel.

+

Images must be at least 696px wide: refer to article image guidelines for further details.

+

Use tokens to provide the correct image for each article. Example token: [node:field_image]. The image field token likely varies by content type.', + array('@image_guidelines' => 'https://developers.google.com/search/docs/data-types/articles#article_types') + ); + $form_options['amp_metadata_config_' . $name]['#attributes'] = ['placeholder' => '[node:field_image]']; + break; + } + } + + $form['amp_metadata']['amp_metadata_options'] += $form_options; + + $form['amp_metadata']['amp_metadata_token_tree'] = array( + '#theme' => 'token_tree', + '#token_types' => array('node'), + '#dialog' => TRUE, + ); + + $form['#submit'][] = 'amp_metadata_node_type_submit'; + } +} + +/** + * Additional submit handler for the node type form. + */ +function amp_metadata_node_type_submit(&$form, $form_state) { + variable_set('amp_metadata_options_' . $form['#node_type']->type, $form_state['values']['amp_metadata_options']); +} + +/** * Implements hook_form_alter(). */ function amp_form_alter(&$form, &$form_state, $form_id) { @@ -511,6 +646,94 @@ function amp_node_view($node, $view_mode, $langcode) { $uri['options']['absolute'] = TRUE; drupal_add_html_head_link(array('rel' => 'amphtml', 'href' => url($uri['path'], $uri['options'])), TRUE); } + + // Add AMP metadata. + if ($view_mode == 'amp' && node_is_page($node)) { + $metadata = variable_get('amp_metadata_options_' . $node->type); + $amp_metadata_type_fields = _amp_get_metadata_type_fields(); + + $metadata_json['@context'] = 'http://schema.org'; + + $main_uri = entity_uri('node', $node); + $main_uri['options']['absolute'] = TRUE; + $metadata_json['mainEntityOfPage'] = url($main_uri['path'], $main_uri['options']); + + foreach ($amp_metadata_type_fields as $name => $values) { + if ($values['type'] == 'schema') { + $metadata_json['@type'] = $metadata['amp_metadata_config_' . $name]; + } + elseif ($values['type'] == 'text') { + $metadata_json[$name] = $metadata['amp_metadata_config_' . $name]; + } + elseif ($values['type'] == 'date') { + $metadata_json[$name] = $metadata['amp_metadata_config_' . $name]; + } + elseif ($values['type'] == 'Person') { + $metadata_json[$name] = array( + '@type' => 'Person', + 'name' => $metadata['amp_metadata_config_' . $name], + ); + } + elseif ($values['type'] == 'ImageObject') { + $image_url = token_replace($metadata['amp_metadata_config_' . $name], array('node' => $node)); + + // Provide backup parsing of image element if token does not output a URL. + if (strip_tags($image_url) != $image_url) { + // Force path to be absolute. + if (strpos($image_url, 'img src="/') !== FALSE) { + global $base_root; + $image_url = str_replace('img src="/', 'img src="' . $base_root . '/', $image_url); + } + + $matches = array(); + preg_match('/src="([^"]*)"/', $image_url, $matches); + if (!empty($matches[1])) { + $image_url = $matches[1]; + } + } + + if (!empty($image_url) && $image_info = image_get_info($image_url)) { + $metadata_json[$name] = array( + '@type' => 'ImageObject', + 'url' => $image_url, + 'width' => $image_info[0], + 'height' => $image_info[1], + ); + } + } + + // Add global publisher info. + $metadata_json['publisher'] = array( + '@type' => 'Organization', + 'name' => variable_get('amp_metadata_organization_name', ''), + ); + + $logo_fid = variable_get('amp_metadata_organization_logo', NULL); + if ($logo_fid) { + $logo = file_load($logo_fid); + $logo_url = file_create_url($logo->uri); + $logo_info = image_get_info($logo_url); + $metadata_json['publisher']['logo'] = array( + '@type' => 'ImageObject', + 'url' => $logo_url, + 'width' => $logo_info[0], + 'height' => $logo_info[1], + ); + } + } + + drupal_alter('amp_metadata', $metadata_json, $node, $type); + + $element = array( + '#tag' => 'script', + '#type' => 'html_tag', + '#attributes' => array( + 'type' => 'application/ld+json', + ), + '#value' => strip_tags(token_replace(json_encode($metadata_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT), array('node' => $node))), + ); + drupal_add_html_head($element, 'amp_metadata'); + } } /** @@ -1214,3 +1437,21 @@ function amp_html_head_alter(&$head_elements) { } } } + +/** + * Helper function to return information about metadata node type fields. + * + * @return array + */ +function _amp_get_metadata_type_fields() { + + return array( + 'schemaType' => array('value' => 'NewsArticle', 'type' => 'schema'), + 'headline' => array('value' => '[node:title]', 'type' => 'text'), + 'datePublished' => array('value' => '[node:created]', 'type' => 'date'), + 'dateModified' => array('value' => '[node:changed]', 'type' => 'date'), + 'description' => array('value' => '[node:summary]', 'type' => 'text'), + 'author' => array('value' => '[node:author:name]', 'type' => 'Person'), + 'image' => array('value' => '', 'type' => 'ImageObject'), + ); +}