Posts

Showing posts with the label documentation

boosting performance for jekyll client side search

Why Performance Matters in Client-Side Search

Client-side search relies on JavaScript and JSON indexes loaded into the browser. As your knowledge base grows, search responsiveness and page load times can degrade if not optimized. Enhancing performance leads to smoother user experiences and lower bounce rates.

Common Performance Bottlenecks

  • Large JSON index files increasing initial load time
  • Slow parsing and processing of search data in JavaScript
  • Unoptimized UI updates causing jank or delays
  • Repeated fetching of the index file on each page visit

Strategies for Effective Caching

Caching reduces redundant network requests and speeds up subsequent searches.

HTTP Cache-Control Headers

Ensure your search.json file is served with proper cache headers via GitHub Pages or CDN settings to allow long-lived caching.

LocalStorage Caching

Use localStorage to store the parsed search index on the client, avoiding repeated downloads.


async function loadSearchIndex() {
  const cached = localStorage.getItem('searchIndex');
  if (cached) {
    return JSON.parse(cached);
  } else {
    const response = await fetch('/search.json');
    const data = await response.json();
    localStorage.setItem('searchIndex', JSON.stringify(data));
    return data;
  }
}

Lazy Loading and Code Splitting

Only load search scripts and data when the user interacts with the search UI.

  • Defer loading the search library until the search box is focused
  • Use dynamic imports or script tags injected on demand

Optimizing JSON Index Size

Smaller indexes load and parse faster.

  • Strip unnecessary fields or verbose content
  • Compress JSON during build with tools or plugins
  • Consider using more compact formats like JSONL or binary (advanced)

Debouncing Search Input

Prevent firing a search on every keystroke by debouncing input events.


let debounceTimeout;
searchInput.addEventListener('input', () => {
  clearTimeout(debounceTimeout);
  debounceTimeout = setTimeout(() => {
    performSearch(searchInput.value);
  }, 300);
});

Profiling and Measuring Performance

Use browser dev tools to profile script execution and identify slow spots.

  • Monitor network waterfall for asset load times
  • Use JavaScript CPU profiling to detect heavy functions
  • Track frame rates and UI responsiveness

Case Study: Performance Gains in a Medium-Sized Knowledge Base

By implementing localStorage caching and lazy loading search assets, a knowledge base of 500+ pages reduced initial search load time by 70%, with near-instant searches thereafter. Debounced inputs improved UI responsiveness significantly.

Conclusion

Optimizing performance for client-side search in Jekyll knowledge bases requires a combination of caching, code management, and thoughtful UI handling. These improvements lead to fast, responsive, and enjoyable user experiences on fully static GitHub Pages sites.

enhancing client search with filters and ui

Introduction to Search UI Enhancements

Basic client-side search provides functionality to find content, but a polished user experience requires thoughtful interface design. Enhancing your Jekyll knowledge base with filters, result highlighting, and intuitive controls empowers users to find relevant information quickly and easily.

Benefits of Advanced Search Interfaces

  • Improved search accuracy with category and tag filters
  • Clear result presentation with highlights and excerpts
  • Faster user decisions with pagination and sorting

Building the Search Input and Results Panel

Start by structuring your search page with a search input, filters panel, and results container.

Basic HTML Structure


<input type="search" id="search-box" placeholder="Search knowledge base..." />
<div id="filters">
  <label><input type="checkbox" name="category" value="tutorial"> Tutorials</label>
  <label><input type="checkbox" name="category" value="reference"> Reference</label>
</div>
<div id="search-results"></div>

Implementing Filtering Logic

Filters restrict results by category, tags, or other metadata. Combine filter selections with the search query to narrow results.

JavaScript Example for Filtering


function applyFilters(results, filters) {
  return results.filter(item => {
    // Check if item categories include all selected filters
    return filters.categories.every(cat => item.categories.includes(cat));
  });
}

Highlighting Search Terms in Results

Highlighting matched terms helps users quickly identify why a result was returned.

Using Fuse.js Match Data

Fuse.js returns match indices which can be used to wrap matched substrings with a highlight span.


function highlightMatches(text, matches) {
  let result = '';
  let lastIndex = 0;
  matches.forEach(match => {
    const [start, end] = match;
    result += text.substring(lastIndex, start);
    result += '<span class="highlight">' + text.substring(start, end + 1) + '</span>';
    lastIndex = end + 1;
  });
  result += text.substring(lastIndex);
  return result;
}

Adding Pagination for Large Result Sets

Displaying many results at once can overwhelm users and slow page rendering. Pagination breaks results into manageable pages.

Simple Pagination Logic

  • Set a page size (e.g., 10 results per page)
  • Calculate total pages based on result count
  • Show only the current page results
  • Render pagination controls to navigate pages

Accessibility and Responsive Design

Ensure search UI works well on all devices and for users with disabilities.

  • Use semantic HTML elements
  • Support keyboard navigation
  • Make controls visible and large enough for touch
  • Test with screen readers

Case Study: Implementing Filters and Highlights

In a recent project, integrating category filters with Fuse.js search reduced irrelevant results by 40%. Highlighting matched terms increased user engagement and decreased time to find information.

Conclusion

Enhancing client-side search UI with filters, highlighting, and pagination transforms a simple search box into a powerful navigation tool for your Jekyll knowledge base. This leads to better user satisfaction and knowledge retention.

mastering fuzzy search and indexing in jekyll

Understanding Fuzzy Search in Static Knowledge Bases

Fuzzy search allows users to find relevant results even when search queries contain typos, partial words, or approximate terms. For Jekyll knowledge bases hosted on GitHub Pages, fuzzy search is performed client-side using JavaScript libraries like Fuse.js, which work with JSON indexes.

Why Fuzzy Search Improves User Experience

  • Reduces user frustration from exact-match requirements
  • Handles spelling errors and typos gracefully
  • Enables partial and approximate matching for broader results

Building an Efficient Search Index

The search index is the heart of client-side search. It contains structured metadata for all searchable pages. A well-structured, minimal index optimizes search speed and user experience.

Index Structure Best Practices

  • Include essential fields only: title, URL, excerpt, tags, categories
  • Normalize text: lowercase all text to support case-insensitive matching
  • Remove unnecessary content: avoid large HTML snippets or images
  • Use concise excerpts: 150–200 characters summarizing the page

Generating the JSON Index Automatically

Using Jekyll plugins or custom scripts, you can generate search.json at build time. Example YAML configuration snippet for a custom Jekyll plugin:


module Jekyll
  class SearchIndexGenerator < Generator
    safe true
    priority :lowest

    def generate(site)
      entries = site.posts.docs.map do |post|
        {
          title: post.data['title'],
          url: post.url,
          excerpt: post.data['excerpt'] || post.content[0..150],
          tags: post.data['tags'] || [],
          categories: post.data['categories'] || []
        }
      end
      File.write('_site/search.json', JSON.pretty_generate(entries))
    end
  end
end

Optimizing Fuse.js Search Configuration

Fuse.js offers several options that affect search precision and performance. Fine-tuning these parameters is crucial for optimal results.

Key Fuse.js Options Explained

Option Description Recommended Setting
threshold Controls fuzziness; 0 exact match, 1 matches everything 0.3–0.4
distance Maximum distance for approximate match 100
minMatchCharLength Minimum query length to perform fuzzy matching 2 or 3
keys Fields to search with weighting e.g., [{name:'title', weight:0.7}, {name:'excerpt', weight:0.3}]

Example Fuse.js Initialization


const options = {
  keys: [
    { name: 'title', weight: 0.7 },
    { name: 'excerpt', weight: 0.3 }
  ],
  threshold: 0.35,
  distance: 100,
  minMatchCharLength: 3,
  includeMatches: true
};
const fuse = new Fuse(data, options);

Handling Large Knowledge Bases

For sites with hundreds or thousands of pages, the search index can become large and slow client-side search.

Strategies to Manage Large Indexes

  • Split index by category or section: Load smaller indexes on demand
  • Lazy load search scripts and indexes: Only load search on user interaction
  • Use pagination or limit results displayed: Show top N results

Preprocessing Text for Better Search

Preprocessing index text helps the search engine find matches more reliably.

  • Strip HTML tags from excerpts
  • Remove stopwords for cleaner indexing
  • Stem words to match different forms (running vs run)

Testing and Measuring Search Quality

Regular testing is important to maintain search quality as content grows.

  • Create test queries with expected results
  • Measure response times on various devices
  • Collect user feedback for usability improvements

Conclusion

Mastering fuzzy search and indexing optimization ensures your Jekyll knowledge base is fast, responsive, and helpful. With careful planning and tuning, client-side search can rival backend-powered solutions while remaining fully static and GitHub Pages-friendly.

enhancing search experience for jekyll knowledge base

Why Search UX Matters in Knowledge Bases

When building a knowledge base, providing effective search functionality is critical. A powerful search engine that is difficult or frustrating to use will drive users away. For Jekyll sites hosted on GitHub Pages, the search is typically client-side, using pre-built JSON indexes. To maximize usability, adding advanced UX features is key.

Core UX Features to Consider

  • Autocomplete suggestions as users type
  • Fuzzy matching to handle typos and partial words
  • Instant result previews without page reloads
  • Keyboard navigation support within results
  • Highlighting matched terms in results

Implementing Autocomplete with Fuse.js

Fuse.js is a lightweight fuzzy-search library perfect for client-side search on static sites. It provides fuzzy matching and supports weighting fields.

Step 1 Setup Fuse.js

Add Fuse.js to your project, either by CDN or local script:


<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/fuse.min.js"></script>

Step 2 Prepare Your Search Index

Your search.json should include fields like title, excerpt, tags, and URL for each page. Example entry:


{
  "title": "How to Use Jekyll Collections",
  "excerpt": "Learn to organize your content with Jekyll collections...",
  "tags": ["jekyll", "collections"],
  "url": "/jekyll-collections"
}

Step 3 Initialize Fuse


const options = {
  keys: ['title', 'excerpt', 'tags'],
  threshold: 0.3,
  includeMatches: true,
  minMatchCharLength: 2
};
let fuse;

fetch('/search.json')
  .then(res => res.json())
  .then(data => {
    fuse = new Fuse(data, options);
  });

Building Autocomplete UI

Create a search input box with a results dropdown container in your layout:


<input type="search" id="search-box" placeholder="Search articles..." autocomplete="off">
<div id="autocomplete-results" role="listbox"></div>

Listening to Input Events


const searchBox = document.getElementById('search-box');
const resultsContainer = document.getElementById('autocomplete-results');

searchBox.addEventListener('input', () => {
  const query = searchBox.value.trim();
  if(query.length < 2) {
    resultsContainer.innerHTML = '';
    return;
  }
  const results = fuse.search(query);
  renderResults(results);
});

function renderResults(results) {
  if(!results.length) {
    resultsContainer.innerHTML = '<p>No results found</p>';
    return;
  }
  resultsContainer.innerHTML = results.map(({ item, matches }) => {
    // Highlight matched terms in title
    let highlightedTitle = item.title;
    if(matches) {
      matches.forEach(match => {
        if(match.key === 'title') {
          match.indices.forEach(([start, end]) => {
            const before = highlightedTitle.substring(0, start);
            const matchText = highlightedTitle.substring(start, end + 1);
            const after = highlightedTitle.substring(end + 1);
            highlightedTitle = `${before}<mark>${matchText}</mark>${after}`;
          });
        }
      });
    }
    return `
      <a href="${item.url}" role="option" tabindex="0">${highlightedTitle}</a>
    `;
  }).join('');
}

Enhancing Accessibility and Keyboard Navigation

Support arrow keys and Enter key to navigate and select results:


let selectedIndex = -1;

searchBox.addEventListener('keydown', e => {
  const options = resultsContainer.querySelectorAll('a');
  if(e.key === 'ArrowDown') {
    selectedIndex = (selectedIndex + 1) % options.length;
    options[selectedIndex].focus();
    e.preventDefault();
  } else if(e.key === 'ArrowUp') {
    selectedIndex = (selectedIndex - 1 + options.length) % options.length;
    options[selectedIndex].focus();
    e.preventDefault();
  } else if(e.key === 'Enter' && selectedIndex > -1) {
    options[selectedIndex].click();
    e.preventDefault();
  }
});

Instant Search Result Previews

Instead of navigating away immediately, you can fetch and show a brief preview snippet from the selected article, improving user confidence.

Approach

  • Use AJAX to fetch the article snippet or excerpt
  • Display it inline below the search result
  • Allow users to click to go to full article

Performance Considerations

  • Limit search index size for very large sites
  • Debounce input to reduce redundant searches
  • Use caching or localStorage for search data
  • Lazy load Fuse.js or search index on demand

Conclusion

Enhancing your Jekyll knowledge base with advanced client-side search features provides users a seamless, fast, and pleasant way to discover content. Combined with previous articles on search indexing and offline caching, this approach creates a modern, user-friendly documentation site on GitHub Pages without backend dependencies.

building offline support for jekyll knowledge base

Why Offline-First Matters for Static Documentation

Most developers assume static sites are already lightweight enough, but even static documentation can benefit from offline support. It improves reliability, supports field use (e.g. technical teams without internet), and enhances perceived performance. This is especially useful for knowledge bases or documentation sites built with Jekyll and hosted on GitHub Pages.

Advantages of Offline Support

  • Enable content access without internet
  • Improve load time on repeated visits
  • Reduce dependency on GitHub Pages’ uptime
  • Lay the foundation for full Progressive Web App features

What You’ll Need to Set Up

GitHub Pages doesn’t allow server-side code, but with Service Workers and a cache-first strategy, you can create a seamless offline experience using only client-side code and static files.

Basic Requirements

  • Valid HTTPS domain (required by Service Workers)
  • List of assets to cache (HTML, CSS, JS, JSON)
  • A custom service-worker.js file
  • Registering the Service Worker on page load

Creating Your Service Worker File

Create a file called service-worker.js at the root of your site:


const cacheName = 'jekyll-knowledge-base-v1';
const staticAssets = [
  '/',
  '/index.html',
  '/search.json',
  '/assets/css/main.css',
  '/assets/js/search.js',
  '/assets/js/autocomplete.js',
  '/favicon.ico'
];

self.addEventListener('install', async event => {
  const cache = await caches.open(cacheName);
  await cache.addAll(staticAssets);
});

self.addEventListener('fetch', event => {
  const req = event.request;
  event.respondWith(cacheFirst(req));
});

async function cacheFirst(req) {
  const cached = await caches.match(req);
  return cached || fetch(req);
}

This service worker pre-caches static files during installation and serves them from cache when offline. You can expand staticAssets to include other collections or pages as needed.

Registering the Service Worker

Add this JavaScript to the bottom of your _layouts/default.html or in a site-wide script:


if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')
      .then(reg => console.log("Service Worker registered:", reg))
      .catch(err => console.error("Service Worker failed:", err));
  });
}

This will activate your offline mode for any returning user who visits with a modern browser.

Caching Jekyll Collections and Pages

To dynamically generate a list of Jekyll pages or collections to cache, you can create a Liquid-generated asset manifest:


---
layout: null
permalink: /asset-manifest.json
---

[
  {% for page in site.pages %}
    {% unless page.layout == null %}
      "{{ page.url | relative_url }}"{% unless forloop.last %},{% endunless %}
    {% endunless %}
  {% endfor %}
]

Then modify your service worker to fetch and cache these entries during install.

Enabling Offline Search

Since your search logic is client-side and relies on search.json, caching that file enables offline search to work as well. Ensure you add it to your asset list.

Test Offline Search Scenario

  1. Load your site while online
  2. Visit 2–3 article pages
  3. Disconnect from the internet
  4. Try navigating to previously visited pages and using the search box

If implemented correctly, content loads from cache, and search functionality continues to work without an active connection.

Expanding to Full PWA Capabilities

To truly turn your knowledge base into a Progressive Web App (PWA), consider adding:

  • manifest.json file with app name, icon, theme color
  • Home screen install prompt support
  • Background sync or push notifications (if using external APIs)

Sample manifest.json


{
  "name": "Jekyll Knowledge Base",
  "short_name": "KnowledgeBase",
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#ffffff",
  "background_color": "#ffffff",
  "icons": [{
    "src": "/icon.png",
    "sizes": "192x192",
    "type": "image/png"
  }]
}

Then reference it in your layout's head:


<link rel="manifest" href="/manifest.json">

Best Practices for Static Offline Support

  • Version your cache names to bust outdated files
  • Don’t cache admin-only or private pages
  • Use lazy caching if the site grows large
  • Monitor Service Worker errors in browser console

Conclusion

Offline-first experiences are not just for mobile apps. Even your static Jekyll documentation can benefit from them. By setting up a simple Service Worker, you can dramatically increase your site’s usability, even in low-connectivity scenarios.

Combined with searchable JSON indexes and client-side logic, your knowledge base becomes more robust, reliable, and modern—all without leaving the GitHub Pages ecosystem.

implementing autocomplete and smart suggestions in jekyll search

Adding Smart Search to Your Knowledge Base

Autocomplete and smart suggestions are key UX features that help users discover content quickly. When implemented well, they reduce bounce rates and improve time on site. In this final installment, we’ll build a feature-rich autocomplete system that works with your existing Jekyll setup and requires no backend.

Why Autocomplete Matters

  • Provides immediate feedback on what’s searchable
  • Reduces user typing effort
  • Surfaces relevant content before a full search is triggered

With a static JSON index and a bit of JavaScript, you can create this experience entirely on the client side and host it for free on GitHub Pages.

Preparing Your Search Index for Autocomplete

Your search index should include titles and excerpts, since those are most relevant for quick suggestions. Here’s an example:


[
  {% assign docs = site.guides | concat: site.faqs %}
  {% for doc in docs %}
  {
    "title": "{{ doc.title | escape }}",
    "url": "{{ doc.url | relative_url }}",
    "excerpt": {{ doc.content | markdownify | strip_html | truncatewords: 20 | jsonify }},
    "tags": {{ doc.tags | jsonify }}
  }{% unless forloop.last %},{% endunless %}
  {% endfor %}
]

Keep this index lightweight—no need to include full content if your main goal is to surface suggestions while typing.

Setting Up the Autocomplete UI

Use a simple input and container for showing results:



    Style the results to match your theme and ensure they are keyboard navigable.

    Building the Autocomplete Logic

    Here’s a basic autocomplete engine using JavaScript. It filters on the fly and highlights matches:

    
    let searchData = [];
    
    fetch('/search.json')
      .then(res => res.json())
      .then(data => searchData = data);
    
    document.getElementById('autocomplete-input').addEventListener('input', function() {
      const query = this.value.toLowerCase();
      const matches = searchData.filter(entry => entry.title.toLowerCase().includes(query));
      showSuggestions(matches.slice(0, 5)); // Limit to 5 suggestions
    });
    
    function showSuggestions(results) {
      const list = document.getElementById('autocomplete-results');
      list.innerHTML = "";
      results.forEach(item => {
        const li = document.createElement("li");
        li.innerHTML = "<a href='" + item.url + "'>" + highlightQuery(item.title) + "</a>";
        list.appendChild(li);
      });
    }
    
    function highlightQuery(text) {
      const query = document.getElementById('autocomplete-input').value;
      return text.replace(new RegExp(`(${query})`, 'gi'), "<strong>$1</strong>");
    }
    

    This is a minimal example, but it forms the base for a powerful UX.

    Adding Fuzzy Matching for Better Suggestions

    To improve flexibility, use libraries like Fuse.js for fuzzy matching:

    
    const fuse = new Fuse(searchData, {
      keys: ['title', 'excerpt', 'tags'],
      threshold: 0.3
    });
    
    document.getElementById('autocomplete-input').addEventListener('input', function() {
      const query = this.value;
      const results = fuse.search(query).slice(0, 5);
      showSuggestions(results.map(r => r.item));
    });
    

    This allows typos and partial matches to still produce useful suggestions, which improves the success rate for users significantly.

    Supporting Keyboard Navigation

    Enhance accessibility by letting users navigate results via keyboard:

    
    let selectedIndex = -1;
    document.getElementById('autocomplete-input').addEventListener('keydown', function(e) {
      const items = document.querySelectorAll("#autocomplete-results li");
      if (e.key === "ArrowDown") {
        selectedIndex = (selectedIndex + 1) % items.length;
        updateHighlight(items);
      } else if (e.key === "ArrowUp") {
        selectedIndex = (selectedIndex - 1 + items.length) % items.length;
        updateHighlight(items);
      } else if (e.key === "Enter" && selectedIndex > -1) {
        items[selectedIndex].querySelector('a').click();
      }
    });
    
    function updateHighlight(items) {
      items.forEach((item, i) => item.classList.toggle("highlight", i === selectedIndex));
    }
    

    This makes the experience more intuitive and works well with screen readers.

    Integrating Smart Suggestions from Tags and Categories

    You can also precompute suggestion lists from your tags or categories and match them against user input:

    
    const tagSuggestions = [...new Set(searchData.flatMap(doc => doc.tags))];
    
    document.getElementById("autocomplete-input").addEventListener("input", function() {
      const input = this.value.toLowerCase();
      const relatedTags = tagSuggestions.filter(tag => tag.toLowerCase().startsWith(input));
      renderTagHints(relatedTags);
    });
    

    This hybrid approach helps both new and returning users quickly navigate your documentation.

    Performance Tips for Large Indexes

    • Lazy-load the search index after page load
    • Throttle input events (e.g. debounce by 200ms)
    • Use session storage to cache the index

    Also consider compressing your search.json using Gzip on build if you deploy to Netlify or Cloudflare Pages.

    Using Third-Party Libraries (Optional)

    Popular libraries for autocomplete in static sites:

    These libraries handle edge cases, accessibility, and UI patterns without reinventing the wheel.

    Conclusion

    Autocomplete and smart suggestions can dramatically enhance the usability of your Jekyll-powered knowledge base. With the right structure, you can build this feature entirely on the client side using JSON, JavaScript, and free hosting via GitHub Pages.

    This completes our 7-part series on building a fully interactive, searchable knowledge base using Jekyll collections, Liquid templates, and browser-based search engines. Whether you're managing technical docs or a product FAQ, this stack offers performance, flexibility, and sustainability—without needing a dynamic backend.

    Now your knowledge base isn’t just static—it’s smart, fast, and made to grow.

    enhancing interactive search with taxonomy filters

    Making Search Smarter with Taxonomies

    As your knowledge base grows, so does the challenge of helping users find exactly what they need. Even with full-text search, users can become overwhelmed by too many results. This is where taxonomy filters—like tags, categories, and custom metadata—come into play. By integrating these into your search UI, you provide a way to narrow down results interactively.

    Why Use Filters in Jekyll

    • Improve usability and speed of discovery
    • Reduce irrelevant search results
    • Group content around common themes or attributes

    In this guide, we'll build dynamic filters for your client-side search using JavaScript and structured front matter in your Jekyll collections.

    Structuring Metadata in Your Jekyll Collections

    Let’s assume your knowledge base has two content types: guides and FAQs. Each document includes taxonomies like category, tags, and product:

    
    ---
    title: "Resetting your password"
    category: "account"
    tags: ["security", "login"]
    product: "core"
    ---
    

    This metadata will be included in your JSON index for client-side filtering.

    Updating the JSON Search Index with Filters

    Let’s modify your search.json template to output taxonomies:

    
    [
      {% assign docs = site.guides | concat: site.faqs %}
      {% for doc in docs %}
      {
        "title": "{{ doc.title | escape }}",
        "url": "{{ doc.url | relative_url }}",
        "collection": "{{ doc.collection }}",
        "category": "{{ doc.category }}",
        "tags": {{ doc.tags | jsonify }},
        "product": "{{ doc.product }}",
        "content": {{ doc.content | strip_html | strip_newlines | jsonify }}
      }{% unless forloop.last %},{% endunless %}
      {% endfor %}
    ]
    

    Now your JavaScript will have access to this metadata and can group or filter documents accordingly.

    Building the Filter UI in HTML

    Create a basic interface to allow users to select filters:

    
    

    Optionally, create a tag cloud or checkbox list if you're dealing with tags:

    
    

    Filtering Search Results with JavaScript

    Once your index is loaded, combine text search and filter logic like this:

    
    function applyFilters(results, documents) {
      const category = document.getElementById("category-filter").value;
      const product = document.getElementById("product-filter").value;
      const tags = Array.from(document.querySelectorAll("#tags-filter input:checked")).map(el => el.value);
    
      return results.filter(result => {
        const doc = documents.find(d => d.url === result.ref);
        return (!category || doc.category === category) &&
               (!product || doc.product === product) &&
               (tags.length === 0 || tags.every(t => doc.tags.includes(t)));
      });
    }
    

    Call this filter logic after Lunr returns its raw matches:

    
    const filtered = applyFilters(results, documents);
    

    Refreshing the UI Dynamically

    Wire up your filter inputs to re-run the search and update the display:

    
    document.querySelectorAll("#filters select, #filters input").forEach(input => {
      input.addEventListener("change", performSearch);
    });
    

    This keeps the interface responsive and dynamic as users interact with it.

    Pre-Building Filter Metadata Lists

    If you want filters to be auto-generated rather than hardcoded, create a data file or use Liquid to gather all values at build time:

    
    {% assign all_categories = site.guides | map: "category" | uniq %}
    {% for cat in all_categories %}
      <option value="{{ cat }}">{{ cat | capitalize }}</option>
    {% endfor %}
    

    This can be done for tags, products, or any other field. Keep in mind that since it's static, the options reflect only what's present at build time.

    Performance Considerations

    • Don’t bloat your index with overly detailed metadata
    • Use data-* attributes on results to improve filtering speed
    • Use debounce on input listeners to reduce lag

    You can also consider paginating long results to improve UI responsiveness.

    Adding URL Parameters for Shareable Filters

    To make filtered states shareable, serialize them to the URL:

    
    const queryParams = new URLSearchParams();
    queryParams.set("category", category);
    queryParams.set("tags", tags.join(","));
    history.pushState({}, "", "?" + queryParams.toString());
    

    Then on load, parse the URL and apply those settings back to the UI and search engine.

    Combining Filters with Multilingual Indexes

    If you followed the multilingual setup from the previous article, filters still work—just apply them to the currently loaded index. You'll need to rebuild each index with taxonomies per language.

    Conclusion

    Faceted search via taxonomy filters is a powerful way to enhance your Jekyll-based knowledge base. By structuring metadata in front matter and combining it with client-side logic, you allow users to slice through your content faster and more precisely. This approach scales well, is SEO-friendly, and works entirely on GitHub Pages without server-side code.

    Next up, we’ll explore integrating autocomplete and smart suggestions to help users get instant answers as they type.

    building multilingual search indexes in jekyll

    Why Multilingual Search Matters

    If you're serving a global audience, your knowledge base should speak their language—literally. While Jekyll is not a CMS with built-in i18n (internationalization) support, it can be extended to support multiple languages through clever use of collections, data files, and routing. The challenge lies in making your search functionality multilingual too, so users can find answers in their preferred language.

    Common Use Cases

    • Product documentation in English, Spanish, and French
    • Localized support pages for international markets
    • Multilingual blog posts with different structures per region

    We'll walk through how to create separate search indexes per language and load them dynamically based on user preference or site structure.

    Setting Up Language-Specific Collections

    Let’s say you have two languages: English and Spanish. You’ll want mirrored collections for each:

    
    collections:
      guides_en:
        output: true
      guides_es:
        output: true
      faqs_en:
        output: true
      faqs_es:
        output: true
    

    This allows full separation of language-specific content while maintaining organizational clarity. Create directories like _guides_en and _guides_es for each collection.

    Creating Search Indexes Per Language

    Next, generate separate search_en.json and search_es.json files using custom templates. For English:

    
    
    ---
    layout: none
    ---
    
    [
      {% assign en_docs = site.guides_en | concat: site.faqs_en %}
      {% for doc in en_docs %}
      {
        "title": "{{ doc.title | escape }}",
        "url": "{{ doc.url | relative_url }}",
        "collection": "{{ doc.collection }}",
        "content": {{ doc.content | strip_html | strip_newlines | jsonify }}
      }{% unless forloop.last %},{% endunless %}
      {% endfor %}
    ]
    

    Repeat this structure for Spanish with search_es.json, substituting guides_es and faqs_es.

    Detecting Language Context

    You have several options to determine which language index to load:

    • URL path: Use /en/ or /es/ prefixes.
    • Subdomains: Like es.example.com
    • User preference: Detect via browser language or selector

    For example, with path-based routing:

    
    const lang = window.location.pathname.startsWith("/es/") ? "es" : "en";
    const indexUrl = `/search_${lang}.json`;
    

    This script will fetch the correct language index dynamically.

    Loading Language-Specific Index in JavaScript

    Here’s how your Lunr initialization might look with language switching support:

    
    let idx = null;
    let documents = [];
    
    fetch(indexUrl)
      .then(res => res.json())
      .then(data => {
        documents = data;
    
        idx = lunr(function () {
          this.ref("url");
          this.field("title");
          this.field("content");
    
          data.forEach(function (doc) {
            this.add(doc);
          }, this);
        });
      });
    
    document.getElementById("search-input").addEventListener("input", function () {
      const query = this.value;
      const results = idx.search(query);
      const output = document.getElementById("search-results");
      output.innerHTML = "";
    
      results.forEach(result => {
        const match = documents.find(doc => doc.url === result.ref);
        const item = document.createElement("li");
        item.innerHTML = `${match.title}`;
        output.appendChild(item);
      });
    });
    

    Optimizing Content for Language Indexes

    When creating multilingual content, consistency matters. Make sure that:

    • Each language version includes similar structure and metadata
    • You use the same layout files if applicable
    • Content in non-Latin scripts (e.g. Arabic, Japanese) is UTF-8 encoded

    Also, avoid mixing languages in the same index to keep search relevance high.

    Optional: Switch Index Dynamically with a Language Selector

    If you're allowing users to change the site language manually via a dropdown, you can also switch indexes accordingly:

    
    document.getElementById("language-select").addEventListener("change", function () {
      const selectedLang = this.value;
      window.location.href = `/${selectedLang}/`;
    });
    

    This navigates users to the correct path and triggers the correct index load automatically.

    Adding Localized UI Elements

    Make sure your search interface elements—placeholders, labels, and feedback messages—are also localized. Use _data/lang/en.yml and _data/lang/es.yml to manage language keys:

    
    # _data/lang/en.yml
    search_placeholder: "Search..."
    no_results: "No results found"
    
    # _data/lang/es.yml
    search_placeholder: "Buscar..."
    no_results: "No se encontraron resultados"
    

    Then inject the appropriate language strings in your templates using Liquid:

    
    
    

    SEO Considerations for Multilingual Search

    To keep your site Google-friendly across languages:

    • Use hreflang tags in your HTML head to define language versions
    • Ensure all versions are crawlable and linked from a central sitemap
    • Don’t rely solely on JavaScript to expose content

    Limitations of Lunr for i18n

    Lunr has limited support for languages with complex grammar or non-Latin alphabets. Consider the following:

    • Use language-specific stemmers or tokenizers (Lunr supports some via plugins)
    • Keep indexes small and focused per language
    • Fallback to fuzzy search or tag-based navigation for low-resource languages

    Conclusion

    Adding multilingual search to a Jekyll-based knowledge base isn’t as difficult as it sounds. With language-specific collections, separate search indexes, and smart JavaScript routing, you can deliver a seamless multilingual experience to your users. This method also scales well as you add more languages over time.

    Next, we’ll dive into using taxonomy filters—like tags and categories—to enhance your interactive search interface with more precise results.

    unifying multiple jekyll collections into one search index

    Why Use Multiple Collections in Jekyll

    Jekyll’s collection feature allows you to manage different types of content with separate configurations and rendering rules. For a knowledge base or documentation site, this is incredibly useful—for example:

    • Guides: Step-by-step tutorials or how-to articles.
    • References: API or configuration specifications.
    • FAQs: Short answers to recurring questions.
    • Use Cases: Real-world implementations and examples.

    Each collection can live in its own folder and be filtered, styled, or organized independently. But if you want a global search experience using Lunr, you’ll need to merge all collection content into a unified search index.

    Understanding the Challenge

    Lunr requires one flat JSON structure to create the index. Since Jekyll processes each collection separately, generating a single search.json file with multiple content types requires custom templating.

    In this tutorial, we’ll build a shared index from multiple Jekyll collections that includes metadata like collection name, title, and content, making it easier to filter or highlight specific content types in the search results.

    Configuring Multiple Collections in Jekyll

    First, define your collections in _config.yml:

    
    collections:
      guides:
        output: true
      references:
        output: true
      faqs:
        output: true
      usecases:
        output: true
    

    Each of these will be stored in folders prefixed with an underscore (e.g., _guides, _references), and Jekyll will treat them as individual collections.

    Generating a Unified search.json

    Create or modify your search.json to include entries from all collections:

    
    ---
    layout: none
    ---
    
    [
      {% assign all_docs = site.guides | concat: site.references | concat: site.faqs | concat: site.usecases %}
      {% for doc in all_docs %}
      {
        "title": "{{ doc.title | escape }}",
        "url": "{{ doc.url | relative_url }}",
        "collection": "{{ doc.collection }}",
        "content": {{ doc.content | strip_html | strip_newlines | jsonify }}
      }{% unless forloop.last %},{% endunless %}
      {% endfor %}
    ]
    

    This will create a single JSON array that merges content from all defined collections.

    Modifying Lunr Initialization

    Now, update your JavaScript search logic to recognize and display the collection type:

    
    let idx = null;
    let documents = [];
    
    fetch("/search.json")
      .then(res => res.json())
      .then(data => {
        documents = data;
    
        idx = lunr(function () {
          this.ref("url");
          this.field("title");
          this.field("content");
          this.field("collection");
    
          data.forEach(function (doc) {
            this.add(doc);
          }, this);
        });
      });
    
    document.getElementById("search-input").addEventListener("input", function () {
      const query = this.value;
      const results = idx.search(query);
      const output = document.getElementById("search-results");
      output.innerHTML = "";
    
      results.forEach(result => {
        const match = documents.find(doc => doc.url === result.ref);
        const item = document.createElement("li");
        item.innerHTML = `${match.title} (${match.collection})`;
        output.appendChild(item);
      });
    });
    

    This approach adds a visual cue about which collection the result belongs to, improving user orientation in mixed-content environments.

    Benefits of a Unified Search Index

    • One Search, All Content: Users don’t have to choose a section to search in.
    • Consistent UX: Results across different collections appear in a uniform layout.
    • Filter Ready: The collection field can be used for post-search filtering.

    Bonus: Filtering Results by Collection

    Want users to filter search results by category or collection?

    
    const selectedCollection = document.getElementById("filter").value;
    const filteredResults = results.filter(result => {
      const match = documents.find(doc => doc.url === result.ref);
      return match.collection === selectedCollection || selectedCollection === "all";
    });
    

    Add a dropdown in your HTML:

    
    <select id="filter">
      <option value="all">All</option>
      <option value="guides">Guides</option>
      <option value="references">References</option>
      <option value="faqs">FAQs</option>
      <option value="usecases">Use Cases</option>
    </select>
    

    Then apply the filter after a search query is executed.

    SEO and Accessibility Considerations

    Make sure all your content is still accessible via normal navigation for bots and users who prefer not to use search. Collections should be linked through menus or breadcrumbs. Also, ensure that your search input and results are keyboard accessible and labeled properly for screen readers.

    Scalability Tips

    • Break large indexes into separate files if content grows beyond a few hundred documents.
    • Use collection-specific JSON files and combine them with JavaScript at runtime if needed.
    • Keep the content field concise by limiting it to the excerpt or summary.

    Conclusion

    By unifying multiple Jekyll collections into one search index, you create a seamless experience that matches modern user expectations. Whether you're building a developer portal, product documentation, or internal knowledge base, a unified search makes the site faster to navigate and easier to maintain.

    In the next article, we’ll explore how to build multilingual search indexes for collections written in different languages—enabling your site to truly go global.

    client side search with lunr for jekyll sites

    Why Static Sites Need Client-Side Search

    One of the biggest limitations of static sites like those powered by Jekyll is the absence of dynamic search. Unlike CMS platforms that rely on databases, Jekyll serves pre-rendered HTML pages, which means traditional search engines or database queries aren't available. This creates a gap when users want to find specific content quickly—especially in large documentation or knowledge base sites.

    Enter Lunr.js—a lightweight, open-source JavaScript search engine that runs entirely in the browser. It indexes your content ahead of time and allows users to search through it on the client side without server overhead.

    What is Lunr and How Does It Work?

    Lunr.js creates an inverted index from your site’s content, enabling full-text search on static files. It's modeled loosely on the behavior of Solr or Elasticsearch but optimized for small-scale, client-side search.

    • It builds a search index from your content during site build time.
    • That index is saved as a JSON file.
    • A JavaScript search form loads the index and performs real-time lookup on the client.

    Advantages of Using Lunr for Jekyll

    • No server or backend needed—works on GitHub Pages.
    • Highly customizable indexing strategy.
    • Instant results without page reloads.
    • Lightweight and scalable for small-to-medium documentation projects.

    Generating the Search Index with Jekyll

    To start using Lunr, you’ll first need to generate a search index file that contains all the searchable content from your Jekyll site. Here's how to do it:

    Step 1: Create search.json

    Inside your root directory, create a file named search.json with the following content:

    
    ---
    layout: none
    ---
    [
      {% assign docs = site.docs | sort: "order" %}
      {% for doc in docs %}
      {
        "title": "{{ doc.title | escape }}",
        "url": "{{ doc.url | relative_url }}",
        "content": {{ doc.content | strip_html | strip_newlines | jsonify }}
      }{% unless forloop.last %},{% endunless %}
      {% endfor %}
    ]
    

    This renders a static JSON file at build time, containing the titles, URLs, and raw content of your documentation files.

    Adding Lunr.js to Your Site

    Download Lunr from its official site or include it via CDN:

    
    <script src="https://unpkg.com/lunr/lunr.js"></script>
    

    Next, create a JavaScript file (e.g., search.js) that will handle indexing and querying.

    Example: Basic Lunr Search Script

    
    let idx = null;
    let documents = [];
    
    fetch("/search.json")
      .then(response => response.json())
      .then(data => {
        documents = data;
    
        idx = lunr(function () {
          this.ref("url");
          this.field("title");
          this.field("content");
    
          data.forEach(function (doc) {
            this.add(doc);
          }, this);
        });
      });
    
    document.getElementById("search-input").addEventListener("input", function () {
      const query = this.value;
      const results = idx.search(query);
      const output = document.getElementById("search-results");
      output.innerHTML = "";
    
      results.forEach(result => {
        const match = documents.find(doc => doc.url === result.ref);
        const item = document.createElement("li");
        item.innerHTML = `${match.title}`;
        output.appendChild(item);
      });
    });
    

    Creating the Search UI

    In your layout or documentation index page, add the following markup:

    
    <input type="text" id="search-input" placeholder="Search documentation..." />
    <ul id="search-results"></ul>
    <script src="/assets/js/search.js"></script>
    

    This creates a simple input field that triggers instant search as users type.

    Improving the Search Experience

    While Lunr is powerful, its default behavior can be extended in many ways:

    Tokenizing and Boosting

    • Boost title matches over content using this.field("title", { boost: 10 }).
    • Ignore stopwords or use stemmers for fuzzy matching.

    Highlighting Matching Terms

    To enhance readability, highlight search terms in results using a simple regex-based highlighter or third-party libraries like mark.js.

    Filtering by Category

    If your documents include a category field in front matter, you can filter search results by category:

    
    const filtered = documents.filter(doc => doc.category === "getting-started");
    

    Deploying to GitHub Pages

    Lunr works perfectly with GitHub Pages since everything is static. Make sure:

    • search.json is committed and not ignored by .gitignore
    • You're using relative URLs or baseurl correctly if your site is in a subfolder

    No extra server configuration is needed. The client-side JS will handle everything at runtime.

    SEO Considerations

    Since search content is client-side, bots won’t index search.json meaningfully. However, your documentation pages themselves will still be fully indexable by search engines. Keep content accessible through both menus and internal linking.

    Limitations of Lunr

    While effective for many use cases, Lunr has some constraints:

    • It loads the entire search index into the browser, which can be heavy on large sites.
    • No fuzzy suggestions or typo correction out of the box.
    • It only works with plain text—no dynamic filters or faceted search.

    If your documentation grows significantly, consider migrating to Algolia DocSearch or building a server-backed solution.

    Conclusion

    Lunr.js is a simple, reliable way to add client-side search to any Jekyll site—especially for documentation and knowledge bases hosted on GitHub Pages. By generating a static JSON index and hooking it to a custom search UI, you empower your users to quickly find what they need without navigating through menus or page reloads.

    In the next article, we’ll explore how to combine multiple collections into one unified search index, handling multilingual content and versioning in a single search experience.

    jekyll collections for structured documentation


    Why Build a Knowledge Base with Jekyll

    A well-structured knowledge base helps customers and users find solutions quickly without support intervention. Using Jekyll, you can create a high-performance static knowledge base that’s easy to update, scalable, and doesn’t rely on server-side technologies. With collections and search integration, you can deliver an experience that rivals dynamic platforms like Help Scout or Zendesk.

    Jekyll Collections: The Foundation of Structured Content

    Jekyll collections allow you to define custom content types beyond blog posts and pages. This is ideal for a knowledge base where you may want to organize content by:

    • FAQs
    • Troubleshooting guides
    • Tutorials or how-to articles

    Add the following to your _config.yml:

    collections:
      kb:
        output: true
        permalink: /kb/:title/
    

    Organizing Your Knowledge Base Content

    Create a folder called _kb. Inside, add Markdown files for each knowledge base article:

    _kb/
      getting-started.md
      reset-password.md
      contact-support.md
    

    Each file should have front matter for categorization and search indexing:

    ---
    title: Resetting Your Password
    category: account
    tags: [password,login,troubleshooting]
    description: Learn how to reset your password if you've forgotten it.
    ---
    

    Displaying Articles by Category

    In a category page, you can loop through site.kb and filter by category:

    {% raw %}
    {% assign articles = site.kb | where: "category", "account" %}
    
    {% endraw %}
    

    Client-Side Search Integration

    To make your knowledge base searchable, integrate a JavaScript-based search like Lunr.js or FlexSearch. First, generate a JSON index:

    Step 1: Create search.json

    Add this file in your root directory:

    ---
    layout: null
    ---
    [
      {% raw %}{% for article in site.kb %}{% endraw %}
        {
          "title": {% raw %}"{{ article.title | escape }}",{% endraw %}
          "url": {% raw %}"{{ article.url }}",{% endraw %}
          "content": {% raw %}"{{ article.content | strip_html | strip_newlines | escape }}" {% endraw %}
        }{% raw %}{% if forloop.last == false %},{% endif %}
      {% endfor %}
    ]
    

    Step 2: Add Search UI

    Include a search bar and a results container:

    
    

      Step 3: Load Index and Filter

      Use JavaScript to load the JSON and filter results:

      
      

      Case Study: Technical Support KB for a SaaS Tool

      A software company built their knowledge base using Jekyll collections and Lunr.js search. Their setup included:

      • 500+ articles across 12 categories
      • Real-time search powered by Lunr.js with auto-complete
      • Separate search.json files for each language

      This allowed them to cut support tickets by 42% in the first 3 months. Updates are made via GitHub, with changes pushed through CI/CD to Netlify in seconds.

      Enhancing UX with Breadcrumbs and Tags

      To improve navigation, implement breadcrumbs using the page’s category and title:

      Knowledge Base > Account > Resetting Your Password

      Also, add tag-based filtering:

      {% raw %}
      

      Tags: {% for tag in page.tags %} {{ tag }} {% endfor %}

      {% endraw %}

      Deploying Your Knowledge Base

      Host your knowledge base on:

      • GitHub Pages
      • Netlify with preview builds
      • Cloudflare Pages for fast global delivery

      Maintaining and Scaling the System

      Keep things scalable by:

      • Using a consistent file naming convention
      • Versioning articles via Git history
      • Running a script to check for broken links across articles

      Conclusion

      Building a Jekyll-powered knowledge base provides full control over structure, performance, and UX. With collections, you gain powerful content modeling. Paired with JavaScript search and thoughtful navigation, you create a user-first experience that helps reduce support load and scales indefinitely.

      scalable knowledge base design with jekyll

      Why Scalability Matters in Knowledge Base Architecture

      When building a knowledge base with Jekyll, it’s tempting to focus solely on content and design. However, long-term success depends heavily on how scalable the architecture is. As the volume of articles increases, a poorly structured setup becomes unmanageable—both for users trying to find information and for contributors maintaining it.

      This article explores how to create a modular, structured, and future-proof content architecture using Jekyll collections, data files, and Liquid logic, specifically tailored for deployment on GitHub Pages.

      The Pitfalls of a Flat Structure

      Many beginner Jekyll setups use a flat folder system with all posts or pages dumped into one or two directories. While this may work for a dozen entries, scaling to hundreds or thousands of entries results in:

      • Slow build times
      • Messy navigation
      • Hard-to-manage categories or tags
      • Poor UX due to lack of organization

      The solution lies in embracing a more sophisticated file organization model, and Jekyll Collections are the backbone of that approach.

      Organizing Content with Jekyll Collections

      Collections allow you to group related content outside of the regular post and page structures. For example, you might have collections for:

      • tutorials/
      • faqs/
      • api-guides/
      • troubleshooting/

      To enable a collection in Jekyll, define it in _config.yml like this:

      
      collections:
        tutorials:
          output: true
        faqs:
          output: true
      

      This allows you to create markdown files inside folders like _tutorials or _faqs, and Jekyll will treat them as content objects. You gain powerful control over sorting, filtering, and layout.

      Example: Structuring a Modular Jekyll Knowledge Base

      Consider the following structure:

      
      /_tutorials/
        install-jekyll.md
        customize-theme.md
      
      _faqs/
        setup-errors.md
        deployment-issues.md
      
      _data/
        navigation.yml
        categories.yml
      

      This pattern makes it easy to:

      • Generate menus dynamically
      • Implement tag filtering
      • Maintain each section independently

      Configuring Front Matter for Discoverability

      Each document inside a collection should include structured front matter. A consistent schema improves filtering and search accuracy. Example front matter:

      
      ---
      title: "How to Install Jekyll"
      tags: [setup, installation]
      category: "tutorials"
      level: beginner
      ---
      

      Tags and categories help group content thematically. Additional fields like level or audience can be introduced to serve specialized navigation needs.

      Use Case: Multiple Audiences, One Base

      Suppose you have users at different technical levels. You could use metadata to segment content:

      • level: beginner
      • level: intermediate
      • level: advanced

      Then, build filtering tools using JavaScript or Liquid to display only relevant documents.

      Leveraging Layouts and Includes for Maintainability

      Each collection can have its own layout template. This keeps your design DRY and maintainable. For example:

      
      layouts/
        tutorial.html
        faq.html
      

      Then specify the layout in the document's front matter:

      
      layout: tutorial
      

      Inside each layout, you can use includes like:

      {% include breadcrumbs.html %}

      This approach makes it easy to update the design across hundreds of articles without editing them individually.

      Use Case: Updating Global Design

      Let’s say you decide to move from a single-column layout to a grid-based layout. Instead of editing 200+ pages, you simply modify a layout file. This is the power of modular architecture.

      Integrating Liquid Filters for Content Grouping

      Liquid filters can be used to dynamically generate lists of documents based on tags, categories, or other front matter fields. For instance:

      
      {% assign tutorials = site.tutorials | where: "level", "beginner" %}
      
      

      This logic can be used on sidebar panels, related posts, and tag navigation systems.

      Building for Future Growth

      Scalability means building with tomorrow in mind. Here are core principles to follow:

      Principle 1: One File, One Purpose

      Don’t mix multiple topics in one markdown file. Break content into small, focused pages to enhance reusability.

      Principle 2: Abstract Configuration

      Move all static content (menus, categories, config) into YAML files in _data. This makes them editable without touching your templates or logic.

      Principle 3: Document Relationships

      If articles reference each other, use front matter fields like related: ["id1", "id2"] and load them dynamically using Liquid.

      Conclusion

      Designing a scalable knowledge base using Jekyll collections is both an art and a science. By leveraging modular collections, structured front matter, and smart layouts, you set yourself up for a content hub that’s not only manageable today, but adaptable for years to come.

      In the next article, we’ll dive deeper into how to structure documentation using Jekyll Collections—including version control, content chunking, and hierarchical flows.