Editor’s Note: the following guest post was written by Matthias Rohmer, Development Director at Jung von Matt
TL;DR: Jung von Matt helped BMW drive amazing user experiences using AMP on Adobe Experience Manager.
It’s fairly uncommon that you are free to choose which technology to use to implement for a client’s project. Clients that have chosen an enterprise solution usually seek products and services that complement those choices while allowing for future growth.
When BMW was searching for a company to partner with to rebuild the brand’s website www.bmw.com in 2017, they were looking for that complementary solution. As almost all of BMW’s websites were built using Adobe Experience Manager (AEM), they were looking for a team that was able to both deal with such a high-end system as well as use its capabilities to build an easy-to-maintain, high-performance website.
This was a goal BMW had missed with their already-existing sites. Despite sharing the same backend, they were based on a variety of frontend frameworks and libraries. Fairly early in the process, we decided to bring some fresh air to this stack. Therefore we wanted to introduce AMP as the governing frontend technology. That was where our heads began spinning: what would be the best way to integrate AMP with AEM, especially with a CMS that makes so many assumptions about your frontend development?
Problems to solve when integrating AMP with AEM
After some research it occurred to us that there were three main challenges that we would need to solve in order to render valid AMP pages from AEM:
- As AMP requires all of a document’s CSS to be inlined in the
<head>
we would need to find a way other than AEM’s built-in ClientLib functionalities to render our CSS - For all of our pages to be AMP valid we would need a mechanism to only render resource hints (
<script async custom-element="amp-carousel" src="https://cdn.ampproject.org/v0/amp-carousel-0.2.js"></script>
) for AMP components actually used on a page - To be able to progressively enhance the website for returning visitors, AEM would need to be able to render AMP and non-AMP pages at the same time
Inline CSS with AMP
AEM has a fairly streamlined approach of handling a page’s styles: the ClientLib mechanism will take care of combining all the CSS needed for a page based on so-called categories. Based on those categories you can then let AEM create <link>
tags in your templates. These will point to built stylesheets containing all your styles.
With AEM’s built-in rewriter pipeline we can use those <link>
elements to combine those stylesheets into a common <style amp-custom>
tag. The following (highly compressed) code-snippet should give you an idea of how such a transformer might look:
StringBuilder styles = new StringBuilder();
boolean writeStyles = false;
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
if (localName.equalsIgnoreCase("link")) {
// If the element currently in queue is a link tag inspect it
String href = atts.getValue("href");
String rel = atts.getValue("rel");
if (rel.equalsIgnoreCase("stylesheet")) {
String css = "";
// TODO: Load the stylesheet from the JCR, store it with others loaded
// so far and append to styles
}
return;
}
if (localName.equalsIgnoreCase("style")) {
if (atts.getIndex("amp-custom")) {
writeStyles = true;
// TODO: Use this flag to emit all styles gathered in styles
// in the transformer's characters method
}
return;
}
contentHandler.startElement(uri, localName, qName, atts);
}
Code language: JavaScript (javascript)
Only add needed resource hints
Another challenge we needed to solve in order to ensure AMP validity was to only add those <script>
elements to the page that are actually needed. We did so by introducing a custom node type to our project:
<ampJS
jcr:primaryType="bmw:ampJSResource"
bmw:ampCustomElementTag="[amp-video]"/>
Code language: HTML, XML (xml)
Each of our AEM components has a child of that type holding information about what AMP component it relies on (amp-video in the example above). The advantage of using a custom node type here is that we can safely and quickly query those nodes when a page gets rendered to determine the required AMP components. In code this looks similar to the following:
final PageManager pageManager = resource.getResourceResolver().adaptTo(PageManager.class);
final String currentPage = pageManager.getContainingPage(resource).getPath() + "/jcr:content";
final String query = String.format("SELECT * FROM [bmw:ampResourceHint] AS s WHERE ISDESCENDANTNODE(s,'%s')", currentPage);
final Iterator<Resource> result = resource.getResourceResolver().findResources(query, Query.JCR_SQL2);
while (result.hasNext()) {
Resource queryResource = result.next();
final String type = queryResource.getParent().getResourceType();
ValueMap properties = queryResource.adaptTo(ValueMap.class);
String[] usedComponents = properties.get("bmw:usedAmpComponents", String[].class);
if (usedComponents != null && usedComponents.length != 0) {
// TODO: Store all used components somewhere for later rendering
}
}
Code language: JavaScript (javascript)
This piece of logic can then be easily called in an HTML template by using a data-sly-use
attribute in combination with data-sly-repeat
to print all required resource hints to the head of the page.
Serve AMP alongside a PWA
For www.bmw.com we wanted to make sure that the site lands on the user’s screen as fast as possible. To achieve this every first-time visitor will receive the AMP version of our page. At the same time, we wanted to implement features that AMP alone could not offer but a PWA (which is still based on AMP!) could.
This means that our application would need to be able to serve two versions of the same document. Luckily for us this functionality is already available with AEM by using Sling selectors.
To establish a selector all you need to do is implement two templates alongside each other. The one the Sling engine should resolve to by default is simply called html.html
. The other one is named after your selector. In our case it’s pwa.html
which makes all of our articles either accessible in their pure AMP version via brooklyn-beckham-car-photography.html or brooklyn-beckham-car-photography.pwa.html with our PWA features.
Using this method, we had found a way to independently serve our valid AMP pages alongside our PWA. But how would a user ever end up on our Progressive Web App? That was where amp-install-service-worker
had its shining debut. By using this AMP component, the www.bmw.com is installing a Service Worker as soon as the user visits one of its pages in any of the AMP caches. From that point on we were then able to rewrite all requests going to brooklyn-beckham-car-photography.html to brooklyn-beckham-car-photography.pwa.html instead in order to silently enhance the user’s experience.
For us, those three were the main challenges when we were building BMW’s new international marketing website. In the end, all of them were solvable without reinventing the wheel, by using functionalities that are already built into AEM and AMP in creative ways.
AMP and Adobe have teamed up with Bounteous to make it even easier to build AMP sites on Adobe Experience Manager. Learn more and get started here.
Written by Matthias Rohmer, Development Director at Jung von Matt