enable full-site search in jekyll using lunr
The Importance of Search in Documentation Sites
As your documentation grows, even the best structured navigation can’t replace a powerful search feature. A search bar provides users with a direct path to what they’re looking for—especially in technical documentation where concepts are spread across pages. In static sites like Jekyll on GitHub Pages, adding search requires a client-side solution since there’s no server to run queries. That’s where Lunr.js comes in.
What Is Lunr.js?
Lunr.js is a small, full-text search library written in JavaScript. It allows you to create an index of your site's content and search through it directly in the browser. Unlike third-party solutions like Algolia or Google Custom Search, Lunr is entirely static and privacy-respecting—ideal for documentation hosted on GitHub Pages.
Benefits of Using Lunr.js
- No server or backend required
- Lightweight and open-source
- Works entirely on the client side
- Supports filtering, partial matches, and ranking
How Site Search Works in Jekyll with Lunr
The core idea is to generate a JSON index of your site’s content during the build process. This JSON file is then loaded by a JavaScript-powered search UI that uses Lunr.js to run queries. Here’s the step-by-step plan:
- Create a JSON index of your site using a Jekyll layout.
- Load that index with JavaScript on a search page.
- Use Lunr to match queries and display the results.
Step 1: Generate a Search Index File
Create a Jekyll Index Page
Create a file named search.json in your root directory with the following front matter:
---
layout: null
sitemap: false
permalink: /search.json
---
[
{% raw %}{% for page in site.pages | where_exp:"page","page.search != false" %}{% endraw %}
{
"title": "{{ page.title | escape }}",
"url": "{{ site.baseurl }}{{ page.url }}",
"content": {{ page.content | strip_html | jsonify }}
}{% unless forloop.last %},{% endunless %}
{% raw %}{% endfor %}{% endraw %}
]
This code loops through your site’s pages (excluding those with search: false in their front matter) and extracts titles, URLs, and content stripped of HTML to create a lightweight searchable index.
Step 2: Add the Search UI
Create a Search Page
In your pages or docs folder, create a new file named search.html:
---
layout: default
title: Search
permalink: /search/
---
<h2>Search the Docs</h2>
<input type="text" id="search-box" placeholder="Search..." />
<ul id="search-results"></ul>
<script src="https://unpkg.com/lunr/lunr.min.js"></script>
<script>
// Fetch the index
fetch('/search.json')
.then(response => response.json())
.then(data => {
const idx = lunr(function () {
this.field('title')
this.field('content')
this.ref('url')
data.forEach(doc => this.add(doc))
});
const searchBox = document.getElementById('search-box');
const resultsList = document.getElementById('search-results');
searchBox.addEventListener('input', function () {
const query = this.value.trim();
resultsList.innerHTML = '';
if (query.length < 2) return;
const results = idx.search(query);
results.forEach(result => {
const match = data.find(d => d.url === result.ref);
const li = document.createElement('li');
const link = document.createElement('a');
link.href = match.url;
link.textContent = match.title;
li.appendChild(link);
resultsList.appendChild(li);
});
});
});
</script>
Optional Styling
Add basic styles in your main stylesheet:
#search-box {
width: 100%;
padding: 0.5em;
margin-bottom: 1em;
}
#search-results {
list-style: none;
padding: 0;
}
Step 3: Filter Searchable Content
Exclude Pages from Search
If you have pages that shouldn't be indexed (like landing pages or custom redirects), add this to their front matter:
search: false
This flag ensures your search.json generation loop skips them.
Limit Index Size
To avoid bloating your JSON, limit {{ page.content | strip_html }} to a certain length:
"content": {{ page.content | strip_html | truncate: 300 | jsonify }}
This keeps your index lightweight and fast.
Improving Search Relevance
Weighting Title vs Content
By default, Lunr gives equal weight to fields. You can give titles more influence with boosts:
this.field('title', { boost: 10 })
Tokenization
Lunr supports fuzzy search and wildcard matches. You can refine search queries like so:
idx.search(query + '*')
Highlighting Matches
Highlighting keywords in results improves UX. After finding results, wrap keywords with <mark> using regex or a small highlighting library.
Deploying to GitHub Pages
No special setup is needed for GitHub Pages. The JSON file is generated statically by Jekyll and served like any other asset. Just push your changes, and your search is live.
Advanced: Category-Based Search
Want to let users filter by documentation section? Extend your search.json to include categories:
{
"title": "{{ page.title | escape }}",
"url": "{{ site.baseurl }}{{ page.url }}",
"category": "{{ page.categories | join: ',' }}",
"content": {{ page.content | strip_html | jsonify }}
}
Then add UI controls to filter results by category.
Use Case: Search Across Versions
If you have multiple documentation versions, create a separate index file for each version. Load the correct index dynamically based on user selection or current version path.
Conclusion
Adding Lunr-based search to your Jekyll documentation site offers a powerful, fast, and self-hosted solution to navigate large amounts of content. With no dependencies beyond static assets and no external services required, it fits perfectly into a GitHub Pages workflow. Next, we’ll explore how to version your documentation intelligently using URL paths and collections.
