Adding Theme Support

Weston Ruter edited this page Sep 9, 2018 · 32 revisions

The classic behavior of the AMP plugin is to serve AMP responses in basic templates (with that blue bar at the top) which are separate from the templates in your active theme. Starting with version 0.7 your theme can register support for amp in order to re-use your theme's templates and styles in AMP instead of having a completely separate user experience. As of 1.0 you can opt-in to this amp theme support via the plugin's admin screen options without any add_theme_support( 'amp' ) code. The default template mode for the plugin remains “Classic” where the legacy post templates will be used. If you do add amp theme support with code then the template mode radio buttons will be disabled. Beyond Classic, the two other template modes are “Paired” and “Native”:

Template modes

When you enable amp theme support, the plugin will re-use your theme's normal templates that you serve to your non-AMP responses, including all the templates in your template hierarchy like single.php, front-page.php, category.php and so on. This allows you to have full visual parity between your non-AMP and AMP responses; it allows you to re-use the vast majority of the same markup and styles. However, what is not re-usable in AMP from your themes and plugins is any scripts; therefore, the baseline experience for enabling AMP on an existing theme is that it largely behaves as if JavaScript is disabled in the user's browser. Many themes and plugins already have fallbacks in case JavaScript is not available so that will serve them well in AMP, but if you want to have full feature parity in AMP with the same interactivity you expect normally, there are alternatives in AMP including the use of AMP components like amp-carousel and then scripting via amp-bind. The plugin includes built-in support for the interactivity features in Twenty Seventeen, Twenty Sixteen, and Twenty Fifteen: these features include toggling the menu on mobile, expanding submenu items, and doing smooth scrolling. For more on how to implement these in your own themes, see Implementing Interactivity.

Template Rendering

To understand how your theme templates are served in AMP, it's important to know what is happening behind the scenes. Before the template starts rendering, the plugin registers all of the content embed handlers (as returned by amp_get_content_embed_handlers()) so that any embeds (and shortcodes) have the opportunity to be output as AMP markup from the start (e.g. YouTube video as <amp-youtube> instead of as an <iframe>). In the same way, the add_buffering_hooks() method of each registered sanitizer (as returned by amp_get_content_sanitizers()) will be called so that the sanitizers have the opportunity to add/remove any actions and filters for the AMP response to preemptively output valid AMP content (which is currently only used by the AMP_Core_Theme_Sanitizer). The plugin then uses output buffering to capture the raw HTML template output by your theme. When the output has finished then the buffered HTML is then loaded into a DOMDocument and it is post-processed via the registered sanitizers to make sure that the response is valid AMP. The sanitizers are responsible for converting img into amp-img (via AMP_Img_Sanitizer), iframe into amp-iframe (via AMP_Iframe_Sanitizer), POST form[action] into form[action-xhr] (via AMP_Form_Sanitizer), among other such mappings.

By far the most important sanitizers are AMP_Style_Sanitizer and AMP_Tag_And_Attribute_Sanitizer. These are always run after all of the other sanitizers have finished. Since AMP limits CSS to a single style element with a max of 50KB, the penultimate style sanitizer fetches all external stylesheets (except for whitelisted fonts), collects all style elements in the document, and creates style rules from inline style attributes. With these stylesheets collected, it will then parse them. The parsed stylesheet is processed to ensure that there are no invalid at-rules and no disallowed CSS properties; relative paths for background images will be made absolute and any !important qualifiers will be transformed into style rules with higher-specificity selectors. Once the CSS has been processed, the resulting stylesheets are then minified (to remove comments and extraneous whitespace) and then serialized into a data structure where the selectors of each declaration block have their class names, tag names, and IDs extracted. The resulting data is then stored in a transient (with cache key including an md5 hash of original stylesheet) since the parsing is relatively expensive. The style sanitizer then finally needs to construct the style[amp-custom] element to put into the head. If it determines that the total CSS would be more than 50KB then does “CSS tree shaking” to remove any rules that don't apply to the current page. It does this by comparing the element names, class, and IDs which are referenced in the selectors with those which actually exist in the current document. If there is no intersection between a selector and the document, then a selector is removed. If all selectors are removed from a declaration block, then the entire declaration block is removed. The CSS tree shaking is not done by default because there could be content dynamically-added to the page after the initial load which could then end up unstyled. The style sanitizer lets you explicitly prevent a selector from being removed by skipping the removal of any selector that is under known dynamic element containers (including amp-list and amp-live-list). In general, this CSS tree shaking is critical for themes and plugins to be served as AMP since they will almost always have more than 50KB. If after tree-shaking there is still more than 50KB, then any stylesheet that takes the total over 50KB will be omitted. For this reason there is an admin setting for whether or not to render the admin bar in AMP responses, since it requires ~17KB of CSS.

After the AMP_Style_Sanitizer finishes its changes to the DOMDocument then the AMP_Tag_And_Attribute_Sanitizer runs. This sanitizer is also known as the “whitelist sanitizer” and it serves as a catch-all for anything that did not get sanitized by the preceding sanitizers. The whitelist sanitizer removes any elements or attributes which would cause the response to be invalid AMP. In particular, this sanitizer will remove any script tags or JavaScript event handler attributes, since custom JavaScript is not allowed in AMP (at least, not in the traditional way). It actually uses the AMP validator specification to inform whether something is valid. With everything invalid then removed from the document, the plugin then ensures the minimal required markup is present (e.g. the meta viewport).

👉 Not only does the whitelist sanitizer remove any markup that is not valid AMP but when it comes across an AMP component it will automatically enqueue the required AMP components script to the page. In the legacy templating when you used an AMP component outside the content you would have to manually make sure that the required script is included via the amp_post_template_data filter (see example). Now with theme support, however, if you want to use an <amp-ad> in your template, all you need to do is just use it: the plugin will automatically do wp_enqueue_script( 'amp-ad' ) for you, essentially.

Conditional Template Support

As of v1.0-beta there is now the ability for you to select which templates you want to be made available in AMP and which you do not. This applies both when you use the native mode and the paired mode. You could, for example, use native AMP on your singular templates (for posts and pages) but then serve non-AMP on all other templates. When you un-check the “Serve all templates as AMP regardless of what is being queried” checkbox, then a list of the main templates of your site are listed and organized according to the WordPress Template Hierarchy:

Supported templates

In addition to being able to opt-in to the templates you want to serve as AMP via the admin settings screen, you can also programmatically force which templates are enabled and which are disabled. For example, when adding amp theme support you can do the following to force singular template to be available in AMP and forbid the search results from being in AMP:

add_theme_support( 'amp', array(
	'templates_supported' => array(
		'is_singular' => true,
		'is_search' => false,
	),
) );

The checkboxes for these templates will be disabled in the UI. Any template conditions which are omitted will allow the user to opt-in or opt-out of AMP. You can also force AMP to be supported by all templates in a theme via the following, which will prevent the “Supported Templates” section from displaying any options at all:

add_theme_support( 'amp', array(
	'templates_supported' => 'all',
) );

If you want to add new items to the template hierarchy, you can use the amp_supportable_templates filter. For example, to add more granularity to the date templates, you can do:

add_filter( 'amp_supportable_templates', function( $templates ) {
	$templates['is_year'] = array(
		'label'  => __( 'Year', 'example' ),
		'parent' => 'is_date',
	);
	return $templates;
} );

Take note of the parent argument to ensure that you follow the template hierarchy for the template conditionals. In this case the array keys happen to be template conditional function name. You can also specify a callback for each supportable template for custom templates. For example, let's say you have a template that is outside the normal template hierarchy which you serve when the custom query var is present (and for which you serve a template at template_redirect). You can add a new supportable template option for this via:

add_filter( 'amp_supportable_templates', function( $templates ) {
	$templates['is_custom'] = array(
		'label'    => __( 'Custom', 'example' ),
		'callback' => function( WP_Query $query ) {
			return false !== $query->get( 'custom', false );
		},
	);
	return $templates;
} );

The conditional template support was previously implemented via the available_callback argument, but this is now deprecated. Additionally, conditional template is now available in both paired and native modes, whereas previously it was only available in paired mode.

Validation Handling

In versions of the plugin prior to the introduction of amp theme support, when the plugin encountered a validation error it would just silently remove the offending markup from the document without telling the user. This was very problematic since it could be that the removed markup is critical to the page being rendered properly. For example, consider a shortcode that outputs a script tag which renders an interactive piece in an article; this will be removed and the article will be broken. Or what if you have ads in your header and sidebar that are placed there with JavaScript? You will lose revenue on the page.

With theme support enabled the plugin now will alert the user that there are any validation errors so that they can take action or else at least be fully informed. Validation errors are presented on the edit screen after having updated a post or page, and the invalid elements and attributes will be listed:

Gutenberg editor with validation warning

In this case, there are validation errors not only with the content but also in the template as a while. Navigating to the “Review issues” link will take you to the AMP Invalid URL screen which will list out all of the validation errors including the source for where they came from (theme/plugin, widget, shortcode, block, action, etc.), and then the ability to take action for each validation error. With each validation error there is a dropdown that indicates whether the validation error is New or if it has been previously Accepted or Rejected. By accepting a validation error, you are acknowledging that it is OK to sanitize in the response. In contrast, rejecting a validation error marks it as something that breaks the page in AMP. In paired mode (where there are separate URLs for AMP) then when accessing a URL that has validation errors, a URL that has a validation error which is either new or rejected, then accessing the AMP version of the page will temporarily redirect (302) to the non-AMP version. When logged-in you'll see this AMP status reflected in the admin bar:

AMP unavailable due to validation errors

When accessing the to link to re-validate will take the user to the same AMP Invalid URL screen as presented above when clicking the “Review issues” link in the editor. Once all of the validation errors are accepted then the status in the admin bar will change:

AMP unavailable due to validation errors

There is also an option on the AMP settings screen to automatically sanitize all validation errors to bypass this redirect behavior, but even in this case the user will still be notified of the validation errors and allowing them to be explicitly accepted or flagging them as rejected to be fixed. Validation error handling in native mode behaves the same way way as if this auto-sanitize option is enabled, since there is no separate non-AMP URL to redirect to.

A validation error is an object that contains a code and any number of properties that uniquely identify it. For example, if the whitelist sanitizer comes across markup that contains an onclick attribute like as depicted in the screenshot above, then the validation error for this will look like:

{
	"code": "invalid_attribute",
	"node_name": "onclick",
	"parent_name": "button",
	"element_attributes": {
	    "onclick": "handleClick()"
	}
}

As long as the properties of such a validation error are do not change then all occurrences are treated as instances of the same unique validation error and there is no duplication. If this button appears in a widget that occurs on every template of a site then once it is accepted then it will be accepted across all other URLs where it appears.

There are two filters which can be used to customize the validation behavior: amp_validation_error and amp_validation_error_sanitized. Let's say you have a script that outputs a nonce for the current user, such as for the the WP REST API:

<script type='text/javascript'>
var wpApiSettings = {
	"root":"https://src.wordpress-develop.test/wp-json/",
	"nonce":"05e8aea418",
	"versionString":"wp/v2/"
};
</script>

For script elements, the validation error will include the inner text as a property. Since the nonce is different for each user and the nonce changes every day, this will result in accepting the validation error with the value of 05e8aea418 being only temporary and only for the current user. So a way to prevent this error duplication from happening is to use the amp_validation_error filter to scrub the dynamic part of the text property. For example, put the following in your theme's functions.php file:

add_filter( 'amp_validation_error', function( $validation_error ) {
	$is_inline_script = (
		'invalid_element' === $validation_error['code']
		&&
		'script' === $validation_error['node_name']
		&&
		! empty( $validation_error['text'] )
	);
	if ( $is_inline_script ) {
		$validation_error['text'] = preg_replace( '/"nonce":".+?",?/', '', $validation_error['text'] );
	}
	return $validation_error;
} );

This will scrub the nonce string from being included in the validation error's properties so that the validation error will not vary by user or the current time. The one validation error can then be accepted and new duplicate validation errors for this script will not be raised. Alternatively, you can just decide to auto-accept the sanitization for such validation errors so they never appear in the UI to accept:

add_filter( 'amp_validation_error_sanitized', function( $sanitized, $error ) {
	$is_random_script = (
		AMP_Validation_Error_Taxonomy::INVALID_ELEMENT_CODE === $error['code']
		&&
		'script' === $error['node_name']
		&&
		isset( $error['text'] )
		&&
		preg_match( '/"nonce":".+?",?/', $error['text'] )
	);
	if ( $is_random_script ) {
		$sanitized = true;
	}
	return $sanitized;
}, 10, 2 );

The amp_validation_error_sanitized filter can also be used to pre-accept validation errors that you cannot fix yourself. For example, you may be creating a child theme of a parent theme which has a CSS at-rule which is not allowed in AMP. For example, Firefox supports a @document rule which AMP does not allow, and you can automatically accept the validation error for sanitization by putting the following in your functions.php file:

add_filter( 'amp_validation_error_sanitized', function( $sanitized, $error ) {
	if ( 'illegal_css_at_rule' === $error['code'] && 'document' === $error['at_rule'] ) {
		$sanitized = true;
	}
	return $sanitized;
}, 10, 2 );

Paired Mode

As noted above, paired template mode means that AMP will be served with separate URLs. When in classic mode, the AMP URLs for posts normally end in /amp/ whereas for pages they end in ?amp (and in both cases the “amp” could be customized to be something else). However, when switching from classic to the new paired mode then only the ?amp query param is used exclusively, and the name cannot be customized to be something else. (For more on why, see #1148.)

In addition to enabling the new paired mode via the admin screen, you can also force-enable it via:

add_theme_support( 'amp', array(
	'paired' => true,
) );

This will automatically re-use the main theme templates in the AMP responses when accessed via AMP-specific URLs. You can also supply a template_dir that pulls in AMP templates from another location, in case you want to make significant changes that isn't facilitated by adding is_amp_endpoint() checks in your main template files.

add_theme_support( 'amp', array(
	'template_dir' => 'amp-templates',
) );

Note that this implies the paired mode. However, it is recommended to use is_amp_endpoint() in your main templates to output different content for the AMP and non-AMP versions since this reduces duplication. For example:

<button
	class="menu-toggle"
	aria-controls="primary-menu"
	aria-expanded="false"
	<?php if ( is_amp_endpoint() ) : ?>
		on="tap:AMP.setState( { siteNavigationMenuExpanded: ! siteNavigationMenuExpanded } )"
		[aria-expanded]="siteNavigationMenuExpanded ? 'true' : 'false'"
	<?php endif; ?>
>
	<?php esc_html_e( 'Primary Menu', '_s' ); ?>
</button>

Native (Canonical) Mode

When AMP is in native mode there are no separate AMP-specific URLs on your site. Your entire site will use AMP (demo screencast, example site). There won't be URLs with /amp or ?amp appended. AMP works great on desktop as well as on mobile.

In a theme you've developed or a child theme, you can force it to be native mode in the functions.php via:

add_theme_support( 'amp', array(
	'comments_live_list' => true
) );

You can load AMP-specific templates by specifying a template_dir in native mode like you can do in paired mode via:

add_theme_support( 'amp', array(
   'paired'       => false, // Force native mode, since template_dir implies paired.
   'template_dir' => 'amp-templates',
) );

For examples of custom Native AMP themes, please see AMP Adventures and AMP News. This screencast shows a working example of the AMP News theme.

Theme support parameters for amp

Parameter Type Description
paired bool Flag indicates when paired mode should be used. If absent will default to native unless template_dir is supplied.
template_dir string The path to the custom AMP template(s), relative to your them, to use in native or paired modes.
templates_supported 'all' or array When 'all' then every template in the theme will be supported in AMP. If an array, then it's a mapping of supportable template conditions to whether or not AMP is forced.
comments_live_list boolean Whether amp-live-list will be used for comments. On making a comment, this will display the comment without a page refresh. If this parameter isn't present or is false, the page will refresh on making a comment. This also requires adding markup to your theme, please see this wiki page.

References

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.