-
Notifications
You must be signed in to change notification settings - Fork 385
Adding Theme Support
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”:
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 this, see Implementing Interactivity.
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.
Finally, 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).
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:
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:
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:
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.
The amp_validation_error_sanitized
filter can then 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 );
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(
'template_dir' => './',
) );
The value for template_dir
here indicates that you intend to re-use all of the same theme templates as on your non-AMP pages. 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. In the following example, AMP is only made available for singular queries and the templates are loaded from the amp-templates
directory in your theme:
add_theme_support( 'amp', array(
'template_dir' => 'amp-templates',
'available_callback' => 'is_singular',
) );
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>
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
) );
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.
Parameter | Type | Description |
---|---|---|
template_dir |
string | Required. The path to the custom AMP "Paired Mode" template(s), relative to your theme. In the example above, you would add an amp-templates/ directory to the root of your theme, and place template(s) in it, like single.php . |
available_callback |
callable | Recommended. This callback should return a boolean that indicates whether to use "Paired Mode." In the example above, only if the request is_singular() will this look for an AMP template in the template_dir . If this callback returns false , there will be no AMP version of the URL, and it won't look for templates in template_dir . If this available_callback parameter isn't present, it will still look in the template_dir for the AMP templates. |
comments_live_list |
boolean | Optional. 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. |
- Weston Ruter's support post on "Paired Mode"
- Example AMP News theme using native mode.
-
Pull request to add
amp
theme support to_s
.
Notice: Please also see the plugin documentation on amp-wp.org