Skip to content

Instantly share code, notes, and snippets.

@westonruter
Last active May 10, 2021 17:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save westonruter/50abd71d9becb5a34f01136052effef5 to your computer and use it in GitHub Desktop.
Save westonruter/50abd71d9becb5a34f01136052effef5 to your computer and use it in GitHub Desktop.
<?php
/**
* AMP Force Hero Image Preloading plugin bootstrap.
*
* @package Google\AmpHeroImagePreloading
* @author Weston Ruter, Google
* @license GPL-2.0-or-later
* @copyright 2021 Google Inc.
*
* @wordpress-plugin
* Plugin Name: AMP Force Hero Image Preloading
* Plugin URI: https://gist.github.com/westonruter/50abd71d9becb5a34f01136052effef5
* Description: Forcing hero images to be preloaded, even when they are responsive and lack media attributes. This is a workaround until <a href="https://github.com/ampproject/amp-toolbox/issues/1230">amp-toolbox#1230</a> is implemented.
* Version: 0.1
* Author: Weston Ruter, Google
* Author URI: https://weston.ruter.net/
* License: GNU General Public License v2 (or later)
* License URI: http://www.gnu.org/licenses/gpl-2.0.html
* Gist Plugin URI: https://gist.github.com/westonruter/50abd71d9becb5a34f01136052effef5
*/
namespace Google\AmpForceHeroImagePreloading;
use AmpProject\Optimizer;
use AmpProject\Optimizer\Transformer\ReorderHead;
/**
* Filter configuration array to register the transformer.
*
* @param array $configuration Associative array of configuration data.
* @return array Configuration.
*/
function filter_amp_optimizer_config( array $configuration ) {
require_once __DIR__ . '/ForcePreloadHeroImage.php';
$transformers = $configuration[ Optimizer\Configuration::KEY_TRANSFORMERS ];
// Add ForcePreloadHeroImage right before the ReorderHead transformer.
$reorder_head_position = array_search( ReorderHead::class, $transformers );
if ( false !== $reorder_head_position ) {
array_splice( $transformers, $reorder_head_position, 0, [ ForcePreloadHeroImage::class ] );
} else {
$transformers[] = ForcePreloadHeroImage::class;
}
$configuration[ Optimizer\Configuration::KEY_TRANSFORMERS ] = array_values( $transformers );
return $configuration;
}
if ( empty( $_GET['amp_disable_force_preload_hero_image'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
add_filter( 'amp_optimizer_config', __NAMESPACE__ . '\filter_amp_optimizer_config' );
}
<?php
/**
* Force preloading the hero images.
*
* @package Google\AmpHeroImagePreloading
*/
namespace Google\AmpForceHeroImagePreloading;
use AmpProject\Dom\Document;
use AmpProject\Optimizer\ErrorCollection;
use AmpProject\Optimizer\Transformer;
use AmpProject\Attribute;
use AmpProject\Tag;
use AmpProject\Dom\Element;
use AmpProject\RequestDestination;
use AmpProject\Extension;
use AmpProject\Url;
use AmpProject\Optimizer\HeroImage;
/**
* Transformer which rewrites image URLs to point the AMP Cache as a CDN.
*/
final class ForcePreloadHeroImage implements Transformer {
/**
* Reference node to attach preload links to.
*
* @var Element|null
*/
private $preload_reference_node;
/**
* Apply transformations to the provided DOM document.
*
* @param Document $document DOM document to apply the transformations to.
* @param ErrorCollection $errors Collection of errors that are collected during transformation.
*/
public function transform( Document $document, ErrorCollection $errors ) {
$elements = $document->xpath->query(
'.//amp-img[ @data-hero and @i-amphtml-ssr ][ not( img/@loading ) or "lazy" != img/@loading ]',
$document->body
);
foreach ( $elements as $element ) {
/** @var Element $element */
$src = $element->getAttribute( Attribute::SRC );
if ( Extension::IMG === $element->tagName && ( new Url( $src ) )->isValidNonDataUrl() ) {
$hero_image = new HeroImage(
$src,
$element->getAttribute( Attribute::MEDIA ),
$element->getAttribute( Attribute::SRCSET ),
$element
);
$this->generatePreload( $hero_image, $document );
}
}
}
/**
* Generate the preload link for a given hero image.
*
* This is adapted from the same method in the PreloadHeroImage transformer in amp-toolbox-php.
*
* @see Transformer\PreloadHeroImage::generatePreload()
* @link https://github.com/ampproject/amp-toolbox-php/blob/86d53aa73edef1aafd748fb94646af6859414e2a/src/Optimizer/Transformer/PreloadHeroImage.php#L499-L552
*
* @param HeroImage $hero_image Hero image to generate the preload link for.
* @param Document $document Document to generate the preload link in.
*/
private function generatePreload( HeroImage $hero_image, Document $document ) {
if ( $this->hasExistingImagePreload( $document, $hero_image->getSrc() ) ) {
return;
}
if ( ! $this->preload_reference_node ) {
$this->preload_reference_node = $document->viewport;
}
$preload = $document->createElement( Tag::LINK );
$preload->setAttribute( Attribute::REL, Attribute::REL_PRELOAD );
$preload->setAttribute( Attribute::HREF, $hero_image->getSrc() );
$preload->setAttribute( Attribute::AS_, RequestDestination::IMAGE );
$preload->appendChild( $document->createAttribute( Attribute::DATA_HERO ) );
if ( $hero_image->getSrcset() ) {
$preload->setAttribute( Attribute::IMAGESRCSET, $hero_image->getSrcset() );
$img = $hero_image->getAmpImg();
if ( $img && $img->hasAttribute( Attribute::SIZES ) ) {
$preload->setAttribute( Attribute::IMAGESIZES, $img->getAttribute( Attribute::SIZES ) );
}
}
$media = $hero_image->getMedia();
if ( $media ) {
$preload->setAttribute( Attribute::MEDIA, $hero_image->getMedia() );
}
if ( $this->preload_reference_node ) {
$this->preload_reference_node->parentNode->insertBefore(
$preload,
$this->preload_reference_node->nextSibling
);
} else {
$document->head->appendChild( $preload );
}
$this->preload_reference_node = $preload;
}
/**
* Check whether an existing preload link exists for a given src.
*
* This is adapted from the same method in the PreloadHeroImage transformer in amp-toolbox-php.
*
* @see Transformer\PreloadHeroImage::hasExistingImagePreload()
* @link https://github.com/ampproject/amp-toolbox-php/blob/86d53aa73edef1aafd748fb94646af6859414e2a/src/Optimizer/Transformer/PreloadHeroImage.php#L611-L639
*
* @param Document $document Document in which to check for an existing preload.
* @param string $src Preload URL to look for.
*
* @return bool Whether an existing preload already exists.
*/
private function hasExistingImagePreload( Document $document, $src ) {
foreach ( $document->head->childNodes as $node ) {
if ( ! $node instanceof Element ) {
continue;
}
if ( $node->getAttribute( Attribute::REL ) !== Attribute::REL_PRELOAD ) {
continue;
}
if ( $node->getAttribute( Attribute::AS_ ) !== RequestDestination::IMAGE ) {
continue;
}
if ( $node->getAttribute( Attribute::HREF ) === $src ) {
return true;
}
}
return false;
}
}
@westonruter
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment