Building server-rendered search for static sites with 11ty Serverless, Netlify, and Algolia
Tags: tutorial, 11ty, webdev, jamstack
Published: 2021-10-08 19:20:04

Progressive enhancement is an important topic when creating any web site or app. What happens when a user's browser isn't able to handle any JavaScript or the specific JavaScript you're using? If you're front end fails, you need a fallback to allow basic functionality to keep working.

What's so hard about that?

When you're working on the Jamstack, that can be much harder than in traditional stacks. Since the Jamstack is dedicated to serving HTML from a CDN, we don't have a traditional server – only static files and serverless functions. Because of that, we might find ourselves rewriting our rendering code for both the frontend and the server. Three years and a redesign go by and suddenly you're in a time machine looking at search results in last year's design.

With 11ty's new Serverless package and Netlify functions, we can quickly start from the server. From there, we can progressively enhance the experience using libraries like InstantSearch.js.

## What is 11ty Serverless?

11ty is a static site generator written in Node.js. It offers multiple template languages and multiple methods for ingesting data. It's built with flexibility in mind. 

Historically, it does all of its work during the build process and generates HTML that can be stored on a CDN. This creates speedy websites that work incredibly well on the Jamstack. 

For any dynamic content, it has relied on front-end JavaScript to fetch data. This works in many situations, but provides no clear path toward progressive enhancement.

If your front-end code fails, the dynamic aspects of your site will fail with it. With the upcoming 1.0 release of 11ty, that won't be a concern any more. 11ty will come bundled with the optional 11ty Serverless plugin. This will allow a developer to specify routes that can handle user input. That input can come from query parameters or from the URL structure itself.

## What we're building

![Screenshot of the live site with 11ty query populating search results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x5sl57iwfegn79xkwgit.png)

In this demo, we'll take a very simple 11ty site and add a dynamic search route. That search route will use 11ty and a template filter to create HTML in a serverless function – all from within the 11ty code we're used to. This will be used for the "fallback" in our progressive enhancement. While we won't go into building out a JavaScript-based search in this demo, creating a [real-time search with InstantSearch.js](https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/js/?utm_source=blog&utm_medium=main-blog&utm_campaign=devrel-jamstack&utm_id=11ty-serverless) is a great basis for a solid user experience.

How we'll build this:

1.  Install the plugin and configure the serverless function
2.  Create the search page
3.  Create a `getResults` template filter to query our Algolia Index

## Setup

We'll start from a basic 11ty template with just a little HTML to get us going.

To get started, [clone this repository](https://github.com/brob/11ty-serverless-search) and install the dependencies (11ty 1.0 and dotenv).

Want to see the finished product? Check out the `final` branch of the repository or see [this demo site](https://11ty-search.netlify.app/).

```sh
npm install && npm start
```

The structure of our project follows the basic structure of an 11ty site. The individual pages are in the root of the project – currently, only the `index.html` file. The templates are in the `_includes` directory. The configuration file is `.eleventy.js` in the root. The site templates are also relatively simplistic: a base template that includes a header and footer.

Once the install is done, we'll have a working 11ty site running locally. It's not very interesting yet, just an index page with a little HTML. Let's add a search page to bring in some content.

## Install and configure the 11ty Serverless plugin

In its latest 1.0 "canary" versions, 11ty ships with the plugin 11ty Serverless. This helps generate the serverless function we'll need to run 11ty on demand.

To install it in our project, we need to update the `.eleventy.js` configuration file. 

```js
require("dotenv").config();  
const { EleventyServerlessBundlerPlugin } = require("@11ty/eleventy");  

module.exports = function(eleventyConfig) {  
    // Configuration rules  
    eleventyConfig.addPlugin(EleventyServerlessBundlerPlugin, {  
       name: "search", // The serverless function name for the permalink object 
       functionsDir: "./netlify/functions/",  
     });  
};
```

Since 11ty is creating files in our project that we don't want to track in version control, update your `.gitignore` with the following items:

```
netlify/functions/search/**  
!netlify/functions/search/index.js
```

When we rerun `npm start`, 11ty will now create a serverless function – and all necessary bundle files – in the directory specified by `functionsDir` with a name specified by the `name` property. 

For most uses, you won't modify these files. The plugin generates the `index.js` file, which can be edited for more advanced use cases. 11ty will override the other files in this directory on each run.

## Create a page to use the serverless function

Now that 11ty has created the function, we can add a page to use it.

Start by creating a new file in the root of the project named `search.html`. In the file, we can configure the page's data with frontmatter.

```html
---  
layout: "base.html"  
title: Search Page  
permalink:  
 search: /search/  
---
```

The `layout` variable will indicate which template in `_includes` to use for display. The `title` variable will display in the HTML's `<title>` element.

The `permalink` object is where we specify what the final URL for this page should be. 

If you're familiar with 11ty, you may remember the `permalink` variable being a string. You can still use a string for simple use cases, but for Serverless, it will be an object. The object's keys will be the name we specified in the configuration for our serverless function.

You can also specify different serverless functions to run based on the URL in this way. If you want the page generated at build time AND request time, you can specify a `build` key for the permalink, as well.

Once added, the page will render at `/search/`. It doesn't have any content yet other than the header and footer. Let's grab some dynamic content from query parameters.

```html
---
layout: "base.html"
title: Search Page
permalink:
  search: /search/
---

<h2 class="is-size-3 mb-3">This list is built at request from the query "{{ eleventy.serverless.query.query }}"</h2>
```
 

This will create a headline that will look at the query parameters of our route and insert whatever value the `query` parameter contains.

If you visit the page with `?query=11ty` at the end of the URL, the string `11ty` will appear in the headline.

So, how do we take that query and get results from Algolia?

## Create a `getResults` template filter

To get the data we need to render our template, we need to create a template filter. The filter will accept the query string from the serverless page, run a query against an Algolia Index, and return an array of articles to our search page.

Before we dive into the code, you'll need to have an Algolia app and a few environment variables. If you already have an Algolia Index, feel free to use that. We'll be using an Index that has blog posts with titles, descriptions, and URLs. If you don't have an Algolia Index, [create an account and app,](https://www.algolia.com/users/sign_up?utm_source=blog&utm_medium=main-blog&utm_campaign=devrel-jamstack&utm_id=11ty-serverless) and use this data to manually create your first Index.

```json
[
    {
        "title": "Creating an omnibar with Autocomplete",
        "description": "In this tutorial, we’ll walk through setting up Autocomplete to fire interactions with JavaScript. Specifically, we’ll build an omnibar to toggle light and dark mode for our website. An omnibar is a search field that has both search and actions that can be taken. A strong example of this is the Chrome or Firefox search and URL bar.",
        "url": "https://www.algolia.com/blog/engineering/creating-an-omnibar-with-autocomplete/"
    },
    {
        "title": "Building a Store Locator in React using Algolia, Mapbox, and Twilio – Part 1",
        "description": "These days, ecommerce shoppers expect convenience and want the physical and online worlds to mesh allowing them to conduct their business on whichever channel they want. For example, users may choose to:",
        "url": "https://www.algolia.com/blog/engineering/building-a-store-locator-in-react-using-algolia-mapbox-and-twilio-part-1/"
    },
    {
        "title": "Introducing Algolia Recommend: The next best way for developers to increase revenue",
        "description": "Now, with the introduction of Algolia Recommend, Algolia further enables developers to unleash the component of the experience that drives the remaining part of the product discovery experience: product recommendations. ",
        "url": "https://www.algolia.com/blog/product/introducing-algolia-recommend-the-next-best-way-for-developers-to-increase-revenue/"
    }
]
```

Once you have an Index, create a `.env` file and add the following variables:

```yaml
# The app id
ALGOLIA_APP = "" 
# The search-only API key
ALGOLIA_SEARCH_KEY = "" 
# The index name
ALGOLIA_INDEX = "" 
```

Once those are in place, we can submit queries via the Algolia JavaScript client to get results. Since our query is accessible in our template, we'll create a new template filter to use the query and return the results.

To create a new filter, we need to extend 11ty in the `.eleventy.js` configuration file.

First, we'll install the `algoliasearch` NPM package.

```sh
npm install algoliasearch
```

```js
require("dotenv").config();
const { EleventyServerlessBundlerPlugin } = require("@11ty/eleventy");
const algoliasearch = require("algoliasearch");

const client = algoliasearch(process.env.ALGOLIA_APP, process.env.ALGOLIA_SEARCH_KEY);
const index = client.initIndex(process.env.ALGOLIA_INDEX);

module.exports = function (eleventyConfig) {
  eleventyConfig.addPlugin(EleventyServerlessBundlerPlugin, {
    name: "search", // The serverless function name from your permalink object
    functionsDir: "./netlify/functions/",
  });

  eleventyConfig.addFilter("getResults", function (query) {
    return index.search(query, {
      attributesToRetrieve: ["title", "url", "date", "description"],

    }).then(res => {
      return res.hits;
    })
  });

};
```

At the top of the file, we'll set up the Algolia search client with the API keys, app name, and index name. After that, inside of the exported function, we'll use 11ty's `addFilter()` method to add a filter.

This method accepts two arguments: a string to use as the filter, and a function to execute when used. The function will receive the data passed from the page file. In this case, it will be the user-entered query.

In this simple example, we pass the query into the `index.search()` method and request back only the attributes we need to keep our response small. When that returns, we can return the results back to our page for use in a template loop.

```html
---
layout: "base.html"
title: Search Page
permalink:
  search: /search/
---

<h2 class="is-size-3 mb-3">This list is built at request from the query "{{ eleventy.serverless.query.query }}"</h2>

{% assign results = eleventy.serverless.query.query | getResults %}
<div class="card-grid">
  {% for result in results %}
    {% include "article.html" %}
  {% endfor %}
</div>
```

In the page, we use the built-in `assign` tag in Liquid to assign the data to a variable. Then, we can loop through the returned array and pass that information into an include. This include can be used for these results as well as ANY article display on the site. The `article.html` file should be created in the `_includes` directory.

```html
<article class="card column">
    <h2 class="title"><a href="{{ result.url }}">{{ result.title }}</a></h2>
    <p class="content">{{ result.description }}</p>
</article>
```

We now have a working server-rendered search in a statically-generated site thanks to 11ty Serverless. What are some other patterns in static sites that 11ty Serverless can help us overcome?