Skip to content

Commit

Permalink
Merge pull request #857 from Automattic/add/canonical-head
Browse files Browse the repository at this point in the history
Add initial canonical AMP support
  • Loading branch information
westonruter committed Jan 12, 2018
2 parents 34a84d9 + b39d440 commit f2b1831
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 9 deletions.
16 changes: 14 additions & 2 deletions amp.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,25 @@ function amp_force_query_var_value( $query_vars ) {
* @return void
*/
function amp_maybe_add_actions() {
if ( amp_is_canonical() || ! is_singular() || is_feed() ) {

// Add hooks for when a themes that support AMP.
if ( current_theme_supports( 'amp' ) ) {
if ( amp_is_canonical() || is_amp_endpoint() ) {
AMP_Theme_Support::register_hooks();
} else {
AMP_Frontend_Actions::register_hooks();
}
return;
}

// The remaining logic here is for paired mode running in themes that don't support AMP, the template system in AMP<=0.6.
if ( ! is_singular() || is_feed() ) {
return;
}

$is_amp_endpoint = is_amp_endpoint();

// Cannot use `get_queried_object` before canonical redirect; see https://core.trac.wordpress.org/ticket/35344
// Cannot use `get_queried_object` before canonical redirect; see <https://core.trac.wordpress.org/ticket/35344>.
global $wp_query;
$post = $wp_query->post;

Expand Down
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@
"homepage": "https://github.com/Automattic/amp-wp",
"type": "wordpress-plugin",
"license": "GPL-2.0",
"version": "0.7.0"
"version": "0.7.0",
"require-dev": {
"wp-coding-standards/wpcs": "^0.14.0",
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.4",
"wimg/php-compatibility": "^8.1"
}
}
37 changes: 34 additions & 3 deletions includes/actions/class-amp-frontend-actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/**
* Class AMP_Frontend_Actions
*
* Callbacks for adding AMP-related things to the main theme
* Callbacks for adding AMP-related things to the main theme in non-canonical mode theme.
*/
class AMP_Frontend_Actions {

Expand All @@ -26,7 +26,38 @@ public static function add_canonical() {
if ( false === apply_filters( 'add_canonical_link', true ) ) {
return;
}
$amp_url = amp_get_permalink( get_queried_object_id() );
printf( '<link rel="amphtml" href="%s" />', esc_url( $amp_url ) );
$amp_url = self::get_current_amphtml_url();
if ( ! empty( $amp_url ) ) {
printf( '<link rel="amphtml" href="%s" />', esc_url( $amp_url ) );
}
}

/**
* Get the amphtml URL for the current request.
*
* @todo Put this function in includes/amp-helper-functions.php?
* @return string|null URL or null if AMP version is not available.
*/
public static function get_current_amphtml_url() {
if ( is_singular() ) {
return amp_get_permalink( get_queried_object_id() );
}

// @todo Get callback from get_theme_support( 'amp' ) to determine whether AMP is allowed for current request. See <https://github.com/Automattic/amp-wp/issues/849>.
if ( ! current_theme_supports( 'amp' ) ) {
return null;
}

$amp_url = '';
$home_url = wp_parse_url( home_url() );
if ( isset( $home_url['scheme'] ) ) {
$amp_url .= $home_url['scheme'] . ':';
}
$amp_url .= '//' . $home_url['host'];
if ( ! empty( $home_url['port'] ) ) {
$amp_url .= ':' . $home_url['port'];
}
$amp_url .= esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
return add_query_arg( AMP_QUERY_VAR, '', $amp_url );
}
}
4 changes: 1 addition & 3 deletions includes/actions/class-amp-paired-post-actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,7 @@ public static function add_fonts( $amp_template ) {
* @param AMP_Post_Template $amp_template Template.
*/
public static function add_boilerplate_css( $amp_template ) {
?>
<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>
<?php
amp_print_boilerplate_code();
}

/**
Expand Down
10 changes: 10 additions & 0 deletions includes/amp-helper-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,13 @@ function is_amp_endpoint() {
function amp_get_asset_url( $file ) {
return plugins_url( sprintf( 'assets/%s', $file ), AMP__FILE__ );
}

/**
* Print AMP boilerplate code.
*
* @link https://www.ampproject.org/docs/reference/spec#boilerplate
*/
function amp_print_boilerplate_code() {
echo '<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>';
echo '<noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>';
}
1 change: 1 addition & 0 deletions includes/class-amp-autoloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class AMP_Autoloader {
'AMP_Actions' => 'includes/actions/class-amp-actions',
'AMP_Frontend_Actions' => 'includes/actions/class-amp-frontend-actions',
'AMP_Paired_Post_Actions' => 'includes/actions/class-amp-paired-post-actions',
'AMP_Theme_Support' => 'includes/class-amp-theme-support',
'AMP_Template_Customizer' => 'includes/admin/class-amp-customizer',
'AMP_Post_Meta_Box' => 'includes/admin/class-amp-post-meta-box',
'AMP_Post_Type_Support' => 'includes/class-amp-post-type-support',
Expand Down
229 changes: 229 additions & 0 deletions includes/class-amp-theme-support.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<?php
/**
* Class AMP_Theme_Support
*
* @package AMP
*/

/**
* Class AMP_Theme_Support
*
* Callbacks for adding AMP-related things when theme support is added.
*/
class AMP_Theme_Support {

const COMPONENT_SCRIPTS_PLACEHOLDER = '<!--AMP_COMPONENT_SCRIPTS_PLACEHOLDER-->';

/**
* Register hooks.
*/
public static function register_hooks() {

// Remove core actions which are invalid AMP.
remove_action( 'wp_head', 'locale_stylesheet' );
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_head', 'wp_print_styles', 8 );
remove_action( 'wp_head', 'wp_print_head_scripts', 9 );
remove_action( 'wp_head', 'wp_custom_css_cb', 101 );
remove_action( 'wp_footer', 'wp_print_footer_scripts', 20 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );

// Replace core's canonical link functionality with one that outputs links for non-singular queries as well. See WP Core #18660.
remove_action( 'wp_head', 'rel_canonical' );
add_action( 'wp_head', array( __CLASS__, 'add_canonical_link' ), 1 );

// @todo Add add_schemaorg_metadata(), add_analytics_data(), etc.
// Add additional markup required by AMP <https://www.ampproject.org/docs/reference/spec#required-markup>.
add_action( 'wp_head', array( __CLASS__, 'add_meta_charset' ), 0 );
add_action( 'wp_head', array( __CLASS__, 'add_meta_viewport' ), 2 );
add_action( 'wp_head', 'amp_print_boilerplate_code', 3 );
add_action( 'wp_head', array( __CLASS__, 'add_scripts' ), 4 );
add_action( 'wp_head', array( __CLASS__, 'add_styles' ), 5 );
add_action( 'wp_head', array( __CLASS__, 'add_meta_generator' ), 6 );

/*
* Disable admin bar because admin-bar.css (28K) and Dashicons (48K) alone
* combine to surpass the 50K limit imposed for the amp-custom style.
*/
add_filter( 'show_admin_bar', '__return_false', 100 );

// Start output buffering at very low priority for sake of plugins and themes that use template_redirect instead of template_include.
add_action( 'template_redirect', array( __CLASS__, 'start_output_buffering' ), 0 );

// @todo Add output buffering.
// @todo Add character conversion.
}

/**
* Print meta charset tag.
*
* @link https://www.ampproject.org/docs/reference/spec#chrs
*/
public static function add_meta_charset() {
echo '<meta charset="utf-8">';
}

/**
* Print meta charset tag.
*
* @link https://www.ampproject.org/docs/reference/spec#vprt
*/
public static function add_meta_viewport() {
echo '<meta name="viewport" content="width=device-width,minimum-scale=1">';
}

/**
* Print AMP script and placeholder for others.
*
* @link https://www.ampproject.org/docs/reference/spec#scrpt
*/
public static function add_scripts() {
echo '<script async src="https://cdn.ampproject.org/v0.js"></script>'; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript

// Replaced after output buffering with all AMP component scripts.
echo self::COMPONENT_SCRIPTS_PLACEHOLDER; // phpcs:ignore WordPress.Security.EscapeOutput, WordPress.XSS.EscapeOutput
}

/**
* Add canonical link.
*
* Replaces `rel_canonical()` which only outputs canonical URLs for singular posts and pages.
* This can be removed once WP Core #18660 lands.
*
* @link https://www.ampproject.org/docs/reference/spec#canon.
* @link https://core.trac.wordpress.org/ticket/18660
*
* @see rel_canonical()
* @global WP $wp
* @global WP_Rewrite $wp_rewrite
*/
public static function add_canonical_link() {
global $wp, $wp_rewrite;

$url = null;
if ( is_singular() ) {
$url = wp_get_canonical_url();
}

// For non-singular queries, make use of the request URI and public query vars to determine canonical URL.
if ( empty( $url ) ) {
$added_query_vars = $wp->query_vars;
if ( ! $wp_rewrite->permalink_structure || empty( $wp->request ) ) {
$url = home_url( '/' );
} else {
$url = home_url( user_trailingslashit( $wp->request ) );
parse_str( $wp->matched_query, $matched_query_vars );
foreach ( $wp->query_vars as $key => $value ) {

// Remove query vars that were matched in the rewrite rules for the request.
if ( isset( $matched_query_vars[ $key ] ) ) {
unset( $added_query_vars[ $key ] );
}
}
}
}

if ( ! empty( $added_query_vars ) ) {
$url = add_query_arg( $added_query_vars, $url );
}

if ( ! amp_is_canonical() ) {

// Strip endpoint.
$url = preg_replace( ':/' . preg_quote( AMP_QUERY_VAR, ':' ) . '(?=/?(\?|#|$)):', '', $url );

// Strip query var.
$url = remove_query_arg( AMP_QUERY_VAR, $url );
}

echo '<link rel="canonical" href="' . esc_url( $url ) . '">' . "\n";
}

/**
* Print Custom AMP styles.
*
* @see wp_custom_css_cb()
*/
public static function add_styles() {
echo '<style amp-custom>';

// @todo Grab source of all enqueued styles and concatenate here?
// @todo Print contents of get_locale_stylesheet_uri()?
// @todo Allow this to be filtered after output buffering is complete so additional styles can be added by widgets and other components just-in-time?
$path = get_template_directory() . '/style.css'; // @todo Honor filter in get_stylesheet_directory_uri()? Style must be local.
$css = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- It's not a remote file.
echo wp_strip_all_tags( $css ); // WPCS: XSS OK.

// Implement AMP version of wp_custom_css_cb().
$custom_css = trim( wp_get_custom_css() );
if ( ! empty( $custom_css ) ) {
echo '/* start:wp_get_custom_css */';
echo wp_strip_all_tags( wp_get_custom_css() ); // WPCS: XSS OK.
echo '/* end:wp_get_custom_css */';
}
echo '</style>';
}

/**
* Print AMP meta generator tag.
*/
public static function add_meta_generator() {
printf( '<meta name="generator" content="%s" />', esc_attr( 'AMP Plugin v' . AMP__VERSION ) );
}

/**
* Determine required AMP scripts.
*
* @param string $html Output HTML.
* @return string Scripts to inject into the HEAD.
*/
public static function get_required_amp_scripts( $html ) {

// @todo This should be integrated with the existing Sanitizer classes so that duplication is not done here.
$amp_scripts = array(
'amp-form' => array(
'pattern' => '#<(form|input)\b#i',
'source' => 'https://cdn.ampproject.org/v0/amp-form-0.1.js',
),
// @todo Add more.
);

$scripts = '';
foreach ( $amp_scripts as $component => $props ) {
if ( preg_match( '#<(form|input)\b#i', $html ) ) {
$scripts .= sprintf(
'<script async custom-element="%s" src="%s"></script>', // phpcs:ignore WordPress.WP.EnqueuedResources, WordPress.XSS.EscapeOutput.OutputNotEscaped
$component,
$props['source']
);
}
}

return $scripts;
}

/**
* Start output buffering.
*/
public static function start_output_buffering() {
ob_start( array( __CLASS__, 'finish_output_buffering' ) );
}

/**
* Finish output buffering.
*
* @param string $output Buffered output.
* @return string Finalized output.
*/
public static function finish_output_buffering( $output ) {
$output = preg_replace(
'#' . preg_quote( self::COMPONENT_SCRIPTS_PLACEHOLDER, '#' ) . '#',
self::get_required_amp_scripts( $output ),
$output,
1
);

// @todo Add more validation checking and potentially the whitelist sanitizer.
return $output;
}
}
6 changes: 6 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
</properties>
</rule>

<!-- Include sniffs for PHP cross-version compatibility. -->
<config name="testVersion" value="5.2-99.0"/>
<rule ref="PHPCompatibility">
<exclude-pattern>bin/*</exclude-pattern>
</rule>

<arg value="s"/>
<arg name="extensions" value="php"/>
<file>.</file>
Expand Down

0 comments on commit f2b1831

Please sign in to comment.