24

This is just a general question. Has anyone implemented AMP with Sitecore before?

Is it possible, how difficult and could you point me in the direction of documentation?

4
  • I've also looked into it, haven't seen anything that can't be built in vanilla Sitecore :) Also curious if anybody did it in production.
    – Alex Shyba
    Oct 19, 2016 at 17:00
  • Thanks for that I'll take a look and see what I can do on my local. I would also be interested if anyone has implemented it in production as I have a feeling I'll have to do this soon.
    – Cmasterd
    Oct 20, 2016 at 9:43
  • There was a discussion on Slack about this a couple weeks ago. I only quickly scanned it, wasn't involved. but I recall someone saying that at the end of the day the effort wasn't worth it. But always fun to see another front end integration in use.
    – Pete Navarra
    Oct 24, 2016 at 3:13
  • @Cmasterd- have you done anything on AMP?, if yes, can you please share the details and production link if available, we are doing some research on AMP so trying to understand if we already have some resource which can be used as ref. Thanks, Dec 6, 2017 at 11:32

2 Answers 2

21

Because Sitecore supports server-side responsive rendering, it's very easy to use the Device feature of the CMS to support AMP.

AMP Layout (using Razor)

This example is adapted from Google's example markup and Razorified:

<!doctype html>
<html amp lang="en">
  <head>
    <meta charset="utf-8">
    <script async src="https://cdn.ampproject.org/v0.js"></script>
    <title>Hello, AMPs</title>
    <link rel="canonical" href="@LinkManager.GetItemUrl(RenderingContext.Current.ContextItem)" />
    <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
    <script type="application/ld+json">
      {
        "@@context": "http://schema.org",
        "@@type": "NewsArticle",
        "headline": "Open-source framework for publishing content",
        "datePublished": "2015-10-07T12:02:41Z",
        "image": [
          "logo.jpg"
        ]
      }
    </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>
    <h1>@Html.Sitecore().Field("Your Title Field Name")</h1>
        @Html.Raw(GoogleAmpConverter.Convert(RenderingContext.Current.ContextItem["ContentFieldName"]))
  </body>
</html>

Note the canonical tag that links back to the Default Device for the context Item. Also note that I'm assuming the field you want to render for body copy is also attached to the context Item. Your needs may be different. Some folks may want to have a very limited Rich Text field just for AMP output, others may want to try to re-use as much of their current content as possible, which is why we have the next section...

Rich Text Conversion

Assuming the page you're trying to AMP-ify is a fairly basic content page, we want to take the normal Rich Text field that represents the content of the page, and strip it of anything that AMP doesn't like.

With HtmlAgilityPack and a little patience, you can use a class like the one below to clean up your normal Rich Text field for AMP:

public class GoogleAmpConverter
{
    private readonly string source;

    public GoogleAmpConverter(string source)
    {
        this.source = source;
    }

    public static string Convert(string source)
    {
        var converter = new GoogleAmpConverter(source);
        return converter.Convert();
    }

    public string Convert()
    {
        var result = ReplaceIframeWithLink(source);
        result = StripInlineStyles(result);
        result = ReplaceEmbedWithLink(result);
        result = UpdateAmpImages(result);
        return result;
    }

    private string ReplaceIframeWithLink(string current)
    {
        // Uses HtmlAgilityPack (install-package HtmlAgilityPack)
        var doc = GetHtmlDocument(current);
        var elements = doc.DocumentNode.Descendants("iframe");
        foreach (var htmlNode in elements)
        {
            if (htmlNode.Attributes["src"] == null) 
            {
                continue;
            }
            var link = htmlNode.Attributes["src"].Value;
            var paragraph = doc.CreateElement("p");
            var text = link; // TODO: This might need to be expanded in the future
            var anchor = doc.CreateElement("a");
            anchor.InnerHtml = text;
            anchor.Attributes.Add("href", link);
            anchor.Attributes.Add("title", text);
            paragraph.InnerHtml = anchor.OuterHtml;

            var original = htmlNode.OuterHtml;
            var replacement = paragraph.OuterHtml;

            current = current.Replace(original, replacement);
        }

        return current;
    }

    private string StripInlineStyles(string current)
    {
        // Uses HtmlAgilityPack (install-package HtmlAgilityPack)
        var doc = GetHtmlDocument(current);
        var elements = doc.DocumentNode.Descendants();
        foreach (var htmlNode in elements)
        {
            if (htmlNode.Attributes["style"] == null)
            {
                continue;
            }

            htmlNode.Attributes.Remove(htmlNode.Attributes["style"]);
        }

        var builder = new StringBuilder();
        var writer = new StringWriter(builder);
        doc.Save(writer);
        return builder.ToString();
    }

    private string ReplaceEmbedWithLink(string current)
    {
        // Uses HtmlAgilityPack (install-package HtmlAgilityPack)
        var doc = GetHtmlDocument(current);
        var elements = doc.DocumentNode.Descendants("embed");
        foreach (var htmlNode in elements)
        {
            if (htmlNode.Attributes["src"] == null) continue;

            var link = htmlNode.Attributes["src"].Value;
            var paragraph = doc.CreateElement("p");
            var anchor = doc.CreateElement("a");
            anchor.InnerHtml = link;
            anchor.Attributes.Add("href", link);
            anchor.Attributes.Add("title", link);
            paragraph.InnerHtml = anchor.OuterHtml;
            var original = htmlNode.OuterHtml;
            var replacement = paragraph.OuterHtml;

            current = current.Replace(original, replacement);
        }

        return current;
    }

    private string UpdateAmpImages(string current)
    {
        // Uses HtmlAgilityPack (install-package HtmlAgilityPack)
        var doc = GetHtmlDocument(current);
        var imageList = doc.DocumentNode.Descendants("img");

        const string ampImage = "amp-img";
        if (!imageList.Any()) 
        {
            return current;
        }

        if (!HtmlNode.ElementsFlags.ContainsKey("amp-img"))
        {
            HtmlNode.ElementsFlags.Add("amp-img", HtmlElementFlag.Closed);
        }

        foreach (var imgTag in imageList)
        {
            var original = imgTag.OuterHtml;
            var replacement = imgTag.Clone();
            replacement.Name = ampImage;
            replacement.Attributes.Remove("caption");
            current = current.Replace(original, replacement.OuterHtml);
        }

        return current;
    }

    private HtmlDocument GetHtmlDocument(string htmlContent)
    {
        var doc = new HtmlDocument
        {
            OptionOutputAsXml = true,
            OptionDefaultStreamEncoding = Encoding.UTF8
        };
        doc.LoadHtml(htmlContent);

        return doc;
    }
}

Disclaimer: This isn't heavily tested, but addresses the main AMP vs. HTML issues you're likely to encounter.

Differentiating your AMP page output from your normal page output

This is a two-step process.

1: Configure a Sitecore Layout Device for AMP

Create a new Device and establish a querystring parameter for switching to this device mode:

enter image description here

You could also probably get fancy and use agent detection, but I don't think there's a dedicated AMP agent, so it's important to have a way to explicitly differentiate between your default output and your AMP output. Querystrings are your friend.

2: Create a meta tag in your Default Device Layout that references your AMP Device layout for the context Item.

<link rel="amphtml" href="@LinkManager.GetItemUrl(RenderingContext.Current.ContextItem)?google=amp">

Profit!

Keep in mind that although you could AMP-ify your entire website, it's only supposed to be for newsy-article kind of stuff. Don't expect to be able to include tons of anciliary content, cross-sells or calls to action. This is mostly about goodwill content for mobile users.

7

I recommend to integrate Google AMP with Sitecore by overriding the following pipelines:

  1. httpRequestBegin to check whether it is an AMP request (for example, by /amp/ URL prefix or suffix) or not and, if a resolved item has an AMP view, set an AMP flag to true;
  2. mvc.buildPageDefinition to substitute a normal Layout with the AMP one for AMP requests on the fly;
  3. mvc.renderRendering to extend the standard Vary By Param cache option to support the AMP view.

Please find the exact implementation on GitHub.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.