Setting up Google AMP with Jekyll

This is the second piece in a series posts about improving my blog built on Jekyll. In this post I’m handling implementation of Google AMP.

There are two ways of implementing Google AMP. Either you can completely convert your entire page to use AMP, or you can run a parallel AMP version. There are arguments for both sides, but in my case, my page is already fast, and I’m not fond of the restrictions AMP puts on it, therefore I went with running a parallel version of my articles. While I’m only doing articles in this post, you’re able to run a parallel version of any page you choose.

Setting up the Header

Like most Jekyll sites my page’s overall structure comprises of a number of layouts and includes. To begin, I created a copy of my regular article template and named it amp.html. I want the AMP page to look identical to the non-AMP version, therefore the only thing I changed was the header include, and I added amp to the opening html tag. It looked something like the following.

<!DOCTYPE html>
<html amp lang="en">
  {% raw %}{% include amp-head.html %}{% endraw %}
    {% raw %}{% include header.html %}{% endraw %}
    <main class="content" role="main">
      <div class="wrapper">
        {% raw %}{{ content }}{% endraw %}
    {% raw %}{% include footer.html %}{% endraw %}

I then created the amp-head.html file inside my includes directory and began laying out the requirements set fourth by AMP to get a validating boilerplate.

  <meta charset="utf-8">
  <script async src=""></script>
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
  <link rel="icon" type="image/png" href="{{ site.baseurl }}/assets/images/favicon-32x32.png" sizes="32x32" />
  <link rel="icon" type="image/png" href="{{ site.baseurl }}/assets/images/favicon-16x16.png" sizes="16x16" />
  <link rel="canonical" href="{% raw %}{{ site.baseurl }}{{ page.canonical }}{% endraw %}">
  <link rel="manifest" href="{{ site.baseurl }}/manifest.json">
  <link href=",900|Mate" rel="stylesheet">
  <title>{% raw %}{{ page.title }}{% endraw %} - {% raw %}{{ site.title }}{% endraw %}</title>

  <!-- AMP Boilerplate -->
  <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>


<!-- I'll be utilizing the opening body tag here later for things like amp-analytics and amp-install-serviceworker -->

Canonical Linking Woes

AMP-HTML is different than regular HTML, different enough at least that you need to modify certain things in your content to accommodate it. Because I’m using layouts to adjust how the page is rendered, I duplicated each of my posts and placed them within an amp-html directory, and within it created a secondary _posts directory. This means that every time Jekyll built my site it handled all markdown files within that directory like other posts on the site. While this worked, and each AMP post was rendering with the correct layout and header, it was causing problems elsewhere. I was unable to hide the AMP versions of the articles on the rest of the site whenever I looped over {% raw %}{{ site.posts }}{% endraw %}. It was also creating issues with pagination and in areas where I limit the number of posts to show. After several hours of research and investigation into using jekyll-paginate to handle this, I couldn’t find a solution that was satisfactory for my use-case. I ended up deciding against making the AMP versions behave like their standard counterparts.

Instead I kept the amp-html directory but moved the markdown files up a level, outside of _posts. This is not the cleanest solution, but it’s the most simple that I found. Now when I compile the site I’ll have a set of amp posts available through /amp-html which does not dirty site.posts. In both the AMP and non-AMP articles I updated the front-matter to include a path to each other, and then called upon these. This is required by AMP so the search engine knows where to find the AMP version so it can be properly cached. I also added a boolean in the form of amp_skip to each post just incase I created a post which I didn’t want an AMP version of. Here’s this in practice first in the regular header file with the following.

<!-- AMP Article -->
{% raw %}{% if page.path contains 'posts' %}{% endraw %}
  {% raw %}{% unless page.amp_skip %}{% endraw %}
    <link rel="amphtml" href="{% raw %}{{ site.baseurl }}{% endraw %}/amp-html/{% raw %}{{ page.amp_path }}{% endraw %}.html">
  {% raw %}{% endunless %}{% endraw %}
{% raw %}{% endif %}{% endraw %}

And then in the AMP version.

<link rel="canonical" href="{% raw %}{{ site.baseurl }}{{ page.canonical }}{% endraw %}">

If you’re curious about how the front-matter looks here it is. Ideally I’d like to automate this in the future but my blog is small enough that’s only a minor inconvenience.

layout: post
title:  "AMP Validator Slack Bot ⚡"
date:   2017-03-16 08:00:00 +0100
categories: adn amp
image: /assets/posts/google-amp-bot/amp_bot.png
canonical: "/adn/amp/2017/03/16/amp-validator-cat.html"
amp_path: "2017-03-16-amp-validator-cat"
amp_skip: false

Using Components

Some of my posts use rich media content in the form of an iFrame, either it be a Giphy or YouTube embed. Because iFrame tags are blocked with AMP I had to resort to using an AMP component. These components require a piece of JavaScript in the page header, but cause a warning error if you load them without actually utilizing the component it’s loading for. Fortunately front-matter makes this really easy, inside my amp-head.html file I added the following.

<!-- Include custom AMP components if the post calls for it -->
{% raw %}{% if page.amp_components %}{% endraw %}
  {% raw %}{% for js_file in page.amp_components %}{% endraw %}
    {% raw %}{{ js_file }}{% endraw %}
  {% raw %}{% endfor %}{% endraw %}
{% raw %}{% endif %}{% endraw %}

I can then include the script file within the amp_components front-matter, adding a new line for each component I want to utilize.

layout: amp
amp_skip: false
  - <script async custom-element="amp-youtube" src=""></script>
    width="880" height="415"></amp-youtube>

While as of the time of writing this, including a script for a component that’s not used doesn’t invalidate the AMP document, however the warning does specify that it might do so in the future. This method ensures that you’re only including script files whenever you need them, which is generally a good practice anyway.

AMP Images

Images are another thing with that are handled differently with AMP. They require an amp-img tag, and with my implementation approach this can be a hassle. This meant I had to re-build each image tag every time I wanted to use an image. To save myself some time I decided to create an include called post-image.html and setup some checks to see what layout the content is being rendered in.

<!-- Determines which layout is being used and builds the image accordingly -->
{% raw %}{% if page.layout == 'amp' %}{% endraw %}
    src="{% raw %}{{ site.baseurl }}{{ include.src }}{% endraw %}"
    width="{% raw %}{{ include.width }}{% endraw %}"
    height="{% raw %}{{ include.height }}{% endraw %}"
    alt="{% raw %}{{ include.alt }}{% endraw %}"
  <noscript><img src="{% raw %}{{ include.src }}{% endraw %}" alt="{% raw %}{{ include.alt }}{% endraw %}"></noscript>
{% raw %}{% else %}{% endraw %}
  <img src="{% raw %}{{ include.src }}{% endraw %}" alt="{% raw %}{{ include.alt }}{% endraw %}">
{% raw %}{% endif %}{% endraw %}

AMP requires you to specify a height and width for responsive elements, therefore the include expects a height, width, source and alt data. Going forward this is something I’d likely want to include in my Gulp setup to automatically convert markdown images into these include tags.

{% raw %}{% include post-image.html src="/assets/posts/jekyll-gulp/montezuma_01.jpg" alt="Montezuma eating Octocat" height="800" with="600" %}{% endraw %}

Styling the Page

AMP has some rather strict rules when it comes to CSS, the overall weight can be no more than 50,000 bytes, and there’s a number of restricted styles. You’re also not allowed to link to a stylesheet, instead all styles must be inlined inside the header. I came across this incredibly helpful post by Kevin Sweet who explains how you to include css inside the header file.

Firstly I created a _includes/css directory and added inline.scss which imports my main stylesheet.

@import "../css/main";

I then added the custom style tag required by AMP and used the scssify filter to render the imported SCSS content as CSS.

<style amp-custom>
  {% raw %}{% capture scss_content %}{% endraw %}
    {% raw %}{% include css/inline.scss %}{% endraw %}
  {% raw %}{% endcapture %}{% endraw %}
  {% raw %}{{ scss_content | scssify }}{% endraw %}

Now all of my styles get rendered and inlined in the page header whenever Jekyll builds my site. Because my pages overall CSS is less than 50KB anyway, I decided to inline the styles into my header for the non-AMP version, as there are some performance perks to doing so.

You can append #development=1 to the url to check for validation errors, in my case I had one or two minor things I needed to fix but for the most part everything was good to go.

Up Next: Service Workers

In the future I’d like to improve this setup with some automation either with a Gem plugin or Gulp. If the site was larger, or if there was more than just myself creating content for it, I’d be more inclined to set it up that way to begin with, but right now this approach is only a minor inconvenience and takes me less than a minute to generate the AMP version of an article.

If you have any questions or comments please feel free to reach out to me on Twitter, LinkedIn, or you can send me an email using my contact form.