Implementing Google AMP on Contentstack-powered Websites

Google Accelerated Mobile Pages, or simply Google AMP, is an open-source HTML web development framework designed to help web pages load faster, especially on mobile devices.

By using Google AMP, you can easily create optimized content for mobile, so they open instantly on almost all devices.

In this guide, we will learn how to implement Google AMP on top of your Contentstack-powered websites to help you deliver information to your end-users instantly.

Prerequisites

Process Overview

  • We have used the Express framework for app development and Nunjucks for templating.
  • There are two navigation sections: Home and Blog with header and footer as defaults.
  • The Home page has the basic template whereas the blog page consists of multiple blogs that take content from Contentstack with dynamic routes.
  • Each blog has its own page when clicked on the See more option.
  • We have hosted our sample app powered by Google AMP and Contentstack on Heroku for reference. In this example, we have used ngrok for testing purposes. 

Steps for Execution

  1. Download the code
  2. Add and publish the entries
  3. Make configuration changes
  4. Start the app
  5. Validate your AMP-powered website
  6. Test your site's performance
  1. Download the Code

    To help you understand how AMP works, we have created a sample app and the code. You can download it from our GitHub repository.

    Once you have the code, you’ll find a zip containing the content types needed for this example. Unzip content-type.zip and import them into your stack.

    Open the code in any code editor of your choice. Let's look into the routes for header & footer of our Express.js sample app (for a single entry).

    /**
    * Module dependencies.
    */
    const express = require('express');
    
    const router = express.Router();
    
    router.get('*', require('./partials'));
    
    module.exports = router;

    If you want to fetch the content of the header and footer data from Contentstack, you can use the async library to parallelly fetch data. The following code shows an example of how you can use the async library:

    /**
    * Module dependencies.
    */
    
    const async = require('async');
    const Contentstack = require('contentstack');
    const configVars = require('../config');
    
    // Initialize stack
    
    const Stack = Contentstack.Stack(configVars.apiKey, configVars.accessToken, configVars.env);
    
    // Below method will fetch header & footer data
    
    module.exports = (req, res, next) => {
     async.parallel(
       [
         // Here in this function we get the data for header
         (callback) => {
           const Query = Stack.ContentType(configVars.contentTypeUid.headerContentTypeUid).Query();
           Query.toJSON()
             .find()
             .then((result) => {
               callback(null, result[0][0]);
             })
             .catch((err) => {
               console.log(err);
             });
         },
         // Here in this function we get the data for footer
         (callback) => {
           const Query = Stack.ContentType(configVars.contentTypeUid.footerContentTypeUid).Query();
           Query.toJSON()
             .find()
             .then((result) => {
               callback(null, result[0][0]);
             })
             .catch((err) => {
               console.log(err);
             });
         },
       ],
       (error, success) => {
         // Here the variable header & footer is used into the template directly for your header and footer
         if (error) return next(error);
         res.locals.header = success[0];
         res.locals.footer = success[1];
         next();
       },
     );
    };

    You can create templates for your app. For our example, we have fed the data of the variables (“header” & “footer”) into the header template, you can create a code similar to the one shown below:

    <!-- Header/nav bar -->
    
    <!-- Here the variable header is fed into template which consist the data fetched from contentstack -->
    
    <amp-sidebar id="sidebar" class="cid-qMPDXEs8Ea" layout="nodisplay" side="right">
     <div class="builder-sidebar" id="builder-sidebar">
       <button on="tap:sidebar.close" class="close-sidebar">
         <span></span>
         <span></span>
       </button>
       {% for menuItems in header.navigation_group.navigation_option %}
       <ul class="navbar-nav nav-dropdown" data-app-modern-menu="true">
         <li class="nav-item">
           <a class="nav-link link text-white display-7" href="{{menuItems.link}}">{{menuItems.title}}</a>
         </li>
       </ul>
       {% endfor %}
     </div>
    </amp-sidebar>
    
    <section class="menu horizontal-menu cid-qMPDXEs8Ea" id="menu1-7">
     <nav class="navbar navbar-dropdown navbar-fixed-top navbar-expand-lg" style="box-shadow: 0px 10px 5px #a09f9f80;
           -webkit-box-shadow: 0px 5px 5px #27272780;
           -moz-box-shadow: 0px 5px 5px #27272780;">
       <div class="navbar-brand">
         <span> <a href="/">
             <amp-img src="{{header.brand_name.logo.url}}" alt="AMP Marketing website template" width="40" height="40">
    
             </amp-img>
           </a></span><span style="padding-left: 7px;" class="navbar-caption-wrap">
           <a style="font-weight: bold;" class="navbar-caption text-white display-5"
             href="/">{{header.brand_name.title}}</a></span>
       </div>
       <div class="collapse navbar-collapse" id="navbarSupportedContent">
         {% for menuItems in header.navigation_group.navigation_option %}
    
         <ul class="navbar-nav nav-dropdown" data-app-modern-menu="true">
           <li class="nav-item">
             <a class="nav-link link text-white display-7" style="font-weight: bold;"
               href="{{menuItems.link}}">{{menuItems.title}}</a>
           </li>
         </ul>
         {% endfor %}
       </div>
     </nav>
    
     <!-- AMP plug -->
    
     <button on="tap:sidebar.toggle" class="ampstart-btn hamburger sticky-but">
       <span></span>
       <span></span>
       <span></span>
       <span></span>
     </button>
    </section>
    

    Similarly, you can create a template for the footer:

    <!-- Footer html -->
    
    <section class="footer1 cid-qMPDT0TXHG" id="footer4-6">
     <div class="container">
       <div class="mbr-col-sm-12 align-center">
         <p class="mbr-text mbr-fonts-style mbr-white display-7">
           {{footer.footer_section}}
         </p>
       </div>
     </div>
    </section>
    

    Let's now discuss how the routes are defined through the code in our example app. The following code depicts the Home page route:

    /**
    * Module dependencies.
    */
    const express = require('express');
    
    const router = express.Router();
    const Contentstack = require('contentstack');
    const configVars = require('../config');
    
    // Initialize stack
    
    const Stack = Contentstack.Stack(configVars.apiKey, configVars.accessToken, configVars.env);
    
    // Below method will fetch & render home page
    
    router.get('/', (req, res) => {
     const Query = Stack.ContentType(configVars.contentTypeUid.homeContentTypeUid).Query();
     Query
       .toJSON()
       .find()
       .then((result) => {
         res.render('pages/home.html', { homeData: result[0][0] });
       }).catch((err) => {
         console.log(err);
       });
    });
    
    module.exports = router;
    

    The homeData variable in the above code is used to provide values to the home-page template as shown below:

    <!-- Home html -->
    {% extends "pages/index.html" %} {% block content %}
    
    <!-- Below is the template for your home-page where the variable "home" is fed to the tag <h1>, <p>,<amp-youtube> etc -->
    
    <section class="header11 cid-qMPCctt0uS mbr-fullscreen" id="header11-0"
       style="background-image: url({{homeData.hero_banner.banner_image.url}});">
       <div class="mbr-overlay"></div>
       <div class="container">
           <h1 class="mbr-section-title mbr-fonts-style mbr-white align-center mbr-bold display-1">
               <div style="font-size: 70px;">{{homeData.hero_banner.title}}</div>
           </h1>
       </div>
    </section>
    
    <section class="features2 mbr-section cid-qMPCzM7s2W" id="features2-1">
       <div class="container">
           <div class="mbr-row mbr-justify-content-center">
               <div>
                   <h1 style="font-size: 40px;">{{homeData.introduction_section.title}}</h1>
                   <p>{{homeData.introduction_section.description}}
                   </p>
                   <amp-youtube  data-videoid="{{homeData.introduction_section.videoid}}" width="450" height="220" layout="responsive">
                   </amp-youtube>
               </div>
           </div>
       </div>
    </section>
    {% endblock %}
    

    In the above code, we have used the tag to achieve improved performance as compared to our normal HTML. If your website has multiple pages (entries), you can write code similar to the one shown below. You can create multiple routes. We have used "blogs" as our example.

    In the downloaded code, there's a content-type.zip file. It contains the content type for the blog page (multiple entries). We have added a Modular Block and Group fields in our content type for blog entries (multiple Content Type). So with the help of Modular Block, we can use a single content-type for both “blog-list” page & individual blog pages.

    Below is the sample router method created for the blog list page. We have used the “blogData” variable to feed the template for the blog list page to render all blogs.

    /**
    * Module dependencies.
    */
    
    const express = require('express');
    
    const router = express.Router();
    const Contentstack = require('contentstack');
    const configVars = require('../config');
    
    // Initialize stack
    
    const Stack = Contentstack.Stack(configVars.apiKey, configVars.accessToken, configVars.env);
    
    // Below middleware will render the blog list page
    
    router.get('/', (req, res) => {
     Stack.ContentType(configVars.contentTypeUid.blogContentTypeUid).Query()
    
       .toJSON()
       .find()
       .then((result) => {
         const blogList = result[0].find((blog) => blog.url === '/blogs');
         res.render('pages/blog-list.html', { blogData: blogList });
       })
       .catch((err) => {
         console.log(err);
       });
    });
    
    module.exports = router;
    

    The following code represents the router method which fetches individual blogs from the modular block i.e blog_page and render that specific blog into a new template using the variable “blogDetail” as shown below:

    // Below code is the router method for an individual blog page`
    
    /**
    * Module dependencies.
    */
    
    const express = require('express');
    
    const router = express.Router();
    const Contentstack = require('contentstack');
    const configVars = require('../config');
    
    // Initialize stack
    
    const Stack = Contentstack.Stack(configVars.apiKey, configVars.accessToken, configVars.env);
    
    // Below router method will fetch & render a specific blog from modular block i.e blog_page
    
    router.get('/', (req, res) => {
     Stack.ContentType(configVars.contentTypeUid.blogContentTypeUid).Query()
    
       .toJSON()
       .find()
       .then((result) => {
         const blogContent = result[0].find((blog) => `/blogs${blog.url}` === `${req.originalUrl}`);
         if (blogContent) {
           res.render('pages/blog-page.html', { blogDetail: blogContent });
         }
       })
       .catch((err) => {
         console.log(err);
       });
    });
    
    module.exports = router;
    

    Now let’s see the blog-list & blog-page templates where the variables (“blogData” & “blogDetail”) are used to feed the template, similar to what we did for header and footer.

    Blog-list template:

    <!-- Blogs list page html
    
    {% extends "pages/index.html" %} {% block content %}
    
    <section class="header11 cid-qMPCctt0uS mbr-fullscreen" id="header11-0"
     style="background-image: url({{blogData.banner_section[0].banner_content.banner_gourp.banner_image.url}});">
     <div class="mbr-overlay"></div>
     <div class="container">
       <h1 class="mbr-section-title mbr-fonts-style mbr-white align-center mbr-bold display-1">
         <div style="font-size: 70px;">{{blogData.banner_section[0].banner_content.banner_gourp.heading}}</div>
       </h1>
     </div>
    </section>
    {% for blogList in blogData.modular_blocks %}
    
    <section class="features22 cid-qMPD3jJ5kk" id="features22-3">
     <div class="container">
       <div class="mbr-row mbr-justify-content-center">
         <div class="mbr-col-lg-6 mbr-col-md-12 mbr-col-sm-12 resize vcenter">
           <div class="img_wrapper vcenter">
             <amp-img src="{{blogList.blog_list.image.url}}" alt="AMP Marketing website template" width="540" height="390"
               layout="responsive">
             </amp-img>
           </div>
         </div>
         <div class="mbr-col-lg-6 mbr-col-md-12 resizeblock vcenter">
           <div style="margin-top: 30px;">
             <h2 class="tab-title mbr-fonts-style align-center display-5" style=" font-weight: 600; font-size: 25px; color: rgb(33, 37, 41); text-align: justify; margin-bottom: -3px;">
               {{blogList.blog_list.heading}}
             </h2>
             <p class="mbr-fonts-style align-center display-7" style="margin-bottom: -13px; text-align: justify; font-size: 20px; color: rgb(141, 138, 138);">{{blogList.blog_list.date}}</p>
             <p class="mbr-fonts-style align-center display-7" style="margin-bottom: -13px; text-align: justify;">{{blogList.blog_list.description}}
             </p>
             <p class="mbr-fonts-style align-center display-7" style="text-align: justify;"><a
                 href="/blogs{{blogList.blog_list.cta.href}}">{{blogList.blog_list.cta.title}}</a></p>
           </div>
         </div>
       </div>
     </div>
    
    </section>
    {% endfor %}
    {% endblock %}
    

    In the above code, the “blogData” & "blogdetail" variables are used in a "for-loop" to iterate over the modular block entries and feed into your tags. When you click on the See more… option, the page will get directed to the in-detail blog-page for more information on the blog.  

    Below is the template for blog-page:

    <!-- Blog content page html -->
    
    {% extends "pages/index.html" %} {% block content %}
    
    <section class="header11 cid-qMPCctt0uS mbr-fullscreen" id="header11-0" style="background-image: url({{blogDetail.modular_blocks[0].blog_page.image.url}});">
     <div class="mbr-overlay"></div>
     <div class="container">
         <h1 style="font-family:Verdana, Geneva, Tahoma, sans-serif ; color: white;">
             <div style="font-size: 60px; ">{{blogDetail.modular_blocks[0].blog_page.heading}}</div>
         </h1>
     </div>
    </section>
    <section class="features2 mbr-section cid-qMPCzM7s2W" id="features2-1">
     <div class="container">
         <div class="mbr-row mbr-justify-content-center">
             <div>
               <p class="mbr-fonts-style align-center display-7" style="margin-bottom: -13px; text-align: justify; font-size: 20px; color: rgb(141, 138, 138);"><span>Date : </span>{{blogDetail.modular_blocks[0].blog_page.date}}</p>
                 <p>{{blogDetail.modular_blocks[0].blog_page.description}}
                 </p>
             </div>
             <div class="returnTag">
               <a style="text-decoration: none; padding: 0px;" href="{{blogDetail.modular_blocks[0].blog_page.cta.href}}">
                     {{blogDetail.modular_blocks[0].blog_page.cta.title}}</a>
             </div>
         </div>
     </div>
    </section>
    {% endblock %}

    As mentioned above, the “blogDetail” variable is used in to feed the template tag for your individual Blog-page. This happens by parsing the URL and fetching only the required content/object (for that specific blog itself).

  2. Add and Publish the Entries

    Before adding and publishing your entries, you will need to create an environment. For this exercise, we have created 4 content types as you must have noticed after importing them to your stack. 

    Add entries in them and publish them on your environment. Now that you are done publishing the entities, let's make changes in the configuration file of your project.

  3. Make Configuration Changes

    Open the downloaded project in any code editor of your choice. Then, open the config.js file and add your stack details, port number, and content type UIDs as shown below:

    image5.png


    Now that you have configured the project, let's move ahead and start the app.

  4. Start the App

    To start the app, you need to install the required node dependencies:

    1. Open your command prompt and move inside the project root directory. Then, issue the following command:

      npm install
    2. This will install all the necessary dependencies required for this project (Express.js app). Staying inside your project root directory, start the app by running the following command:

      npm start
    3. Then, open your browser and hit the localhost at 4000 (or whatever port you defined in the config.js file.

    You should see the app running on localhost:4000 as shown below (desktop version):

    image8.png


    The following image shows the mobile version of the app:

    image10.png


    With this, you now have the AMP-powered app running on top of Contentstack. Let's now validate whether your website is AMP powered.

  5. Validate Your AMP-powered Website

    To validate whether your website is AMP-powered or not, you can use any of the following methods: 

    Using Chrome DevTools Console

    1. Open your app in the browser.
    2. Add "#development=1" to the URL, for example, http://localhost:4000/#development=1.
    3. Open the Chrome DevTools console and check for validation errors. If there are no errors, it will show AMP validation successful in the Chrome Devtools console.

    You can verify this as depicted in the following image (on the right-hand side of the console):

    image6.png


    Using AMP Validator Chrome Extension

    If you are using the AMP Validator Chrome extension, you will see a green icon on the top right corner of your Chrome browser extension bar as shown below:

    image7.png


    If it's green, it indicates that the page was compiled with all AMP tags used in the project and if there is any error, it will turn red. The AMP Validator basically checks whether the tag/component used for building the website complies with AMP regulation tags, structure & styling guide. 

    Additional resources: For more information on AMP Page validation, you can check the Validate AMP pages, AMP validation errors, Create your AMP HTML page, The AMP Component Catalog, AMP Websites Guides & Tutorials.

    Now let’s compare the performances of a non-AMP website and an AMP-powered website using Contentstack.

  6. Test Your Site's Performance

    To determine the performance of your AMP-powered site, you can use the following tools:

    To test your site’s performance using Google Test my site & Page Insights, you can host your site globally through ngrok.

    1. Install ngrok on your system globally by using the following command:

      npm i ngrok
    2. Once you have installed ngrok, open another terminal and hit the command: ngrok http 4000 (make sure to provide your respective port value after http).
    3. You will get an ngrok URL, similar to 'https://0c722a14610f.ngrok.io/

Was this article helpful?

Thanks for your feedbackSmile-icon

On This Page

^