AMP or Accelerated Mobile Pages is a Google-backed project that allows you to use restricted HTML to delivery static content quickly. Since AMP HTML is restricted it isn't a fit for every site.
Since this blog is published as static HTML articles it is a good candidate for publishing an AMP version. An open source AMP jekyll plugin was amended to add AMP versions of pages.
The major discovery was that the validation tooling around AMP is awesome. Compare that to Facebook Instant Articles where there is almost no validation tooling (that I could discover at least)…
This didn't feel like a topic that justified several posts so to avoid taking too long this is a bit of a whistle-stop tour of adding AMP pages to this blog.
Jekyll Plugin
The basic idea is adapted from a Jekyll plugin on github.
There are several parts here:
- Adding an AMP layout to the site
- Adding a 'generator' to the Jekyll module
- Adding an 'amp_images' filter
- Adding an 'amp_tweets' filter
This was a very manual process but not particularly onerous. Jekyll proved to be well-made for extension.
Adding an AMP layout
AMP has some required markup. So an amp-post.html was added to the _layouts
folder.
<!DOCTYPE html>
<html
amp
lang="en"
>
<head>
<meta charset="utf-8" />
<title>{{ page.title }}</title>
<link
rel="canonical"
href="{{ site.url }}{{ page.url }}"
/>
<meta
name="viewport"
content="width=device-width,minimum-scale=1,initial-scale=1"
/>
<link
href="https://fonts.googleapis.com/css?family=Khula"
rel="stylesheet"
/>
<style amp-custom>
{% include syntax.css %}
{% capture include_to_scssify %}
{% include main.scss %}
{% endcapture %}
{{ include_to_scssify | scssify }}
</style>
<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
>
{% if page.body contains "florp-wrapper" %}
<script
async
custom-element="amp-twitter"
src="https://cdn.ampproject.org/v0/amp-twitter-0.1.js"
></script>
{% endif %}
<script
async
custom-element="amp-analytics"
src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"
></script>
<script
async
src="https://cdn.ampproject.org/v0.js"
></script>
<link
rel="shortcut icon"
href="/favicon.ico"
/>
{% include openGraph.html %}
</head>
</html>
So, there's an <style amp-boilerplate/>
element which has to be included and the <html amp lang="en">
declaration.
script
elements are declared async. Not just any javascript can be included. Here the amp-analytics script is loaded to allow adding google analytics to the page.
Currently the AMP validator considers including an unnecessary script a warning and not an error but that could change in future. So the amp-twitter script is loaded but only if there is an embedded tweet in the page.
Styles
All styles are included in the head in the <style amp-custom/>
element. It was found to be easier to load all styles that way even on non-AMP pages. There was no measurable difference in page rendering with styles in a linked stylesheet versus in a style tag in the head.
Previously the site used bootstrap v3 for styling (which is burned into my muscle memory). But assessing how much of bootstrap was being used (hardly any) vs. how much was being copied into the head of the page (oodles) for AMP made bootstrap a difficult choice to keep.
Bootstrap is MIT licensed so only the used styles were copied into the site's scss file. Mixed in with the custom styles there are only c400 lines of styles.
Presumably it is not true for all sites that there is no performance difference between an in-page style element and a linked sheet but there's only 12Kb of SCSS to be compiled for this site… and a third of that is for syntax highlighting of code blocks.
The Body
{% capture header %}{% include header.html %}{% endcapture %} {{
header | amp_images: false, 32, 32 }}
<div class="main">
{% include structuredData.html headline=page.title genre=page.category
keywords=page.keywords content=page.body link=page.permalink date=page.date %}
<article>
{% capture post_header %}{% include post_header.html %}{% endcapture %} {{
post_header | amp_images }}
<div class="post">
{{ page.body | markdownify | amp_images | amp_tweets }}
</div>
</article>
</div>
{% capture footer %}{% include footer.html %}{% endcapture %} {{ footer |
amp_images: false, 25, 25 }}
All images have to be fed to the amp_images
filter (see below).
Structured data is apparently not required for AMP but Google's webmaster tools were unhappy if it was not present so the structured data include is added.
The main content is also passed through the amp_tweets
filter as well as the amp_images
filter.
` {{ page.body | markdownify | amp_images | amp_tweets }} ` |
So far so straightforward
Adding a generator
Jekyll generators run as part of Jekyll's build and "create additional content based on your own rules".
This generator is almost exactly the same as found on Github.
require 'thread'
require 'thwait'
# Generates a new AMP post for each existing post
class AmpGenerator < Generator
priority :low
def generate(site)
dir = site.config['ampdir'] || 'amp'
threads = site.posts.docs.map do |post|
Thread.new do
index = AmpPost.new(site, site.source, File.join(dir, post.id), post)
index.render(site.layouts, site.site_payload)
index.write(site.dest)
site.pages << index
end
end
ThreadsWait.all_waits(*threads)
end
end
end
For each of the posts in the site this initializes an AmpPost
as a copy of that non AMP post and adds that new post into an amp folder in the output.
Site build was taking around 18 seconds after adding this generator (and the image and twitter filters). Amending the generator so that it creates a new thread for each AmpPost and then waits for all of those threads to finish reduce build time to around 7 seconds!
Adding an 'amp_images' filter
AMP images must be given an explicit size. And this filter, which is unchanged from that found on github, uses nokogiri to find each img element and convert it to an amp-image element.
So markup like
<p>
<img
src="/images/yarn-desc.png"
alt="Yarn description"
/>
</p>
becomes
<p>
<amp-img
src="/images/yarn-desc.png"
alt="Yarn description"
width="900"
height="304"
layout="responsive"
>
</amp-img>
</p>
Adding an 'amp_tweets' filter
If you want to embed a tweet in a blog post (and I, for my sins, often do) then twitter provide HTML something like
<blockquote
class="twitter-tweet"
data-lang="en"
>
<p
lang="en"
dir="ltr"
>
<a href="https://twitter.com/some_user">@some_user</a> the content content
@sender (@sender)
<a href="https://twitter.com/sender/status/IDFORTHETWEET"
>August 20, 2016</a
>
</p>
</blockquote>
<script
async=""
defer=""
src="//platform.twitter.com/widgets.js"
charset="utf-8"
></script>
AMP insists this element has a known height and width so that has to be manually edited to
<div
class="florp-wrapper"
data-width="292"
data-height="350"
>
<blockquote
class="twitter-tweet"
data-lang="en"
>
<p
lang="en"
dir="ltr"
>
<a href="https://twitter.com/some_user">@some_user</a> the content content
@sender (@sender)
<a href="https://twitter.com/sender/status/IDFORTHETWEET"
>August 20, 2016</a
>
</p>
</blockquote>
<script
async=""
defer=""
src="//platform.twitter.com/widgets.js"
charset="utf-8"
></script>
</div>
The amp_tweet filter then uses that .florp-wrapper
class to find each tweet and convert it to an amp-twitter element.
<div
class="florp-wrapper"
data-width="292"
data-height="350"
>
<div>
<amp-twitter
layout="responsive"
data-tweetid="IDFORTHETWEET"
width="292"
height="350"
></amp-twitter>
</div>
</div>
The necessity to manually remember to wrap the embedded tweets in a div with the correct class is the least nice part of this whole process (but it's not the worst thing in the world).
(the tweets aren't really wrapped with florp-wrapper
but using the real class meant the script was included and so failed AMP validation :/)
AMP Validation
The AMP validator is fudging awesome! It was invaluable in figuring out if I'd set this all up correctly and then identifying old posts which were only imported HTML and not Markdown that Jekyll was building. Those old posts held the majority of the AMP issues identified.
You can
- paste the generated AMP html directly to the validator.
- load via URL
- have it as a browser extension
- and run it as part of a script
Google Webmaster tools
Google webmaster tools are also, slowly, picking up that the AMP pages are present. Highlighting warnings and errors and linking out to the validator.
And so…
If you're already generating articles using Jekyll it's well worth investigating a little time to get this setup. Either because it'll be interesting to do or because you believe you enough traffic from mobile devices to justify not making those readers wait before they can consume your awesome content.