Umbraco AMP Pages

By Stephen Garside on 5/6/2017

In this article I am going to explain how to implement Accelerated Mobile Pages (AMP) using the Umbraco CMS. AMP pages are great for Search Engine Optimisation and are fast becoming a must if you are serious about improving your website's SEO ranking in Google. 

Unfortunately at the time of writing there does not seem to be a plugin for AMP Umbraco pages, so you are going to have to do some customisation, but I have attempted to keep this as simple as possible. 

In summary, to implement AMP pages in Umbraco here is what you need to do:-

  • Create a new Master Page Template for your AMP Pages.
  • Create custom AMP Razor template views.
  • Customise the Umbraco Rich Text Editor and Media Selector to make images AMP friendly.
  • Create a new HTML Helper to output an AMP friendly Umbraco Grid Control.  
  • Add a link reference from your standard to AMP page and vice-versa.

In this article I don't cover what AMP is, or go into any detail on implementing AMP, for that you might as well read the excellent documentation on the official AMP website

New Master Template for AMP Pages

Our first step is to create a new Master page to use on all our AMP pages. The easiest way to do this is copy your original Master template and rename it to Master_Amp:-

Then, within your new Master_Amp.cshtml file you can copy and paste the following code:-

@using System.Web.Optimization
@inherits Umbraco.Web.Mvc.UmbracoTemplatePage
 
@{
    Layout = null;
}
 
<!doctype html>
<html amp lang="en">
<head>
<meta charset="utf-8">
<title>@Umbraco.Field("seoTitle")</title>
<link rel="canonical" href="@Umbraco.NiceUrlWithDomain(CurrentPage.id)" >
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,700" rel="stylesheet">
@Html.Partial("~/views/partials/_ampcss.cshtml")
<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>
<script async src="https://cdn.ampproject.org/v0.js"></script>
@RenderSection("headerscripts"false)
</head>
<body>
    <header>
        @Html.Partial("~/views/partials/_headeramp.cshtml")
    </header> 
    <section>
        @RenderBody()
    </section>
    <footer>
        @Html.Partial("~/views/partials/_footeramp.cshtml")
    </footer>
</body>
</html>

There are a few things worth pointing out in the code above.

First of all some of the code is specific to my implementation i.e. @Umbraco.Field("seoTitle"), and the links to fonts at Google.

Next, I have added a canonical link to the non-amp page so Google can understand where to pick up the standard page.

AMP css should be included inline at the top of the document so I chose to split this out into another partial view. This just helps to keep things clean and means I have a separate file dedicated to style:-

@Html.Partial("~/views/partials/_ampcss.cshtml")

I have also opted to create separate partial views for my header and footer, but you don't necessarily have to follow what I have done.

For completeness, here is my code for the main inline stylesheet (_ampcss.cshtml):-

<style amp-custom>
    /* General ------------------------------------- */
    body { background-color#ffffont-family'Roboto'GenevaVerdanasans-seriffont-size16pxfont-size100%color#383838; }
    a { text-decorationunderline; }
    pp ali { font-size1.375remline-height2remcolor#777; }
    p.p { padding0margin0 0 0.9375rem 0; }
        p.last { margin-bottom1.25rem; }
    .h1.h2.h3h1h2h3h4.h4h5.h5h6.h6 { padding0margin0margin-bottom1.25remfont-weightbold; }
    .h1.h2.h3h1h2h3 { margin-bottom0.625rem; }
    h1.h1 { font-size2rem; }
    h2.h2h3.h3h4.h4 { font-size1.625rem; }
    h5.h5h6.h6 { font-size1.375rem; }
 
    /* Layout */
    .container { padding0.625rem; }
 
    /* Menus ----------*/
    .sub-menu { margin-bottom30pxbackground-colorwhitesmoketext-aligncenter; }
    .sub-menu-link.sub-menu-link:hover { text-decorationnone; }
    .sub-menu-link { displayinline-blockpadding0.9375rem 1.25remcolor#000font-size1.25rem; }
        .sub-menu-link.active { background-color#e8e8e8; }
 
    /* Footer n Gutter ---------------------------------- */
    .footer { background-imageurl('/images/website/footer.jpg')background-sizecoverbackground-repeatno-repeatpadding-top2rempadding-bottom2rem; }
        .footer .title,
        .footer .address,
        .footer .link { color#ffffont-weightnormal; }
        .footer .address { margin-bottom:2rem; }
    .gutter { background-color:#000padding-top:0.3125rempadding-bottom:0.3125rem; } 
    .gutter .list { list-style:nonepadding:0; }
    .gutter .option { display:inline-block; }
        .gutter .option:first-child { margin-right:0.625rem; }
    .gutter .option.gutter .link { color:#ffffont-size:1rem; }
</style>

One last thing to point out on your new AMP Master page is the boiler plate code shown below. Visual Studio likes to re-format it when you paste it into your view, but this re-formatting will cause your AMP page not to validate correctly. Once you have pasted it into the page, press CTRL+Z to remove the formatting - it should be one long string with no spacing.

<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>

Custom AMP Razor Template Views

You can potentially get away with using the same Razor View template for your AMP page and standard page, but I decided to go with a separate template:-

To implement separate AMP templates I used a simple technique illustrated in an article on how to implement AMP pages in standard MVC on the excellent Danylyko blog. 

The first step is to paste the following code snippet into the WebsiteStartupHandler class, which is executed when your Umbraco website fires up.  The code adds a new GoogleAmpDisplayMode which allows you to create razor views with a .amp.cshtml extension.  If the incoming http request is for an AMP page then MVC selects the view ending in .amp.cshtml rather than the standard .cshtml view:- 

public void OnApplicationStarting(
    UmbracoApplicationBase umbracoApplication,
    ApplicationContext applicationContext)
{
    DisplayModeProvider.Instance.Modes.Clear();
    DisplayModeProvider.Instance.Modes.Add(new GoogleAmpDisplayMode());
    DisplayModeProvider.Instance.Modes.Add(new DefaultDisplayMode());
}

This next code snippet creates the aforementioned GoogleAmpDisplayMode class responsible for identifying which of your Razor views to use.  You may need to tweak the variable in the query string you are using to identify the request as requiring the AMP page, personally I use ?amp=1 to identify mine:-

using System.Web.WebPages;
 
namespace Website.DisplayModes
{
    public class GoogleAmpDisplayMode : DefaultDisplayMode
    {
        public GoogleAmpDisplayMode()
            : base("amp"// for filename.amp.cshtml files.
        {
            ContextCondition = context => context.Request.RawUrl.Contains("?amp");
        }
    }
}

Customising the Umbraco Rich Text Editor and Media Selector

The next step to implementing AMP pages in Umbraco is to make the Rich Text Editor and Media Selector controls AMP friendly.  This is achieved by customising the controls to add the image width and height, which is a requirement for AMP validation. At the same time we also need to remove the 'rel' tag from all images as these also cause AMP validation to fail. 

 

Media Selector

The first file to amend is Media.cshtml which can be found in the following folder:-

The code for the entire file is shown below, with the key customisations between the //Amp Customisation comments.  All we are doing here is adding width and height attributes and a layout='responsive' tag, but only to images that have not been manually resized in the rich text editor by the user when they are added.

@inherits Umbraco.Web.Mvc.UmbracoViewPage<dynamic/* Reqd to get media */
@using Umbraco.Web.Templates

@if (Model.value != null)
{   
    var url = Model.value.image;

    if(Model.editor.config != null && Model.editor.config.size != null){
        url += "?width=" + Model.editor.config.size.width;
        url += "&height=" + Model.editor.config.size.height;

        if(Model.value.focalPoint != null){
            url += "&center=" + Model.value.focalPoint.top +"," + Model.value.focalPoint.left;
            url += "&mode=crop";
        }
    }
 
    // Amp Customisation Start
    var isAmp = Request.QueryString["amp"] != null;
 
    if (isAmp)
    {        
        // Out of the box make un-sized umbraco image responsive
        var mediaItem = Umbraco.Media((string)Model.value.id);
        string imgWidth = mediaItem.GetPropertyValue("umbracoWidth");
        string imgHeight = mediaItem.GetPropertyValue("umbracoHeight");
        bool isResponsive = true;
 
        // Any size overrides?
        if (Model.editor.config != null && Model.editor.config.size != null)
        {
            imgWidth = Model.editor.config.size.width;
            imgHeight = Model.editor.config.size.height;
            isResponsive = false;
        }      
        
        <amp-img src="@url" alt="@Model.value.altText" width="@imgWidth" height="@imgHeight" @Html.Raw(isResponsive ? "layout=\"responsive\"" : string.Empty)></amp-img>
    }
    else
    {
        // Standard
        <img src="@url" alt="@Model.value.altText" />
    }
    // Amp Customisation End
    
 
    if (Model.value.caption != null)
    {
        <p class="caption">@Model.value.caption</p>
    }
}

Rich Text Editor - Media Selector

The Umbraco Rich Text Editor has its own media selector which also needs a minor customisation to make images AMP friendly.  The file that needs amending is called umbraco.services.js and can be found in the Umbraco > JS folder:-

 

This file is a biggy so I have only included the few lines of code that you need to tweak. The function you need to find is called insertMediaInEditor and amend the start of the function as below. This customisation adds the image width and height tags and removes the rel and data-id tags:-

insertMediaInEditor: function(editor, img) {
            if(img) {
 
               var data = {
                   alt: img.altText || "",
                   src: (img.url) ? img.url : "nothing.jpg",
                   // Amp Customisation
                   //rel: img.id,
                   //'data-id': img.id,
                   id: '__mcenew'
               };
 
                // Amp Customisation - Insert Width / Height Attributes
               if(img.properties && img.properties.length > 0)
               {
                   for(var i = 0; i < img.properties.length; i++)
                   {
                       var property = img.properties[i];
 
                       if(property.alias && property.alias == 'umbracoWidth')
                       {
                           data.width = property.value;
                       }
                       else if(property.alias && property.alias == 'umbracoHeight')
                       {
                           data.height = property.value;
                       }
                   }
               }
 
               editor.insertContent(editor.dom.createHTML('img', data));

HTML Helper To Make Umbraco Grid Control AMP Friendly

The Umbraco Grid Control is next in our sights to get the AMP treatment.  AMP images and Iframes are output with <amp-img and <amp-iframe tags, so the standard <img and<iframe tags need to be replaced with their AMP equivalants.  I have chosen to keep this simple and create a custom MVC Razor helper to do this:-

public static MvcHtmlString GetGridHtmlAmp(this HtmlHelper htmlHelper, MvcHtmlString originalGridHtml)
{
    string originalGridHtmlStr = originalGridHtml.ToString();
    originalGridHtmlStr = originalGridHtmlStr.Replace("<img ""<amp-img layout='responsive' ");
    originalGridHtmlStr = originalGridHtmlStr.Replace("<iframe ""<amp-iframe layout='responsive' ");
 
    return MvcHtmlString.Create(originalGridHtmlStr);
}

In your AMP views you can then just pass in your original grid control output and get an AMP friendly version like so:-

@Html.GetGridHtmlAmp((MvcHtmlString)CurrentPage.GetGridHtml("article"))

AMP Views

The next piece of the Umbraco AMP jigsaw is the creation of AMP specific Razor views. Here is an example of my BlogArticle.amp.cshtml Razor view:-

@inherits Umbraco.Web.Mvc.UmbracoTemplatePage
@using System.Web.Optimization
 
@{
    Layout = "Master_Amp.cshtml";
}
 
@section headerscripts{
    <script async custom-element="amp-iframe" src="https://cdn.ampproject.org/v0/amp-iframe-0.1.js"></script>
}
 
@Html.Partial("~/views/partials/blogNav.cshtml")
<div class="container">
    <div class="article-header">
        <h1>@Umbraco.Field("pageTitle")</h1>
        <p class="by">@string.Format("By {0} on {1}", CurrentPage.CreatorName, CurrentPage.CreateDate.ToShortDateString())</p>
    </div>
    <div>
        @Html.GetGridHtmlAmp((MvcHtmlString)CurrentPage.GetGridHtml("article"))
    </div>
</div>

You will notice I reference my AMP specific Master page created earlier, I also add a link to an AMP script file that takes care of any iframes I might want to include in my blog articles.

Making Your Umbraco AMP Page Discoverable

The final step in our AMP journey is to let Google know your page has an AMP version.  This is as easy as adding and extra META tag to your original Razor view in a section that will appear in the <head> tag. This tag holds a link to your AMP version of the page:-

@section meta{
    <link rel="amphtml" href="@string.Format("{0}?amp=1", Umbraco.NiceUrlWithDomain(CurrentPage.id))" >
}

A Final Word on AMP in Umbraco

There seems quite a lot to implementing AMP in Umbraco and I am sure I have not covered all eventualities, but the code outlined in this article will get you validated AMP pages - to prove it you can you can see this page in valid AMP format.

I would recommend downloading a Chrome plugin for AMP validation which you can get here

Unfortunately this approach does not make your existing Umbraco pages that utilise the Grid Control AMP friendly.  You will need to work through your articles (that use the Grid Control) and re-add any images, this will cause your customised code to add widths and heights to image tags.  Once you have done this you should find your existing articles will validate correctly.