The issue

I like the JAM-idea, generate your content and serve static files.
The benefits are obvious, serving basic files makes scaling easier and cheaper by using a CDN.

But some features don't work that well without a server, a search feature is usually done by a server getting a search string and returning results.

This isn't possible in 11ty, you'd need some kind of server that knows all your content and returns results based on an input.

Or is it?

The solution

It is possible for our website to know all the content, at least in a condensed version. Not all searches need to search every property, in the case of this blog it would be enough to search the title, excerpt, tags and some predefined keywords.

We can easily generate an index at build time that knows about all the posts, and then let the client load and search this file. Similar to what an RSS-feed in 11ty does.

The index

First, we'll create a new nunjucks file that writes a search.json file at build time. This page loops through all our posts and returns a JSON array that we'll load with a bit of JavaScript.

search-index.njk
---
permalink: "/search.json"
---
[
  {%- for post in collections.post | reverse %}
    {%- set url %}{{ post.url | url }}{% endset -%}
    {
      "url": "{{ url }}",
      "title": "{{ post.data.title }}",
      "excerpt": "{{ post.data.excerpt }}",
      "keywords": "{{ post.data.keywords }}",
      "tags": "{{ post.data.tags }}"
    }
    {%- if not loop.last -%}
    ,
    {%- endif -%}
  {%- endfor %}
]

The Javascript

The Javascript is equally simple, fetch the /search.json file and then check every entry against the search string.

This is a very basic search, you could build a more sophisticated search by using something like lunr which ranks results based on the search term.

search.njk
<input type="text" id="search" autocomplete="off" />
<div id="results"></div>
<script src="search.js" async defer></script>
search.js
(async () => {
  document.getElementById('search').addEventListener('keyup', (event) => {
    const searchString = event.target.value.toLowerCase()
    const results = []
    posts.forEach((post) => {
      if (
        post.title.toLowerCase().includes(searchString) ||
        post.excerpt.toLowerCase().includes(searchString) ||
        post.keywords.toLowerCase().includes(searchString) ||
        post.tags.toLowerCase().includes(searchString)
      ) {
        results.push(`<a href="${post.url}">
          <h2>
            ${post.title}
          </h2>
          <p>
            ${post.excerpt}
          </p>
        </a>`)
      }
    })

    document.getElementById('results').innerHTML = results.join('')
  })

  const posts = await fetch('/search.json').then(res => res.json())
})()