AMP

AMP Camp: Using templates in the client and the server

Websites

Overview

Welcome to the latest in our series of posts about AMP Camp, our demo that shows how to create an interactive site with AMP! In this series, we’ll discuss the techniques and tools we used in creating it, as well as best practices we developed. If you’re interested in creating an interactive site with AMP, we hope you’ll learn something!

In the previous post, we talked about the build process used to create our site. In this post, we’ll discuss how to use templates on both the client and your server.

AMP is commonly used to serve pages instantly through link sharing platforms, what’s referred to as Paired AMP. In a previous post, we’ve discussed the benefits of using AMP as the primary framework to build your entire website, an approach that’s called AMP-First.

In this post, we’ll discuss how to produce dynamic content in AMP-First sites. That way users always access fresh data, whether they arrive at the pages from the AMP Cache, or from the site’s origin.

To achieve this, we’ll explore how to differentiate client-side rendered parts of the page (not mediated by the cache) from server-side rendered ones (that can be safely cached).

Also, to simplify our development we’ll see how we can use templates in the client and the server by using the same templating engine. This can make our code more readable and maintainable, as long as we make sure to differentiate clearly which parts have to be rendered by which layer.

As a recap, let’s review how content can be accessed in AMP-First sites.

Dynamic content in AMP-First sites

In AMP-First sites, users can access your pages on two different origins:

  • Site’s origin: These AMP pages are hosted by the site’s owner. Users commonly land on those pages by navigating the site’s URLs.
  • AMP Cache origin: These versions of the AMP pages are stored after being discovered by search engines like Google and Bing (for example at cdn.ampproject.org or bing-amp.com). Users commonly land on those pages after clicking on links from search results or link sharing platforms.

In the first case, site owners have full control on the caching strategies to apply, for example, by using traditional HTTP Caching strategies and/or service workers.

In the case of pages served from an AMP cache, even when the site’s owner can force updates (e.g. by using update cache API), by default the cache will follow a stale-while-revalidate model. This consists of checking the origin’s caching headers (such as max-age and s-max-age) on each user visit, and requesting a new copy to be fetched in the background, if the resource is stale. For that reason, one user might see stale data until the latest version of a page has been fetched.

For many sites, relying on the default behavior of the AMP Cache is acceptable. For example, frequently accessed articles on news sites are kept fresh automatically, and having, at most, one user see stale content from time to time might not be a big issue. 

E-commerce sites usually have stricter UX requirements: showing stale prices even to a single user could prevent an entire purchase, or what is worse, negatively impact the user’s trust on the brand.

In high impact cases, you can apply a mixed strategy for dynamic data, to make sure that critical fields are always fresh while leveraging the speed benefits of the cache for non-critical parts of the page:

  • Client-side rendered data: For critical fields (like prices) client-side requests can be used. These requests are not mediated by the AMP Cache, and can be implemented by using dynamic AMP components like <amp-list>, <amp-bind> and <amp-access>.
  • Server-side rendered data: Non-critical parts of the page that change less frequently (like a product title) can be fully rendered in the server. This is commonly achieved by combining dynamic data (from databases, APIs, etc), with static HTML templates.

As a result, when the page is being served, the resulting HTML code will contain fields already populated (non-critical), along with calls to components like <amp-list> for client-side generated ones (critical fields). 

In the following snippet, the parts marked in blue are server-side rendered fields, while the red ones are fields that will by resolved later in the client:

<div class="product-details"> 
      <h1>Stretched Jeans</h1> 
     <amp-list height="24" layout="fixed-height" src="/static/samples/json/product.json"> 
           <template type="amp-mustache"> Price: ${{price}} </template> 
     </amp-list>
     <p class=”product-description”>A classic choice for every day.</p>
</div>Code language: HTML, XML (xml)

The code looks simple, but it’s the result of some complex work done in the backend. Next, we’ll explore how to achieve that by using popular web technologies.

Implementing a server-side rendering strategy

To put to practice the strategy discussed before, we’ll use a popular backend technology: Node.js.

As mentioned, for the server-side rendered parts of pages, you need some way of combining data fetched from APIs and databases with static HTML templates. In a Node.js environment, this is usually accomplished via JS templating engines.

There are many available options on the market. In this post, we’ll explore a popular one: mustache.js. Besides the fact that it’s easy to implement and widely used, one of the advantages of picking this templating engine is that it’s already used by AMP to render the responses of dynamic components, like <amp-list>, through a component called <amp-mustache>. This saves us the effort of learning another technology while keeping our code more coherent and readable.

A typical mustache template contains any number of mustache tags. By default these tags are written with curly brackets (e.g. {{price}} and {{availability}}).

Even when they are simple to write, these templates are “logic-less”, meaning you can use things like conditions in the templates, but not much more. Most of the logic will be executed and contained in data objects that are sent to these templates to populate fields.

The challenge of using the same templating engine for client and server is that we’ll be using the same tags to populate both client and server-side rendered fields.This can lead to collisions.

In the previous example code, if we were using the same default mustache tags {{ }} in the client and server, when the engine runs in the server, and finds the following code:

<div class="product-details"> 
    <h1>${{product-title}}</h1> 
     <amp-list height="24" layout="fixed-height" src="/static/samples/json/product.json"> 
           <template type="amp-mustache"> Price: ${{price}} </template> 
     </amp-list>
     <p class=”product-description”>${{product-description}}</p>
</div>Code language: HTML, XML (xml)

It will replace each dynamic field with the value of the corresponding property in the object that is used to populate the template. The resulting version of the page will be:

<div class="product-details"> 
      <h1>Stretched Jeans</h1> 
     <amp-list height="24" layout="fixed-height" src="/static/samples/json/product.json"> 
           <template type="amp-mustache"> Price: $50 </template> 
     </amp-list>
     <p class=”product-description”>A classic choice for every day.</p>
</div>Code language: HTML, XML (xml)

When the AMP page is served, the following sequence will take place: 

  1. <amp-list> will be executed at page load time.
  2. The response will be passed to <amp-mustache>.
  3. Since the price has already populated in the backend, mustache won’t find any dynamic fields to resolve.

This prevents our goal of making sure that the price is always fresh.

To avoid this, you can use custom delimiters, to declare a different set of tags in the client and the server. For example:

  • {{ }}: For fields rendered by <amp-mustache> with the result from <amp-list>.
  • <% %>: For fields populated by mustache in the server.

The resulting code will look like the following:

<div class="product-details"> 
    <h1><%product-title%></h1> 
     <amp-list height="24" layout="fixed-height" src="/static/samples/json/product.json"> 
           <template type="amp-mustache"> Price: ${{price}} </template> 
     </amp-list>
     <p class=”product-description”><%product-description%></p>
</div>Code language: HTML, XML (xml)

By combining these delimiters in a template, the server will first populate all the fields marked with <% %>, and leave the ones marked with {{ }} untouched, so that they can be used by <amp-mustache> after <amp-list>  executes.

Going the extra mile: serving different versions of the page according to user agent

In the case of AMP-First sites, one could apply an additional optimization, by serving two different versions of the page to users and crawlers:

  • For crawlers – Mixed client and server-side rendered approach: The strategy discussed before can be applied only for requests coming from the Google crawler, by verifying Googlebot using a reverse IP lookup. When the crawler fetches the AMP URLs, those are the versions that are going to be retrieved, stored and served from the AMP Caches.
  • For site’s origin users – Fully server-side rendered approach: If the request doesn’t come from a bot, the page can be fully server-side rendered, so that user’s navigating the site from the origin don’t need to incur into any additional latency from <amp-list> requests unnecessarily.

That way, since users navigating on the site’s origin won’t run into the risk of seeing potentially stale content, there’s no need to make any client-side request for critical fields. In those cases, all the content can be fully server-side rendered, to avoid incurring potential latency of client-side requests.

Breaking up the serving strategy in this way ensures the best possible performance for users accessing the site from different surfaces.

Summary

In this post, we have explored a strategy to make sure that the AMP content served is always fresh by combining client and server-side rendered content in the same page. 

To that end, we explored how to use templates in the client and the server, by using popular web technologies: Node.js and mustache.js.

For concrete examples of application of this pattern, you can take a look at the code of AMP Camp

The product details page is a good example of applying this technique. It contains a mix of fields, written with different tags: {{ }}, for client-side requests, and <% %> for server-side rendered parts of the page, as discussed throughout this guide.

If you want to take this implementation further, you can analyze the user-agent of the request, and only serve mixed client and server-side rendered pages to search engines, while serving fully server-side rendered versions of those pages to users navigating on your origin.

Written by Demián Renzulli, Web Ecosystem Consultant, Google