Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve generation of AMP pages in post-processor #3039

Merged
merged 12 commits into from
Sep 3, 2019
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions includes/amp-helper-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,23 @@ function amp_get_asset_url( $file ) {
* @return string Boilerplate code.
*/
function amp_get_boilerplate_code() {
return '<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style>'
. '<noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>';
$stylesheets = amp_get_boilerplate_stylesheets();
return sprintf( '<style amp-boilerplate>%s</style><noscript><style amp-boilerplate>%s</style></noscript>', $stylesheets[0], $stylesheets[1] );
}

/**
* Get AMP boilerplate stylesheets.
*
* @since 1.3
* @link https://www.ampproject.org/docs/reference/spec#boilerplate
*
* @return string[] Stylesheets, where first is contained in style[amp-boilerplate] and the second in noscript>style[amp-boilerplate].
*/
function amp_get_boilerplate_stylesheets() {
return [
'body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}',
schlessera marked this conversation as resolved.
Show resolved Hide resolved
'body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}',
];
}

/**
Expand Down
20 changes: 20 additions & 0 deletions includes/class-amp-story-post-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,26 @@ public static function register() {

add_action( 'wp_enqueue_scripts', [ __CLASS__, 'add_custom_stories_styles' ] );

add_action( 'amp_story_head', 'rel_canonical' );
add_action( 'amp_story_head', 'amp_add_generator_metadata' );
add_action( 'amp_story_head', 'wp_enqueue_scripts' );
add_action( 'amp_story_head', 'rel_canonical' );

// @todo Create methods/functions for some of these.
add_action(
'amp_story_head',
function() {
// See _wp_render_title_tag().
echo '<title>' . esc_html( wp_get_document_title() ) . '</title>' . "\n";
}
);
add_action(
'amp_story_head',
function() {
wp_styles()->do_items();
}
);

// Remove unnecessary settings.
add_filter( 'block_editor_settings', [ __CLASS__, 'filter_block_editor_settings' ], 10, 2 );

Expand Down
143 changes: 89 additions & 54 deletions includes/class-amp-theme-support.php
Original file line number Diff line number Diff line change
Expand Up @@ -1016,34 +1016,6 @@ static function( $html ) {
1
);

/*
* "AMP HTML documents MUST contain the AMP boilerplate code (head > style[amp-boilerplate] and noscript > style[amp-boilerplate])
* in their head tag." {@link https://www.ampproject.org/docs/fundamentals/spec#required-markup AMP Required markup}
*
* After "Specify the <link> tag for your favicon.", then
* "Specify any custom styles by using the <style amp-custom> tag."
*
* Note that the boilerplate is added at the very end because:
* "Finally, specify the AMP boilerplate code. By putting the boilerplate code last, it prevents custom styles from accidentally
* overriding the boilerplate css rules." {@link https://docs.google.com/document/d/169XUxtSSEJb16NfkrCr9y5lqhUR7vxXEAsNxBzg07fM/edit AMP Hosting Guide}
*
* Other required markup is added in the ensure_required_markup method, including meta charset, meta viewport, and rel=canonical link.
*/
add_action(
'wp_head',
static function() {
echo '<style amp-custom></style>';
},
0
);
add_action(
'wp_head',
static function() {
echo amp_get_boilerplate_code(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
},
PHP_INT_MAX
);

add_action( 'admin_bar_init', [ __CLASS__, 'init_admin_bar' ] );
add_action( 'wp_head', 'amp_add_generator_metadata', 20 );
add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_assets' ], 0 ); // Enqueue before theme's styles.
Expand Down Expand Up @@ -1434,8 +1406,9 @@ static function( $body_classes ) {
*
* @since 0.7
* @link https://www.ampproject.org/docs/reference/spec#required-markup
* @link https://docs.google.com/document/d/169XUxtSSEJb16NfkrCr9y5lqhUR7vxXEAsNxBzg07fM/edit#heading=h.2ha259c3ffos
* @link https://amp.dev/documentation/guides-and-tutorials/optimize-and-measure/optimize_amp/
* @todo All of this might be better placed inside of a sanitizer.
* @todo Consider removing any scripts that are not among the $script_handles.
*
* @param DOMDocument $dom Document.
* @param string[] $script_handles AMP script handles for components identified during output buffering.
Expand All @@ -1447,6 +1420,8 @@ public static function ensure_required_markup( DOMDocument $dom, $script_handles
* @var DOMElement $meta
* @var DOMElement $script
* @var DOMElement $link
* @var DOMElement $style
* @var DOMElement $noscript
*/

$xpath = new DOMXPath( $dom );
Expand Down Expand Up @@ -1496,9 +1471,9 @@ public static function ensure_required_markup( DOMDocument $dom, $script_handles
* there are a few basic optimizations that you can apply. The key is to structure the <head> section
* in a way so that all render-blocking scripts and custom fonts load as fast as possible."
*
* "The first tag should be the meta charset tag, followed by any remaining meta tags."
* "1. The first tag should be the meta charset tag, followed by any remaining meta tags."
*
* {@link https://docs.google.com/document/d/169XUxtSSEJb16NfkrCr9y5lqhUR7vxXEAsNxBzg07fM/edit#heading=h.2ha259c3ffos Optimize the AMP Runtime loading}
* {@link https://amp.dev/documentation/guides-and-tutorials/optimize-and-measure/optimize_amp/ Optimize the AMP Runtime loading}
*/
$meta_charset = null;
$meta_viewport = null;
Expand Down Expand Up @@ -1606,10 +1581,10 @@ public static function ensure_required_markup( DOMDocument $dom, $script_handles

/* phpcs:ignore Squiz.PHP.CommentedOutCode.Found
*
* "Next, preload the AMP runtime v0.js <script> tag with <link as=script href=https://cdn.ampproject.org/v0.js rel=preload>.
* "2. Next, preload the AMP runtime v0.js <script> tag with <link as=script href=https://cdn.ampproject.org/v0.js rel=preload>.
* The AMP runtime should start downloading as soon as possible because the AMP boilerplate hides the document via body { visibility:hidden }
* until the AMP runtime has loaded. Preloading the AMP runtime tells the browser to download the script with a higher priority."
* {@link https://docs.google.com/document/d/169XUxtSSEJb16NfkrCr9y5lqhUR7vxXEAsNxBzg07fM/edit#heading=h.2ha259c3ffos Optimize the AMP Runtime loading}
* {@link https://amp.dev/documentation/guides-and-tutorials/optimize-and-measure/optimize_amp/ Optimize the AMP Runtime loading}
*/
$prioritized_preloads = [];
if ( ! isset( $links['preload'] ) ) {
Expand All @@ -1626,6 +1601,10 @@ public static function ensure_required_markup( DOMDocument $dom, $script_handles
]
);

/*
* "3. If your page includes render-delaying extensions (e.g., amp-experiment, amp-dynamic-css-classes, amp-story),
* preload those extensions as they're required by the AMP runtime for rendering the page."
*/
$amp_script_handles = array_keys( $amp_scripts );
foreach ( array_intersect( $render_delaying_extensions, $amp_script_handles ) as $script_handle ) {
if ( ! in_array( $script_handle, $render_delaying_extensions, true ) ) {
Expand All @@ -1643,6 +1622,12 @@ public static function ensure_required_markup( DOMDocument $dom, $script_handles
}
$links['preload'] = array_merge( $prioritized_preloads, $links['preload'] );

/*
* "4. Use preconnect to speedup the connection to other origin where the full resource URL is not known ahead of time,
* for example, when using Google Fonts."
*
* Note that \AMP_Style_Sanitizer::process_link_element() will ensure preconnect links for Google Fonts are present.
*/
$link_relations = [ 'preconnect', 'dns-prefetch', 'preload', 'prerender', 'prefetch' ];
foreach ( $link_relations as $rel ) {
if ( ! isset( $links[ $rel ] ) ) {
Expand All @@ -1657,41 +1642,87 @@ public static function ensure_required_markup( DOMDocument $dom, $script_handles
}
}

/*
* "Specify the <script> tags for render-delaying extensions (e.g., amp-experiment, amp-dynamic-css-classes, and amp-story)."
* "Specify the <script> tags for remaining extensions (e.g., amp-bind, ...). These extensions are not render-delaying and therefore
* should not be preloaded because they might take away important bandwidth for the initial render."
* {@link https://docs.google.com/document/d/169XUxtSSEJb16NfkrCr9y5lqhUR7vxXEAsNxBzg07fM/edit AMP Hosting Guide}
*/
// "5. Load the AMP runtime."
if ( isset( $amp_scripts['amp-runtime'] ) ) {
$ordered_scripts['amp-runtime'] = $amp_scripts['amp-runtime'];
westonruter marked this conversation as resolved.
Show resolved Hide resolved
} else {
$script = $dom->createElement( 'script' );
$script->setAttribute( 'async', '' );
$script->setAttribute( 'src', $runtime_src );
$ordered_scripts['amp-runtime'] = $script;
}

/*
* "6. Specify the <script> tags for render-delaying extensions (e.g., amp-experiment amp-dynamic-css-classes and amp-story"
*
* {@link https://amp.dev/documentation/guides-and-tutorials/optimize-and-measure/optimize_amp/ AMP Hosting Guide}
*/
foreach ( $render_delaying_extensions as $extension ) {
if ( isset( $amp_scripts[ $extension ] ) ) {
$ordered_scripts[ $extension ] = $amp_scripts[ $extension ];
unset( $amp_scripts[ $extension ] );
}
}

/*
* "7. Specify the <script> tags for remaining extensions (e.g., amp-bind ...). These extensions are not render-delaying
* and therefore should not be preloaded as they might take away important bandwidth for the initial render."
*/
$ordered_scripts = array_merge( $ordered_scripts, $amp_scripts );
foreach ( $ordered_scripts as $ordered_script ) {
$head->insertBefore( $ordered_script, $previous_node->nextSibling );
$previous_node = $ordered_script;
}

/*
* "Specify the <link> tag for your favicon."
* {@link https://docs.google.com/document/d/169XUxtSSEJb16NfkrCr9y5lqhUR7vxXEAsNxBzg07fM/edit AMP Hosting Guide}
* "8. Specify any custom styles by using the <style amp-custom> tag."
*/
if ( isset( $links['icon'] ) ) {
foreach ( $links['icon'] as $link ) {
$link->parentNode->removeChild( $link ); // So we can move it.
$head->insertBefore( $link, $previous_node->nextSibling );
$previous_node = $link;
$style = $xpath->query( './style[ @amp-custom ]', $head )->item( 0 );
if ( $style ) {
// Ensure the CSS manifest comment remains before style[amp-custom].
schlessera marked this conversation as resolved.
Show resolved Hide resolved
if ( $style->previousSibling instanceof DOMComment ) {
$comment = $style->previousSibling;
$comment->parentNode->removeChild( $comment );
$head->insertBefore( $comment, $previous_node->nextSibling );
$previous_node = $comment;
}

$style->parentNode->removeChild( $style );
$head->insertBefore( $style, $previous_node->nextSibling );
$previous_node = $style;
}

/*
* "9. Add any other tags allowed in the <head> section. In particular, any external fonts should go last since
* they block rendering."
*/

/*
* "10. Finally, specify the AMP boilerplate code. By putting the boilerplate code last, it prevents custom styles
* from accidentally overriding the boilerplate css rules."
*/
$style = $xpath->query( './style[ @amp-boilerplate ]', $head )->item( 0 );
if ( ! $style ) {
$style = $dom->createElement( 'style' );
$style->setAttribute( 'amp-boilerplate', '' );
$style->appendChild( $dom->createTextNode( amp_get_boilerplate_stylesheets()[0] ) );
} else {
$style->parentNode->removeChild( $style ); // So we can move it.
}
$head->appendChild( $style );

$noscript = $xpath->query( './noscript[ style[ @amp-boilerplate ] ]', $head )->item( 0 );
if ( ! $noscript ) {
$noscript = $dom->createElement( 'noscript' );
$style = $dom->createElement( 'style' );
$style->setAttribute( 'amp-boilerplate', '' );
$style->appendChild( $dom->createTextNode( amp_get_boilerplate_stylesheets()[1] ) );
$noscript->appendChild( $style );
} else {
$noscript->parentNode->removeChild( $noscript ); // So we can move it.
}
$head->appendChild( $noscript );

// Note the style[amp-custom] and style[amp-boilerplate] are output in the add_hooks() method.
unset( $previous_node );
}

Expand Down Expand Up @@ -1840,8 +1871,11 @@ public static function prepare_response( $response, $args = [] ) {
);
}

// Abort if the response was not HTML.
if ( 'text/html' !== substr( AMP_HTTP::get_response_content_type(), 0, 9 ) || '<' !== substr( ltrim( $response ), 0, 1 ) ) {
/*
* Abort if the response was not HTML. To be post-processed as an AMP page, the output-buffered document must
* have the HTML mime type and it must start <html> followed by <head> tag (with whitespace, doctype, and comments optionally interspersed).
westonruter marked this conversation as resolved.
Show resolved Hide resolved
*/
if ( 'text/html' !== substr( AMP_HTTP::get_response_content_type(), 0, 9 ) || ! preg_match( '#^(?:<!.*?>|\s+)*<html.*?>(?:<!.*?>|\s+)*<head.*?>#is', $response ) ) {
schlessera marked this conversation as resolved.
Show resolved Hide resolved
return $response;
}

Expand Down Expand Up @@ -2025,11 +2059,14 @@ public static function prepare_response( $response, $args = [] ) {
* See <https://www.w3.org/International/questions/qa-html-encoding-declarations>.
*/
if ( ! preg_match( '#<meta[^>]+charset=#i', substr( $response, 0, 1024 ) ) ) {
$meta_charset = sprintf( '<meta charset="%s">', esc_attr( get_bloginfo( 'charset' ) ) );

$response = preg_replace(
'/(<head[^>]*>)/i',
'$1' . sprintf( '<meta charset="%s">', esc_attr( get_bloginfo( 'charset' ) ) ),
'/(<head.*?>)/is',
schlessera marked this conversation as resolved.
Show resolved Hide resolved
'$1' . $meta_charset,
$response,
1
1,
$count
);
}

Expand Down Expand Up @@ -2253,8 +2290,6 @@ public static function whitelist_layout_in_wp_kses_allowed_html( $context ) {
* @return void
*/
public static function enqueue_assets() {
wp_enqueue_script( 'amp-runtime' );

// Enqueue default styles expected by sanitizer.
wp_enqueue_style( 'amp-default', amp_get_asset_url( 'css/amp-default.css' ), [], AMP__VERSION );
wp_styles()->add_data( 'amp-default', 'rtl', 'replace' );
Expand Down
17 changes: 8 additions & 9 deletions includes/templates/single-amp_story.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,25 @@

the_post();

$metadata = amp_get_schemaorg_metadata();
?>
<!DOCTYPE html>
<html amp <?php language_attributes(); ?>>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<title><?php echo esc_html( wp_get_document_title() ); ?></title>

<?php
wp_enqueue_scripts();
wp_scripts()->do_items( [ 'amp-runtime' ] ); // @todo Duplicate with AMP_Theme_Support::enqueue_assets().
wp_styles()->do_items();
/**
* Prints scripts or data in the head tag on the front end.
*
* @since 1.3
*/
do_action( 'amp_story_head' );
?>
<?php rel_canonical(); ?>
<?php amp_add_generator_metadata(); ?>
<script type="application/ld+json"><?php echo wp_json_encode( $metadata, JSON_UNESCAPED_UNICODE ); ?></script>
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
</head>
<body>
<?php
$metadata = amp_get_schemaorg_metadata();
if ( isset( $metadata['publisher']['logo']['url'] ) ) {
$publisher_logo_src = $metadata['publisher']['logo']['url'];
} elseif ( isset( $metadata['publisher']['logo'] ) && is_string( $metadata['publisher']['logo'] ) ) {
Expand Down
33 changes: 33 additions & 0 deletions tests/php/test-amp-helper-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,39 @@ public function capture_filter_call( $value ) {
return $value;
}

/**
* Test amp_get_asset_url.
*
* @covers ::amp_get_asset_url()
*/
public function test_amp_get_asset_url() {
$this->assertStringEndsWith( '/assets/foo.jpg', amp_get_asset_url( 'foo.jpg' ) );
}

/**
* Test amp_get_boilerplate_code.
*
* @covers ::amp_get_boilerplate_code()
*/
public function test_amp_get_boilerplate_code() {
$boilerplate_code = amp_get_boilerplate_code();
$this->assertStringStartsWith( '<style amp-boilerplate>', $boilerplate_code );
$this->assertContains( '<noscript><style amp-boilerplate>', $boilerplate_code );
}

/**
* Test amp_get_boilerplate_stylesheets.
*
* @covers ::amp_get_boilerplate_stylesheets()
*/
public function test_amp_get_boilerplate_stylesheets() {
$stylesheets = amp_get_boilerplate_stylesheets();
$this->assertInternalType( 'array', $stylesheets );
$this->assertCount( 2, $stylesheets );
$this->assertContains( 'body{-webkit-animation:-amp-start', $stylesheets[0] );
$this->assertContains( 'body{-webkit-animation:none', $stylesheets[1] );
}

/**
* Test amp_add_generator_metadata.
*
Expand Down
Loading