Home Blog How to Add Search to a Jekyll Site (Lunr.js vs Algolia)
Tutorial

How to Add Search to a Jekyll Site (Lunr.js vs Algolia)

Add full-text search to your Jekyll site using Lunr.js (free, no backend) or Algolia (fast, scalable). Setup guides for both options with pros and cons.

How to Add Search to a Jekyll Site (Lunr.js vs Algolia)

Jekyll doesn’t include search out of the box — but adding it is simpler than you might think. This guide covers the two best options: Lunr.js for a free, no-backend solution and Algolia for a fast, scalable search service.


Option 1: Lunr.js (Free, No Backend Required)

Lunr.js is a client-side search library. Jekyll generates a JSON search index at build time, and Lunr searches it entirely in the browser.

Pros: Free, privacy-friendly, no external service required, works on GitHub Pages
Cons: Index grows with site size, can be slow on very large sites (500+ posts)

Step 1: Generate a Search Index

Create search-index.json at your site root. This file is regenerated on every build:


---
layout: null
---
[
  {% for post in site.posts %}
  {
    "title": {{ post.title | jsonify }},
    "url": {{ post.url | absolute_url | jsonify }},
    "date": {{ post.date | date: "%B %d, %Y" | jsonify }},
    "excerpt": {{ post.excerpt | strip_html | truncatewords: 50 | jsonify }},
    "content": {{ post.content | strip_html | truncatewords: 300 | jsonify }},
    "tags": {{ post.tags | jsonify }},
    "categories": {{ post.categories | jsonify }}
  }{% unless forloop.last %},{% endunless %}
  {% endfor %}
  {% if site.posts.size > 0 and site.pages.size > 0 %},{% endif %}
  {% for page in site.pages %}
  {% if page.title and page.layout != null %}
  {
    "title": {{ page.title | jsonify }},
    "url": {{ page.url | absolute_url | jsonify }},
    "excerpt": {{ page.content | strip_html | truncatewords: 50 | jsonify }},
    "content": {{ page.content | strip_html | truncatewords: 300 | jsonify }}
  }{% unless forloop.last %},{% endunless %}
  {% endif %}
  {% endfor %}
]

Save this as search-index.json in your site root.

Step 2: Create the Search Page

Create search.md:

---
layout: page
title: Search
permalink: /search/
---

<input type="search" id="search-input" placeholder="Search posts and pages..." autofocus>
<div id="search-results"></div>

<script src="https://unpkg.com/lunr/lunr.js"></script>
<script>
let searchIndex;
let documents = {};

fetch('/search-index.json')
  .then(res => res.json())
  .then(data => {
    data.forEach(doc => { documents[doc.url] = doc; });

    searchIndex = lunr(function () {
      this.ref('url');
      this.field('title', { boost: 10 });
      this.field('tags', { boost: 5 });
      this.field('content');

      data.forEach(doc => { this.add(doc); });
    });
  });

document.getElementById('search-input').addEventListener('input', function () {
  const query = this.value.trim();
  const resultsDiv = document.getElementById('search-results');

  if (!query || !searchIndex) {
    resultsDiv.innerHTML = '';
    return;
  }

  const results = searchIndex.search(query + '*');

  if (results.length === 0) {
    resultsDiv.innerHTML = '<p>No results found.</p>';
    return;
  }

  resultsDiv.innerHTML = results.map(result => {
    const doc = documents[result.ref];
    return `<div class="search-result">
      <a href="${doc.url}"><h3>${doc.title}</h3></a>
      <p>${doc.excerpt}</p>
    </div>`;
  }).join('');
});
</script>

Step 3: Add Search to Your Navigation

Add a search link or a header search box that submits to /search/?q=query:

<form action="/search/" method="get">
  <input type="search" name="q" placeholder="Search...">
  <button type="submit">Search</button>
</form>

Then in your search page, pre-fill from the URL parameter:

const params = new URLSearchParams(window.location.search);
const q = params.get('q');
if (q) {
  document.getElementById('search-input').value = q;
  // Trigger search...
}

Option 2: Algolia (Fast, Scalable)

Algolia is a hosted search service. It’s significantly faster than Lunr for large sites and provides instant results as you type.

Free plan: 10,000 search requests/month and 10,000 records
Pros: Fast, typo-tolerant, instant search, great developer experience
Cons: Requires an Algolia account, data is stored externally

Step 1: Create an Algolia Account

  1. Sign up at algolia.com
  2. Create a new application
  3. Create an index (e.g. jekyllhub_production)
  4. Note your Application ID, Search-Only API Key, and Admin API Key

Step 2: Install jekyll-algolia

# Gemfile
gem "jekyll-algolia"
# _config.yml
algolia:
  application_id: YOUR_APP_ID
  index_name: YOUR_INDEX_NAME
  search_only_api_key: YOUR_SEARCH_ONLY_KEY

Important: Your search-only key is safe to put in _config.yml. Never put your Admin API key in version control.

Set the Admin key as an environment variable:

export ALGOLIA_API_KEY='your-admin-key'

Step 3: Index Your Content

bundle exec jekyll algolia

This pushes all your posts and pages to Algolia. Run this whenever you add new content (or add it to your CI/CD pipeline).

Step 4: Add InstantSearch to Your Site

Algolia provides InstantSearch.js for the frontend:

<!-- search.html -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@7/themes/satellite-min.css">

<div id="searchbox"></div>
<div id="hits"></div>
<div id="pagination"></div>

<script src="https://cdn.jsdelivr.net/npm/algoliasearch@4/dist/algoliasearch-lite.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4"></script>
<script>
const searchClient = algoliasearch('YOUR_APP_ID', 'YOUR_SEARCH_ONLY_KEY');

const search = instantsearch({
  indexName: 'YOUR_INDEX_NAME',
  searchClient,
});

search.addWidgets([
  instantsearch.widgets.searchBox({
    container: '#searchbox',
    placeholder: 'Search posts...',
  }),
  instantsearch.widgets.hits({
    container: '#hits',
    templates: {
      item(hit) {
        return `
          <a href="${hit.url}">
            <h3>${instantsearch.highlight({ hit, attribute: 'title' })}</h3>
            <p>${instantsearch.snippet({ hit, attribute: 'content' })}</p>
          </a>
        `;
      },
      empty: '<p>No results found.</p>',
    },
  }),
  instantsearch.widgets.pagination({
    container: '#pagination',
  }),
]);

search.start();
</script>

Which Should You Choose?

  Lunr.js Algolia
Cost Free Free up to 10k requests/month
Setup Simple Moderate
Speed Good (small sites) Excellent (any size)
Typo tolerance Limited Built-in
Works offline Yes No
GitHub Pages Yes Yes
Privacy Full (no external service) Data sent to Algolia

Choose Lunr.js if: your site has under 200 posts, you want zero dependencies, or privacy is a priority.

Choose Algolia if: you have a large site, want instant-as-you-type results, or need typo-tolerant search.


Many Jekyll themes on JekyllHub include search functionality built in — check the theme’s feature list before building from scratch.


Pagefind: a newer alternative worth knowing

Pagefind is an open-source static search library released in 2022 that has quickly become a strong alternative to Lunr.js for Jekyll and other static sites. It works differently: instead of generating a JSON index in Liquid, Pagefind runs after the Jekyll build and indexes the compiled HTML. This makes the search index much more accurate — it searches the rendered content rather than the raw Markdown.

Install Pagefind via npm and add it to your build process:

npm install -D pagefind
{
  "scripts": {
    "build": "bundle exec jekyll build && npx pagefind --site _site"
  }
}

Add the Pagefind UI to your search page:

<link href="/_pagefind/pagefind-ui.css" rel="stylesheet">
<div id="search"></div>
<script src="/_pagefind/pagefind-ui.js"></script>
<script>
  window.addEventListener('DOMContentLoaded', () => {
    new PagefindUI({ element: "#search", showSubResults: true });
  });
</script>

Pagefind handles the index generation, the UI, and the search logic. The resulting bundle is small (typically under 50kb for average sites), and it works offline after the initial page load. For GitHub Pages, you can run Pagefind in a GitHub Actions workflow after the Jekyll build step.

Designing the search experience

The technical setup is only half of adding useful search. The search experience itself — how users discover and interact with it — determines whether people actually use it.

Keep the search input visible and prominent. A small icon in the corner that reveals a search input only when clicked adds an unnecessary interaction step. On desktop, a search box in the header or at the top of a dedicated /search/ page works better. On mobile, a full-screen search modal triggered by a fixed button is the most accessible pattern.

Show results as users type, without waiting for them to press Enter. This requires a client-side library (Lunr.js or Pagefind both support this). Debounce the input handler to avoid triggering a search on every single keystroke — wait until the user has stopped typing for 200–300 milliseconds before running the query.

Display results with enough context to judge relevance: a title, a snippet of text around the matching phrase, the publication date (for posts), and the content type if your site mixes posts, pages, and collection documents. Algolia’s InstantSearch does this with snippet highlighting built in. For Lunr.js, you implement it manually using the matching positions Lunr returns.

Provide a meaningful empty state. “No results for ‘jekyll paginate v3’” is more helpful than a blank page — it confirms the search ran and helps users understand that the content might not exist. Optionally suggest checking spelling or trying simpler terms.

Improving Lunr.js search quality

The default Lunr.js configuration is functional but not optimised. Several settings improve result quality for Jekyll blogs.

Boosting fields. Matches in the title are more relevant than matches in the body. Apply boost factors when building the index:

searchIndex = lunr(function () {
  this.ref('url');
  this.field('title', { boost: 15 });
  this.field('tags', { boost: 10 });
  this.field('categories', { boost: 5 });
  this.field('excerpt', { boost: 3 });
  this.field('content');
});

Wildcard search. Appending * to the query enables trailing wildcard matching — “jekyll*” matches “jekyll”, “jekyllhub”, “jekyll-themes”:

const results = searchIndex.search(query + '*');

This significantly improves the experience when users type partial words.

Stemming. Lunr’s built-in stemmer handles English word variants automatically — searching “deploying” also matches documents containing “deploy” and “deployment”. This works without any configuration. For non-English content, Lunr provides language-specific stemmer plugins.

Pipeline. For very precise search where you want exact matches only (no stemming), configure the pipeline explicitly:

searchIndex = lunr(function () {
  this.pipeline.remove(lunr.stemmer);
  this.ref('url');
  this.field('title');
  this.field('content');
});

If your site uses collections (a themes directory, a portfolio, a team page), include them in the search index alongside posts. Modify the index generator to loop over collection documents:


---
layout: null
---
[
  {% for post in site.posts %}
  {
    "type": "post",
    "title": {{ post.title | jsonify }},
    "url": {{ post.url | absolute_url | jsonify }},
    "excerpt": {{ post.excerpt | strip_html | truncatewords: 50 | jsonify }},
    "content": {{ post.content | strip_html | truncatewords: 200 | jsonify }},
    "tags": {{ post.tags | jsonify }}
  }{% unless forloop.last %},{% endunless %}
  {% endfor %}
  {% if site.posts.size > 0 %},{% endif %}
  {% assign theme_docs = site.themes %}
  {% for theme in theme_docs %}
  {
    "type": "theme",
    "title": {{ theme.title | jsonify }},
    "url": {{ theme.url | absolute_url | jsonify }},
    "excerpt": {{ theme.description | jsonify }},
    "content": {{ theme.content | strip_html | truncatewords: 200 | jsonify }},
    "tags": {{ theme.tags | jsonify }}
  }{% unless forloop.last %},{% endunless %}
  {% endfor %}
]

In your search results UI, use the type field to show a badge or icon that distinguishes posts from theme pages — this helps users understand what kind of content they are looking at.

Adding keyboard shortcuts

Power users appreciate keyboard shortcuts for search. The standard pattern is to open the search modal when the user presses / (the shortcut used by GitHub and many developer tools):

document.addEventListener('keydown', (e) => {
  if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) {
    e.preventDefault();
    document.getElementById('search-input').focus();
  }
  if (e.key === 'Escape') {
    document.getElementById('search-input').blur();
    document.getElementById('search-results').innerHTML = '';
  }
});

Add a hint in the search input’s placeholder: placeholder="Search (press / to focus)". Users who notice the hint remember the shortcut; users who do not notice it are unaffected.

Search analytics

Understanding what users search for reveals content gaps on your site. If many users search for a topic that returns zero results, that topic is a strong candidate for a new blog post or documentation page.

Algolia provides built-in search analytics in its dashboard — you can see the top queries, the no-results queries, and click-through rates by result position without any additional configuration.

For Lunr.js, implement basic search analytics manually by sending a custom event to your analytics tool on each search:

// Google Analytics 4
document.getElementById('search-input').addEventListener('input', debounce(function() {
  const query = this.value.trim();
  if (query.length > 2) {
    gtag('event', 'search', { search_term: query });
  }
}, 300));

Check your analytics monthly for no-results queries. Each one is a specific signal about a content gap or a terminology mismatch between how your audience describes something and the words you use in your content. Closing these gaps improves both the search experience and your site’s search engine visibility.

Search is one of the most impactful features you can add to a Jekyll site with meaningful content depth. Start with Lunr.js for simplicity, move to Pagefind or Algolia when your content grows, and invest time in the search UX — the technical setup is a weekend project, but the search experience is what your readers will actually use every visit.


Putting search in your navigation header

A search box in the header — visible on every page — dramatically increases how often visitors use search. Most users will not navigate to a dedicated /search/ page, but they will use an input that is already in front of them.

The header search pattern for Jekyll typically works like this: a visible search icon button in the nav, which opens a full-screen search overlay or a dropdown panel on click. The overlay contains the search input, which queries the index (Lunr or Algolia) as the user types and shows results below.

<button class="search-toggle" aria-expanded="false" aria-controls="search-overlay">
  <svg aria-hidden="true"><!-- search icon --></svg>
  <span class="sr-only">Search</span>
</button>

<div id="search-overlay" class="search-overlay" hidden>
  <div class="search-overlay__inner">
    <input type="search" id="search-input" placeholder="Search..." autofocus>
    <button class="search-overlay__close" aria-label="Close search">×</button>
    <div id="search-results" aria-live="polite"></div>
  </div>
</div>

The aria-live="polite" attribute on the results container announces new results to screen reader users without interrupting them mid-navigation — an important accessibility detail that most search implementations miss.

Use CSS to hide the overlay by default (display: none or visibility: hidden; opacity: 0) and a short transition to show it on button click. The search input should receive focus automatically when the overlay opens so keyboard users can type immediately without a second interaction.

Whether you use Lunr.js, Pagefind, or Algolia, the frontend overlay pattern is the same. The library only changes how the search query is executed and how results are fetched. Investing in the UX of the search interface pays off regardless of which backend you choose.

The choice between Lunr.js, Pagefind, and Algolia is ultimately a trade-off between simplicity and scale. Start with the simplest option that meets your needs. Lunr.js requires no external service and deploys anywhere — it is a solid choice for blogs and documentation sites under a few hundred pages. Pagefind is becoming the default recommendation for new static site projects due to its accuracy and ease of setup. Algolia is the right choice when you need typo tolerance, faceted filtering, and analytics dashboards without building them yourself. All three integrate cleanly with Jekyll, and switching between them later is a contained change — the search JSON index or build step changes, but the front-end overlay UI and the Jekyll content itself stay the same.

Share LinkedIn