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?
Sitecore Stack Exchange is a question and answer site for developers and end users of the Sitecore CMS and multichannel marketing software. It only takes a minute to sign up.
Sign up to join this communityThis 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?
Because Sitecore supports server-side responsive rendering, it's very easy to use the Device feature of the CMS to support AMP.
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...
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.
This is a two-step process.
Create a new Device and establish a querystring parameter for switching to this device mode:
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.
<link rel="amphtml" href="@LinkManager.GetItemUrl(RenderingContext.Current.ContextItem)?google=amp">
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.
I recommend to integrate Google AMP with Sitecore by overriding the following pipelines:
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;mvc.buildPageDefinition
to substitute a normal Layout with the AMP one for AMP requests on the fly;mvc.renderRendering
to extend the standard Vary By Param
cache option to support the AMP view.Please find the exact implementation on GitHub.