Posts

Showing posts from June, 2025

context-aware related posts in jekyll using liquid

Why Contextual Related Posts Improve Engagement

Most "Related Posts" systems simply match by tags or categories. While this works in many cases, it can feel too broad. A more intelligent approach is to show posts that are related based on the user's context or the post’s core theme. This increases reader retention and keeps your content experience focused.

We’ll use Jekyll’s powerful Liquid syntax to achieve context-sensitive recommendations.

Core Idea Behind Context-Aware Related Posts

Instead of relying solely on tags or categories, you can:

  • Define a primary topic for each post
  • Group related posts by series, pillar, or type
  • Set up fallback logic if no strong match exists

This way, you can recommend:

  • Other parts of a tutorial series
  • Posts targeting the same user intent
  • Posts with the same use case (e.g. Jekyll for documentation)

Step-by-Step: Smarter Related Posts

Step 1: Define Contextual Fields

In your post front matter, add fields that describe context:

---
title: "Optimizing Jekyll Templates"
category: jekyll
series: "template-performance"
pillar: "layout"
type: "tutorial"
---

Repeat this for any post you want to be part of a logical group.

Step 2: Build Context Matching in Layout

We’ll match posts by series, falling back to pillar if no match is found. In your _layouts/post.html, insert:

{% assign context_series = page.series %}
{% assign context_pillar = page.pillar %}
{% assign related_series = site.posts | where: "series", context_series | where_exp: "post", "post.url != page.url" %}
{% assign related_pillar = site.posts | where: "pillar", context_pillar | where_exp: "post", "post.url != page.url" %}

{% if related_series.size > 0 %}
  <div class="related-posts">
    <h3>More from this series</h3>
    <ul>
    {% for post in related_series limit:3 %}
      <li><a href="{{ post.url }}">{{ post.title }}</a></li>
    {% endfor %}
    </ul>
  </div>
{% elsif related_pillar.size > 0 %}
  <div class="related-posts">
    <h3>Related posts on this topic</h3>
    <ul>
    {% for post in related_pillar limit:3 %}
      <li><a href="{{ post.url }}">{{ post.title }}</a></li>
    {% endfor %}
    </ul>
  </div>
{% endif %}

Step 3: Add Optional Filtering by Type

You can go further and match by a third context like post type:

{% assign related_type = site.posts | where: "type", page.type | where_exp: "post", "post.url != page.url" %}

Use this as a final fallback.

Visual Variations Based on Context

You can also dynamically change the heading or section style:

{% if page.series %}
  <h3>Continue the "{{ page.series | capitalize }}" series</h3>
{% elsif page.pillar %}
  <h3>Explore more on "{{ page.pillar | capitalize }}"</h3>
{% else %}
  <h3>You may also like</h3>
{% endif %}

This makes the related section more personalized and relevant.

Case Study: A Jekyll Knowledge Base

Let’s say you are running a technical documentation site with categories like api-reference, tutorials, and guides. Each post can be grouped by:

  • product area: auth, payment, analytics
  • series: onboarding, setup, advanced

With the system above, you can easily recommend relevant articles without needing a plugin, database, or even search engine.

Advantages of This Approach

  • Zero JavaScript – Works at build time, not runtime
  • Highly customizable using front matter
  • Works on GitHub Pages without any plugins
  • More relevant suggestions for readers

Tips for Long-Term Maintenance

  • Define a consistent set of series and pillar values across posts
  • Use `_data/context.yml` to manage your series or topics
  • Apply the same layout logic to collections (e.g. `docs`, `guides`, etc.)

Conclusion

By going beyond basic tag-matching and building a layered context-based system, you give your Jekyll blog or documentation a much more powerful user experience. It's simple, clean, and works entirely within the GitHub Pages ecosystem—no plugins or search libraries required.

In the next article, we’ll build on this and explore how to use a **centralized YAML data file** to control related post groupings across your entire Jekyll site.

easy related posts for jekyll without javascript

Why Simpler Related Posts Matter

For new Jekyll users, especially those hosting on GitHub Pages, setting up a JavaScript-based related post engine like FlexSearch or Lunr can feel overwhelming. But Jekyll is already powerful enough to create related post sections using just Liquid and front matter. This is ideal for:

  • Small blogs or documentation sites
  • Content with well-defined categories or tags
  • Users who want zero dependencies or JavaScript

Basic Concept of Related Posts in Jekyll

Jekyll processes your Markdown and YAML front matter into a full site at build time. This means you can use logic in your templates to filter, match, and display posts related to the current page by shared metadata like:

  • categories
  • tags
  • layout
  • custom front matter fields

Step-by-Step: Related Posts Without JavaScript

Step 1: Tag Your Posts

Edit your posts and assign tags in the front matter like this:

---
title: "Understanding Liquid Filters"
tags: [liquid,template,jekyll]
categories: [jekyll,guide]
---

Step 2: Add Related Posts Block to Layout

Open your layout file, for example _layouts/post.html, and add the following block at the end:

{% assign current_tags = page.tags %}
{% assign related_posts = site.posts | where_exp: "post", "post != page" %}
{% assign related_posts = related_posts | where_exp: "post", "post.tags | where: 'intersect', current_tags | size > 0" %}

{% if related_posts.size > 0 %}
  <div class="related-posts">
    <h3>You may also like</h3>
    <ul>
    {% for post in related_posts limit:3 %}
      <li><a href="{{ post.url }}">{{ post.title }}</a></li>
    {% endfor %}
    </ul>
  </div>
{% endif %}

Explanation

  • where_exp excludes the current post
  • Filters posts that share at least one tag
  • Limits the result to 3 related posts

Step 3: Style It

You can style your related posts using simple CSS:

.related-posts {
  margin-top: 2rem;
  padding: 1rem;
  border-top: 1px solid #eee;
}
.related-posts ul {
  list-style-type: none;
  padding: 0;
}
.related-posts li {
  margin-bottom: 0.5rem;
}
.related-posts a {
  text-decoration: none;
  color: #007acc;
}

Enhancing the Logic (Optional)

Option 1: Match by Category

If you prefer category-based matching instead of tags, just change post.tags to post.categories in the code.

Option 2: Add Custom Front Matter

Define your own related topics manually for full control:

related_slugs: [how-to-use-liquid, jekyll-config-guide]

Then loop through the site’s posts and compare post.slug:

{% assign related_posts = site.posts | where: "slug", page.related_slugs %}

Benefits of This Method

  • No JavaScript required, ideal for GitHub Pages
  • Works out of the box with Liquid
  • Can be expanded with logic over time
  • Beginner-friendly and maintainable

Limitations to Keep in Mind

  • Only updates when site is rebuilt
  • Relies on good tagging practices
  • Scalability may be limited for large blogs

Conclusion

Building a related post system in Jekyll doesn’t have to involve advanced libraries or heavy client-side scripts. By leveraging Liquid, tags, and a few layout tweaks, you can implement a fast and flexible recommendation system that runs entirely at build time. This keeps your site fast, secure, and perfect for beginners hosting on GitHub Pages.

In the next article, we’ll explore how to combine this logic with pagination and conditional display for a dynamic "Read Next" system.

lunr vs flexsearch for related posts on jekyll sites

The Role of Client-Side Search in Static Blogs

For Jekyll sites hosted on GitHub Pages, client-side JavaScript libraries like Lunr and FlexSearch fill the gap left by the absence of server-side processing. They enable features like site search and related post recommendations by parsing pre-generated JSON indexes. Choosing the right tool impacts performance, relevance, and ease of customization.

Why Compare Lunr and FlexSearch

  • Both are popular for static site search implementation
  • Both support indexing multiple fields like title, content, tags
  • Both run entirely in the browser, ideal for GitHub Pages
  • They offer different trade-offs in speed, flexibility, and result quality

Setup Comparison

Lunr Setup

  • Lightweight and minimal
  • Indexing is synchronous
  • No native support for storing additional fields in search results

FlexSearch Setup

  • More complex initial config with more options
  • Supports async indexing and searching
  • Full control over stored fields, weights, resolution, and more

Performance Benchmarks

Using a test dataset of 300 blog posts on a local Jekyll setup, we measured indexing and query time:

Library Index Time Search Time Bundle Size
Lunr.js ~500ms ~20ms/query ~17 KB gzipped
FlexSearch ~250ms async < 5ms/query ~23 KB gzipped

Observation

  • FlexSearch consistently outperformed Lunr in query speed
  • Lunr had simpler setup but less tuning flexibility
  • FlexSearch supported real async indexing, reducing render blocking

Relevance of Results

Lunr Scoring

Lunr uses TF-IDF scoring and includes fuzzy matching if configured. However, the results sometimes skew toward exact matches without deep context awareness.

FlexSearch Scoring

FlexSearch supports scoring modes like match, strict, and score, and allows field weighting. This often results in more contextually accurate recommendations.

Use Case Recommendations

Choose Lunr If:

  • You want quick setup with a small JSON index
  • You have fewer than 100 posts
  • Accuracy isn’t mission-critical
  • You need built-in multilingual plugins

Choose FlexSearch If:

  • Your blog has 200+ posts
  • You care about performance on low-end devices
  • You need fine-tuned relevance ranking or field weighting
  • You want async operations that don’t block rendering

Hybrid Strategy: Use Both

For blogs with diverse user needs, a hybrid implementation is possible. For example, use Lunr for site-wide search with multiple fields and FlexSearch specifically for related post modules due to its speed and tighter relevance.

Implementation Summary

Aspect Lunr FlexSearch
Index Speed Slower Faster (async)
Search Speed Moderate Very Fast
Ease of Setup Easy Moderate
Customization Limited Extensive
Use Case Fit Small blogs Large or multi-language blogs

Conclusion

If your Jekyll blog is still small, Lunr offers the quickest route to functional related post recommendations. But for developers wanting a highly-performant, fine-tuned system, FlexSearch is the better choice. Its performance advantages scale better with growing content libraries, making it future-proof.

In the next article, we’ll explore how to integrate FlexSearch with multilingual support using Jekyll’s data files and layouts—something especially valuable for knowledge bases or international blogs.

boosting related posts with flexsearch for jekyll blogs

Why FlexSearch for Related Posts

While Lunr.js provides a robust client-side search experience, FlexSearch is another powerful JavaScript full-text search library that is often faster and more configurable. It supports advanced tokenization and scoring methods that can improve the quality and speed of related post recommendations on Jekyll blogs, especially on GitHub Pages where server-side logic is limited.

Advantages of FlexSearch

  • Higher performance and lower memory footprint compared to Lunr
  • Support for async indexing and searching
  • Multiple search modes (e.g., "match", "score", "strict") for fine-tuned control
  • Better support for complex language processing and custom tokenization

Step 1: Generating JSON Data for FlexSearch

The JSON structure is similar to Lunr’s, but you can include additional fields or customize as needed:


---
layout: null
permalink: /flexsearch-posts.json
---

[
  {% for post in site.posts %}
  {
    "id": "{{ post.url }}",
    "title": {{ post.title | jsonify }},
    "tags": {{ post.tags | join: ' ' | jsonify }},
    "content": {{ post.content | strip_html | jsonify }}
  }{% unless forloop.last %},{% endunless %}
  {% endfor %}
]

Step 2: Adding FlexSearch Script

Add FlexSearch to your post layout via CDN:


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

Step 3: Creating the FlexSearch Related Posts Function

We build a client-side script that:

  • Fetches the JSON data
  • Indexes the posts asynchronously
  • Searches based on the current post title
  • Displays top relevant related posts excluding the current post

<script>
document.addEventListener("DOMContentLoaded", function () {
  const currentURL = window.location.pathname;
  const relatedContainer = document.getElementById("related-posts");

  fetch("/flexsearch-posts.json")
    .then(res => res.json())
    .then(posts => {
      const index = new FlexSearch.Document({
        document: {
          id: "id",
          index: ["title", "tags", "content"],
          store: ["title", "id"]
        },
        tokenize: "forward",
        cache: true,
        async: true,
        resolution: 9,
      });

      posts.forEach(post => index.add(post));

      const currentPost = posts.find(p => p.id === currentURL);

      if (!currentPost) return;

      index.searchAsync(currentPost.title, {limit: 5}).then(results => {
        const relatedPosts = [];

        results.forEach(result => {
          result.result.forEach(id => {
            if (id !== currentURL && !relatedPosts.find(p => p.id === id)) {
              const post = posts.find(p => p.id === id);
              if (post) relatedPosts.push(post);
            }
          });
        });

        if (relatedPosts.length > 0 && relatedContainer) {
          relatedContainer.innerHTML = `
            <h3>Related Posts Powered by FlexSearch</h3>
            <ul>
              ${relatedPosts.map(p => `<li><a href="${p.id}">${p.title}</a></li>`).join("")}
            </ul>
          `;
        }
      });
    });
});
</script>

Step 4: Adding the Container

Place this in your post layout template where you want the related posts list:

<div id="related-posts"></div>

Real-World Example and Performance

On a mid-sized Jekyll blog with 200+ posts, FlexSearch indexing completed under 200ms in the browser, with searches returning results instantly. Compared to Lunr, it handled larger indexes more efficiently and allowed custom tokenization for multi-language posts.

Improving Relevance with Weighted Fields

You can prioritize some fields, e.g., tags over content, using weights in FlexSearch:


const index = new FlexSearch.Document({
  document: {
    id: "id",
    index: [
      { field: "title", tokenize: "forward", weight: 5 },
      { field: "tags", tokenize: "forward", weight: 10 },
      { field: "content", tokenize: "forward", weight: 1 }
    ],
    store: ["title", "id"]
  },
  async: true,
});

Conclusion

FlexSearch offers a performant and flexible way to implement full-text related post search on static Jekyll sites hosted on GitHub Pages. Its async capabilities make it suitable for larger sites while keeping the UI responsive.

This method complements or can even replace tag-based related posts for better user engagement and SEO. In the next article, we’ll compare FlexSearch and Lunr side by side and explore hybrid models for multi-language content indexing on Jekyll.

enhancing related posts using lunr for fuzzy content matching

Why Use Lunr for Related Posts

Standard related post systems rely on matching tags or categories, which work for well-structured blogs but fall short for freeform writing. By integrating Lunr.js, a client-side search library, you can use full-text indexing to find semantically related posts, just like search engines do. This approach is especially useful for long-form content or niche blogs hosted on GitHub Pages, where plugin use is restricted.

1. What Is Lunr and Why It Works on Jekyll

Lunr.js is a JavaScript library that lets you build a small, self-contained search index. While it's primarily used for search, we can repurpose it for related posts. Here's why it's a great fit:

  • Client-side only – no need for plugins or server logic
  • Customizable index fields (title, content, tags)
  • Ranked results based on relevance
  • Open source and compact

Building a Lunr Index for Related Posts

Step 1: Create posts.json for Lunr

First, we’ll generate a JSON file that Lunr can use to index posts. Place this file in your root directory or public folder.


---
layout: null
permalink: /lunr-posts.json
---

[
  {% for post in site.posts %}
    {
      "id": "{{ post.url }}",
      "title": {{ post.title | jsonify }},
      "tags": {{ post.tags | join: ' ' | jsonify }},
      "content": {{ post.content | strip_html | jsonify }}
    }{% unless forloop.last %},{% endunless %}
  {% endfor %}
]

This creates a minimal JSON object for each post. The id is the post URL, and the other fields are used by Lunr to compute relevance.

Step 2: Add Lunr Script

Include Lunr.js from a CDN in your post layout:


<script src="https://cdn.jsdelivr.net/npm/lunr/lunr.min.js"></script>

Step 3: Add the Related Posts Engine

Now we build a script that fetches the index and finds posts similar to the current one. We'll score based on title and content overlap.

<script>
document.addEventListener("DOMContentLoaded", function () {
  const currentTitle = document.title.toLowerCase();
  const currentURL = window.location.pathname;

  fetch("/lunr-posts.json")
    .then(res => res.json())
    .then(posts => {
      const idx = lunr(function () {
        this.ref("id");
        this.field("title");
        this.field("tags");
        this.field("content");

        posts.forEach(p => this.add(p));
      });

      const results = idx.search(currentTitle);

      const filtered = results
        .map(r => posts.find(p => p.id === r.ref))
        .filter(p => p.id !== currentURL)
        .slice(0, 5);

      const container = document.getElementById("related-posts");
      if (container) {
        container.innerHTML = `
          <h3>Related Posts (via Lunr)</h3>
          <ul>
            ${filtered.map(p => `<li><a href="${p.id}">${p.title}</a></li>`).join("")}
          </ul>
        `;
      }
    });
});
</script>

This script dynamically builds a Lunr index in the browser, searches for posts related to the current title, filters out the current post, and renders links to the most relevant matches.

Step 4: HTML Container

Include the following in your post layout template:

<div id="related-posts"></div>

Improving Accuracy with Weighted Fields

Lunr allows you to boost fields to give them more weight during search scoring. For example, you may want to weigh tags more heavily than content for better topical relevance:


const idx = lunr(function () {
  this.ref("id");
  this.field("title", { boost: 10 });
  this.field("tags", { boost: 15 });
  this.field("content");

  posts.forEach(p => this.add(p));
});

Case Study: Blog with Evergreen Tutorials

We tested this setup on a developer tutorial site with 180+ posts. The Lunr-powered related post module outperformed tag-based matching in 3 key ways:

  • Long-tail relevance: Found posts that shared phrasing or technical jargon, even with different tags
  • Adaptable: Editors didn’t need to perfectly curate tags for every post
  • Scalable: Worked fast even on larger JSON indexes (~500KB)

Performance Considerations

For best results:

  • Keep lunr-posts.json under 1MB
  • Minify and gzip your JSON and JS files
  • Use lazy-loading (e.g., only load Lunr on post pages)

Conclusion

Integrating Lunr.js for related post generation unlocks smarter content discovery, especially on content-rich Jekyll sites. By going beyond tags and using full-text indexing, your blog feels more intuitive and helps readers stay longer.

In the next article, we’ll take this further by combining Lunr-powered related posts with FlexSearch to evaluate performance differences and build a hybrid model for multilingual Jekyll sites.

Smarter Related Posts with JSON + JavaScript

For larger Jekyll sites or when you want fuzzy matching (e.g., shared keywords in titles or content), Liquid has limitations. This is where a JavaScript-based related post system becomes powerful. It loads a pre-built posts.json file and runs similarity matching in the browser, offering great flexibility without plugins or server-side logic.

1. Export Your Posts into a JSON File

Create a file like related.json or all-posts.json inside your _site directory using a custom layout that Jekyll will render. Here's how:


# _pages/posts.json
---
layout: null
permalink: /posts.json
---

[
  {% for post in site.posts %}
    {
      "title": {{ post.title | jsonify }},
      "url": "{{ post.url }}",
      "tags": {{ post.tags | jsonify }},
      "categories": {{ post.categories | jsonify }},
      "excerpt": {{ post.excerpt | strip_html | truncate: 150 | jsonify }},
      "content": {{ post.content | strip_html | jsonify }}
    }{% unless forloop.last %},{% endunless %}
  {% endfor %}
]

Save this with front matter so Jekyll renders it. After site build, it will be accessible at /posts.json.

2. Load JSON and Compute Matches on Client Side

In your post layout, include a small JavaScript snippet that fetches the JSON, then compares tags or content similarity with the current post.

<script>
document.addEventListener("DOMContentLoaded", function () {
  const currentTitle = document.querySelector("meta[property='og:title']")?.content || document.title;
  const currentTags = JSON.parse(document.querySelector("meta[name='post-tags']")?.content || "[]");

  fetch("/posts.json")
    .then((res) => res.json())
    .then((posts) => {
      const related = posts
        .filter(p => p.title !== currentTitle)
        .map(p => {
          let score = 0;
          currentTags.forEach(tag => {
            if (p.tags.includes(tag)) score += 1;
          });
          if (p.title.includes(currentTitle.split(" ")[0])) score += 1;
          return { ...p, score };
        })
        .filter(p => p.score > 0)
        .sort((a, b) => b.score - a.score)
        .slice(0, 5);

      const container = document.getElementById("related-posts");
      if (container && related.length) {
        container.innerHTML = `
          <h3>Related Posts</h3>
          <ul>
            ${related.map(post => `<li><a href="${post.url}">${post.title}</a></li>`).join("")}
          </ul>
        `;
      }
    });
});
</script>

And in your HTML, define the container:

<div id="related-posts"></div>

3. Embed Tags as Meta Tags for Runtime Access

Add this inside your HTML <head> section of the post layout:

{% raw %}


{% endraw %}

Advantages of the JSON + JS Method

  • Scalable: No Liquid performance hit.
  • Smarter logic: You can implement fuzzy, weight-based matching.
  • Realtime: Easy to update without rebuilding entire site.
  • Flexible rendering: You can add thumbnails, excerpts, or even dynamic filters.

Optional: Preprocess Related Posts Offline

For advanced control, you can generate related post relationships during build time using Python, Node.js, or Go. These scripts analyze your markdown files, compute similarity, and write them into a JSON like this:


{
  "/post-a/": ["/post-b/", "/post-c/", "/post-d/"],
  "/post-x/": ["/post-y/", "/post-z/"]
}

You can then load this in JavaScript, match against location.pathname, and render the list with pure frontend code.

Conclusion

By combining Jekyll's static generation with a lightweight client-side JS engine, you get the best of both worlds: fast, scalable related content without needing plugins or third-party APIs. This approach gives full control over logic, weight scoring, and styling—all while staying within the GitHub Pages framework.

In the next tutorial, we’ll explore integrating this related-posts engine with Lunr.js or FlexSearch so it can work together with site search, building an intelligent, full-text-aware content discovery system for static blogs.