Academic
Blog
Analytics

Search posts functionality in AMP static website make with Jekyll

⏳ 6 mins read time ❀️ Views


Everyone wants that their web page appears naturally at the first position on Google search. Google’s sorting algorithm takes into account the structure of the page, the metadata, the contents of the page, the mobile compatibility and also how fast the page loads. Google worked on a web standard called AMP, which imposes some restrictions to the web page, but guaranties fast page loading.

These restriction are:

  • No external js: Only AMP components are allowed
  • No external css stylesheet: Only in-line minified css at the header which can’t exceed 50kb.
  • External fonts calls are allowed but only from some CDN.

Not having external javascript calls makes really difficult to include some dynamic functionalities like fetching external content or calling API services. It’s the case for a search bar which needs to fetch some content and render the results. This gets more complicated for static web content that is hosted in servers like Netlify, Gitlab or Github Pages. These options don’t provide any backend and thus no dynamic content.

Even if it seems complicated, it’s not impossible. One can adapt a website to AMP format and include dynamic functionalities. For example, this post shows how to implement a javascript call in AMP pages and implement a search bar which allows searching for content in a static website hosted on Github pages.


AMP search bar integration in static website made with Jekyll

Static API

There are various alternatives to integrate a search function in a static website:

  • Client side options like Lunr.js or SimpleJekyllSearch provide some javascript functions to search indexed content, but these are not compatible with AMP because of the external javascript libraries restriction.
  • Services like Algolia, Swifttype or Google’s Custom Search Engine, which use external API services, but which are not compatible with AMP due to CORS endpoint restrictions.

I personally prefer the approach from SimpleJekyllSearch which exposes an endpoint with the posts information and which can be then used in AMP components. The front-matter informs Jekyll that is a special page so it takes into account the defined parameters in order to render it. The front-matter defines the following properties:

  • limit, which is used to limit the number of posts that will be iterated in the loop.
  • permalink, to overwrite the default URL of the file, so the endpoint can be found at the URL /api/github-pages.json.

Jekyll allows looping over the posts using the site.posts array and get data from each post. Here’s how my endpoint file looks like:

---
limit: 100
permalink: /api/github-pages.json
---
[{% for post in site.posts limit: page.limit %}{"title": "{{ post.title }}", "description": "{{ post.description }}", "image": "{{ post.image.path }}", "thumb": "{{ post.thumb.path }}", "date": "{{ post.date | date: "%B %d, %Y" }}",{% if post.source %}"source": "{{ post.source }}",{% endif %}{% if post.link %}"link": "{{ post.link }}",{% endif %}{% if post.categories %}"categories": [{% for category in post.categories %}"{{ category }}"{% if forloop.last %}{% else %},{% endif %}{% endfor %}],{% endif %}{% if post.categories == nil %} "categories"  : [],  {% endif %} "url": "{{ post.url }}", {% if post.tags %}"tags": [{% for tag in post.tags %}"{{ tag }}"{% if forloop.last %}{% else %},{% endif %}{% endfor %}]{% endif %}{% if post.tags == nil %}"tags": []{% endif %}}{% unless forloop.last %},{% endunless %}{% endfor %}]

The content of the file uses the liquid syntax to iterate between the posts. The result is an array that contains the following information for each post:

  • title
  • description
  • image
  • categories
  • description

There are AMP components that can use the static API:

  • The amp-state component allows to fetch a JSON from a CORS compatible endpoint. The fetch is done once the page loads and then it’s binned to the component.
  • The binned element can be filtered using the amp-bind component, which listen to DOM events like clicks in buttons on writing in input fields.

For example, if we define an HTML input element, it’s possible to add a listener that takes the input value and filters the results using some javascript functions (not all ES6 are allowed). The javascript indexOf function returns an int larger than -1 if the input element is found, so one can use it as a simple search like this:

allArticles.filter(a => a.title.indexOf(event.value) != -1)

For more complex research, in multiple fields, the components looks like the following:

<amp-state id="allArticles"
         src="/api/github-pages.json"></amp-state>
<input class="input" type="text" on="input-throttled:AMP.setState({filteredArticles: allArticles.filter(a => ((a.title.toLowerCase().indexOf(event.value.toLowerCase()) != -1) || (a.description.toLowerCase().indexOf(event.value.toLowerCase()) != -1) || (a.tags.join(' ').toLowerCase().indexOf(event.value.toLowerCase()) != -1) || (a.categories.join(' ').toLowerCase().indexOf(event.value.toLowerCase()) != -1) ) ), inputValue: event.value }), top-blog-page.scrollTo(duration=200)" placeholder="πŸ” Search" [value]="inputValue">

Render search results with AMP

AMP provides some components to process the JSON object that is binned with amp-bind:

  • The amp-list component dynamically downloads data and creates a list of items using a template.
  • The amp-mustache component renders an element using mustache syntax.

AMP lists

The amp-list component loads the filteredArticles object, the height is controlled dynamically with the number of elements in the object.

By default amp-list expects an object with the format {"items": [obj1, obj2]}, it’s possible to configure the location of the items list using the items parameter.

AMP template

Mustache syntax allows to filter properties of the component using the conditional notation. Here are the definition of the special characters:

  • {{variable}}: A variable tag. It outputs the HTML-escaped value of a variable.
  • {{#section}} {{/section}}: A section tag. It can test the existence of a variable and iterate over it if it’s an array.
  • {{^section}} {{/section}}: An inverted tag. It can test the non-existence of a variable.
  • {{{unescaped}}}: Unescaped HTML. It’s restricted in the markup it may output (see β€œRestrictions” below).

Please note that static websites generators like Jekyll need to escape mustache templates using { raw } and { endraw } elements. Otherwise the results would not be rendered. Here is how looks like the final combination of the two elements:

<amp-list height="500"
         [height]="(400) * filteredArticles.length"
         layout="fixed-height"
         items="."
         src="/api/github-pages.json"
         [src]="filteredArticles"
         binding="no">
<div placeholder>Loading ...</div>
<div fallback>Failed to load data.</div>

<template type="amp-mustache">
{% raw %}
<div class="box">
 <div class="columns is-multiline">
   <div class="column is-3-desktop is-12-mobile">
     <a class="image" href="{{#link}}{{ link }}{{/link}}{{^link}}{{ url }}{{/link}}" data-proofer-ignore>
       <amp-img 
         width="300" height="200" 
         src="{{ thumb }}" 
         srcset="{{ thumb }} 400w, {{ image }} 900w"
         alt="{{ title }}" layout="responsive"></amp-img>
     </a>
   </div>
   <div class="column is-9-desktop is-12-mobile">
     <div class="content">
       <a class="title is-6" href="{{#link}}{{ link }}{{/link}}{{^link}}{{ url }}{{/link}}" data-proofer-ignore>{{ title }}</a>
       <p class="is-size-7">
         {{ description }}
       </p>
       <div class="tags has-addons">
         <span class="tag"><i class="fas fa-calendar-alt"></i>&nbsp;{{ date }}</span>
         {{#tags}}<a class="tag" href="/blog/tags#{{.}}" data-proofer-ignore> {{.}}</a>{{/tags}}
         {{#categories}}<a class="tag is-link" href="/blog/categories#{{.}}" data-proofer-ignore> {{.}}</a>{{/categories}}
         {{#source}}
         <span class="tag is-danger"><i class="fas fa-external-link-alt"></i>&nbsp; {{ source }}</span>
         {{/source}}
       </div>
     </div>
   </div>
 </div>
</div>
{{% endraw %}}
</template>
</amp-list>

The final version of the search bar can be seen in the following video. The search bar filters the posts at every key stroke. An additional AMP event is called to place the window at the top of results list and the amp list height is controlled by the number of posts that are shown.

The search bar is always visible using a position: fixed css property. The code can be found at the github repository of this page.

Your browser doesn't support HTML5 video.

Conclusions

I have seen different opinions on the effect of using AMP format. For my personal point of view, my SEO has increased since I started using this format and also the page load speed. The AMP rules impose good web format that guaranties the best performances. The traffic data of my website can be found at this github page.

AMP integrates smoothly with API endpoints defined in static website build tools like Jekyll. The endpoint with the contents of the blog is a small json, which can be filtered using simple javascript functions allowed in AMP.

I’m happy with the results since, the json is still very small (less than 19kb) and I think that when I write more posts I can add more advanced functions of amp-list to include a pagination.

In relation with 🏷️ amphtml, jekyll, bulma:

Static AMP website creation with Bulma and Jekyll

This posts shows how to include bulma css classes inside a jekyll website but keeping good performance from AMP websites

Algolia search engine in Jekyll static pages

This article shows how to implementent Algolia searching engine in a static website and update the entries using Github Actions.

Writing notes with Vimwiki and Hugo static generator

Vim is a simple and ubiquitous text editor that I use daily. In this article I show how to use Vim to take and publish diary notes using Vimwiki and Hugo.

Muse: Mopidy web client with Snapcast support

Services like Mopidy and Snapcast are ideal to create a multiroom streaming audio player using devices like the RaspberryPi or android telephones. This posts presents a web interface that uses the state of the art web technologies and integrates nicely with Mopidy and Snapcast.

Subscribe to my newsletter πŸ“°

and share it with your friends: