Posts

Showing posts with the label deep-linking

Multilingual Breadcrumb Navigation in Jekyll

Why Breadcrumb Navigation Matters

Breadcrumbs provide a visual map of where the user is within your site's content hierarchy. They're useful for long documentation, nested pages, and blog categories. For multilingual sites, breadcrumbs should reflect the language of the current page and the translated structure.

For example, instead of always showing:


Home > Guides > Installation

The Spanish version should show:


Inicio > Guías > Instalación

This not only improves user experience but also helps search engines understand your site structure in multiple languages.

Step 1: Define Page Hierarchies

Each page should define its parent(s) in the front matter. You can use a custom field like breadcrumbs with unique IDs:


breadcrumbs:
  - home
  - guides
  - installation

This list forms the trail from the homepage to the current page. These IDs will be used for translation and linking.

Step 2: Prepare Translations

Add a breadcrumbs section in each language’s translation data file:


# _data/translations/en.yml
breadcrumbs:
  home: "Home"
  guides: "Guides"
  installation: "Installation"

# _data/translations/es.yml
breadcrumbs:
  home: "Inicio"
  guides: "Guías"
  installation: "Instalación"

These keys match the breadcrumb IDs declared in each page’s front matter. This keeps your translations clean and maintainable.

Step 3: Create the Breadcrumb Include

Create an include file _includes/breadcrumbs.html:

{% raw %}
{% if page.breadcrumbs %}
  <nav class="breadcrumbs" aria-label="Breadcrumb">
    <ul>
      {% for crumb in page.breadcrumbs %}
        <li>
          <a href="{{ site.baseurl }}/{% if crumb != page.breadcrumbs.last %}{{ site.data.routes[page.lang][crumb] }}{% endif %}">
            {{ site.data.translations[page.lang].breadcrumbs[crumb] }}
          </a>
        </li>
      {% endfor %}
    </ul>
  </nav>
{% endif %}
{% endraw %}

This loop renders the breadcrumb items with language-specific labels and links. You’ll need a routing data file to define the URL paths.

Step 4: Add Route Mappings per Language

Create route mapping files like _data/routes/en.yml:


home: ""
guides: "guides"
installation: "guides/installation"

# _data/routes/es.yml
home: "es"
guides: "es/guias"
installation: "es/guias/instalacion"

This allows your breadcrumb links to correctly point to the translated version of each page.

Step 5: Include Breadcrumbs in Your Layout

Edit your layout file, e.g., _layouts/default.html, and include the breadcrumb:

{% raw %}
{% include breadcrumbs.html %}
{% endraw %}

Place it before the main content so users immediately know where they are.

Step 6: Add Structured Data for SEO

You can add schema.org markup to help search engines display breadcrumb paths in search results:

{% raw %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {% for crumb in page.breadcrumbs %}
      {
        "@type": "ListItem",
        "position": {{ forloop.index }},
        "name": "{{ site.data.translations[page.lang].breadcrumbs[crumb] }}",
        "item": "{{ site.url }}/{{ site.data.routes[page.lang][crumb] }}"
      }{% unless forloop.last %},{% endunless %}
    {% endfor %}
  ]
}
</script>
{% endraw %}

This enhances visibility in Google and helps with multilingual SEO.

Styling Your Breadcrumb

Basic breadcrumb styles:


.breadcrumbs ul {
  list-style: none;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
}
.breadcrumbs li + li::before {
  content: ">";
  padding: 0 0.5em;
}
.breadcrumbs a {
  text-decoration: none;
  color: #336699;
}

Use Case: Documentation Navigation

Imagine a multilingual API documentation with 100+ pages. Breadcrumbs give users instant context, and allow devs to link directly to relevant sections while maintaining language fidelity.

They also allow content editors to reorganize documentation by just changing breadcrumb sequences without hardcoding links.

Enhancing UX and SEO

  • Breadcrumbs reduce bounce rate by guiding users deeper into related content
  • They help users orient themselves in complex structures
  • Multilingual breadcrumbs allow better internationalization of your UX

Conclusion

With just data files and includes, you can implement flexible, multilingual breadcrumbs in Jekyll. They enhance navigation, boost search visibility, and make your content easier to explore.

In the next guide, we’ll discuss how to add search term highlighting for client-side search results to improve user orientation after navigation.

Multilingual Deep Linking with URL Hash

Why Deep Linking Matters in Multilingual Content

Deep linking allows users to link directly to specific sections of a page. In documentation, tutorials, and knowledge bases, this enhances shareability, SEO, and user navigation—especially when working with multilingual content.

For example, a user reading the Spanish version of a guide should be able to link directly to a section like "Instalación" and land on that exact heading, not its English counterpart. This behavior also encourages content reuse and seamless collaboration across language teams.

Challenge: Language-Dependent Anchors

The challenge is keeping section anchors consistent or mirrored across translations. Most Jekyll setups use the heading text (slugified) as the id, which changes by language. So the anchor #setup in English might become #instalacion in Spanish.

We’ll solve this by assigning custom, language-agnostic IDs to each section using data files or consistent keys.

Step 1: Use Language-Agnostic IDs

Instead of generating id attributes from translated text, we define them manually and translate only the visible label. For example:


{% raw %}
{% assign section_id = 'installation' %}
<h2 id="{{ section_id }}">{{ site.data.translations[page.lang].sections[section_id] }}</h2>
{% endraw %}

This way, no matter the language, the anchor stays the same: #installation.

Step 2: Update Translations

In your _data/translations/en.yml:


sections:
  installation: "Installation"
  usage: "Usage Guide"

And in _data/translations/es.yml:


sections:
  installation: "Instalación"
  usage: "Guía de uso"

This keeps visible labels localized while keeping anchor IDs universal.

Step 3: Auto Scroll to Hash on Page Load

To ensure the page scrolls to the correct section on load, you don’t need any extra JavaScript. The browser will handle it if the ID exists. But with fixed headers (e.g., sticky navbar), you might need an offset scroll adjustment:

{% raw %}

{% endraw %}

This adds a small delay to ensure layout has loaded before scrolling. Adjust 80 based on your header height.

Step 4: Add Link Icons for Sharing

You can append a link icon to each heading to copy the anchor:

{% raw %}
<h2 id="installation">
  {{ site.data.translations[page.lang].sections.installation }}
  <a href="#installation" class="anchor-link">#</a>
</h2>
{% endraw %}

Style the link in CSS:


.anchor-link {
  text-decoration: none;
  margin-left: 5px;
  opacity: 0.4;
}
.anchor-link:hover {
  opacity: 1;
}

Step 5: Share Anchored Links Across Languages

Since anchors are now universal, switching languages should preserve the hash when navigating. You can do this by appending the hash manually during language switch:

{% raw %}

{% endraw %}

This ensures that when a user switches from English to Spanish, they land on the same section (e.g., #installation) in the other language.

Use Cases

  • Linking to specific sections in support documentation
  • Referencing parts of multilingual blog posts
  • Cross-linking between tutorials across languages

Edge Case: Nonexistent Anchors

If a section is missing in one translation (e.g., due to incomplete work), the anchor will not scroll correctly. You can highlight this in your QA process or fallback by detecting such hashes and showing a warning:

{% raw %}
if (!document.getElementById(id)) {
  alert('Section not available in this language yet.');
}
{% endraw %}

Enhancing SEO and Accessibility

  • Anchored headings improve crawlability and indexability
  • They help screen reader users with better navigation
  • They support WCAG guidelines via aria landmarks

Conclusion

Implementing deep linking across languages significantly improves usability, consistency, and content sharing potential. By standardizing anchor IDs and syncing them with translated text, your Jekyll site becomes more accessible and globally usable.

In the next guide, we’ll explore how to create dynamic breadcrumb trails that also reflect the multilingual structure and user position within a document.

Multilingual Scroll Spy in Jekyll Posts

Improving UX with Scroll-Aware TOC

After building a multilingual table of contents (ToC) in the previous article, it's time to enhance usability further. A scroll spy highlights the current section in the navigation menu as the user scrolls, making it easier to track their reading position—especially in multilingual content where users might skim differently based on text length or structure.

We'll use the modern IntersectionObserver API for a performance-friendly implementation that avoids scroll event bottlenecks.

Understanding the Scroll Spy Behavior

The goal is to dynamically highlight the currently visible section in the ToC. This involves:

  • Detecting when a heading (h2, h3) enters the viewport
  • Finding the corresponding link in the ToC
  • Adding/removing an active class to style the link

Step 1: Update Your TOC Markup

In your _includes/toc.html, make sure the anchor tags have a consistent structure. Each should wrap a heading's ID:


    <li><a href="#section-id">Section Title</a></li>

No changes are needed here if you followed the previous guide.

Step 2: Add Scroll Spy JavaScript

Below the script that generates your ToC list, add this code:

{% raw %}

{% endraw %}

Step 3: Style the Active TOC Link

Add this to your CSS (e.g., in assets/css/main.css):


#toc-list a.active {
  font-weight: bold;
  color: #007acc;
  text-decoration: underline;
}

You can also add indicators like a border or icon if desired.

Step 4: Adapt to Multiple Languages

Because we previously structured our headings and ToC links based on the page language, this scroll spy works seamlessly across locales. The anchor structure doesn’t change; the labels are already localized via Liquid and data files.

Make sure all headings that appear in the ToC have an id attribute and visible text content in the appropriate language.

Performance Considerations

The IntersectionObserver is highly efficient because it avoids the costly scroll event. It's also widely supported by modern browsers. For legacy fallback (e.g., IE), you could include a polyfill, though this is optional for most projects today.

Testing Across Languages

  • Switch between English, Spanish, or other available translations
  • Scroll through the post and observe the ToC highlight update
  • Ensure ToC labels match the language set in front matter

Making Scroll Spy Reusable

Encapsulate the scroll spy logic in a separate include, like _includes/scroll-spy.html, and call it in your layout:

{% raw %}
{% include scroll-spy.html %}
{% endraw %}

This ensures DRY principles and centralized control over behavior.

Enhancing Accessibility

Add aria-current="true" to the active link for screen readers:


link.classList.toggle('active', match);
if (match) {
  link.setAttribute('aria-current', 'true');
} else {
  link.removeAttribute('aria-current');
}

Use Cases

  • Documentation sites with multiple headings
  • Academic articles with complex structures
  • Multilingual how-to guides

Next Steps

This scroll spy is a powerful addition to your multilingual Jekyll site. To make it even better, consider syncing the URL hash to reflect the active section for deep-linking support or generating dynamic breadcrumbs. We'll explore this in the next article.

Conclusion

Combining multilingual content with interactive UI like scroll spy improves both accessibility and user experience. By relying on native browser APIs and Liquid templating, we’ve created a future-proof system for global websites.

In the next article, we’ll build on this by making scroll position affect the browser’s URL hash and allow users to share section-specific links across languages.

Dynamic TOC for Multilingual Jekyll Posts

Why a Multilingual Table of Contents Matters

Longform content needs a navigable structure to help users skim and understand context. In multilingual websites, it's not enough to generate a generic ToC—you need to respect the language of the user, label sections accordingly, and ensure the experience remains consistent across locales.

In this tutorial, we’ll build a reusable, dynamic table of contents system for Jekyll posts that adapts based on language, using Liquid for static structure and JavaScript for client-side interaction.

Planning the Multilingual Navigation

We want to achieve the following:

  • Detect headings within the article content
  • Generate anchor links with readable text in the correct language
  • Enable smooth scrolling
  • Make it reusable across all posts

We'll separate content logic from layout logic using includes and data-driven design.

Step 1: Add Headings with ID Attributes

Make sure your headings have an `id` attribute for linking. If using Markdown, Jekyll automatically assigns slug-based IDs to headings. For manual HTML, you should write:


Introduction

Step 2: Create a ToC Include File

Create an include file named _includes/toc.html with the following content:

{% raw %}


{% endraw %}

This script collects all h2 and h3 elements and dynamically builds the list when the page is loaded.

Step 3: Add Language Support via Data Files

Create a file in _data/lang.yml with the following structure:


en:
  toc_title: "Table of Contents"
es:
  toc_title: "Índice de Contenidos"

Make sure your posts set the `lang` front matter:


lang: en

Step 4: Insert ToC into Layout

Edit your layout file (e.g. _layouts/post.html) and add the include where you want the ToC to appear:


{% raw %}{% include toc.html %}{% endraw %}

Step 5: Style the ToC

Add CSS to enhance readability:


#toc {
  background: #f9f9f9;
  padding: 1em;
  border: 1px solid #ddd;
  margin-bottom: 2em;
}
#toc h3 {
  margin-top: 0;
}
#toc-list {
  list-style: none;
  padding: 0;
}
#toc-list li {
  margin-bottom: 0.5em;
}

Optional: Smooth Scrolling

Add smooth scrolling behavior via CSS:


html {
  scroll-behavior: smooth;
}

Optional: Collapse ToC Sections

To support posts with many headings, consider enhancing the list with collapsible sections:

  • Wrap subheadings in nested `
      ` tags
    • Use JavaScript to toggle visibility

    Making the ToC Reusable Across Languages

    By using `page.lang` and the `lang.yml` data file, the ToC heading and structure become automatically translatable. You can extend this by adding localized tooltips or button labels as well.

    Benefits of This Approach

    • No third-party libraries required
    • Works entirely on the client side
    • Fully adaptable to any language
    • Reusable across all posts

    Use Cases and Examples

    • Documentation pages with technical walkthroughs
    • Longform blog articles with multiple sections
    • Tutorials that span many h2/h3 subsections

    Conclusion

    With just Liquid and a bit of JavaScript, you can provide multilingual-aware navigation within any Jekyll page. This boosts readability, improves UX for global audiences, and requires zero dependencies. In the next guide, we’ll explore how to dynamically highlight the current section as the user scrolls—a feature often referred to as “scroll spy.”

Multilingual Search with Lunr in Jekyll

Why Client-Side Multilingual Search Matters

Search is a critical feature of any knowledge base or blog. In multilingual setups, it's not enough to simply list content by tag or category—users want to search in their language. However, most search engines or plugins don't support client-side indexing and filtering for multilingual content out of the box.

This article shows you how to use Lunr.js to build a fast, client-side search feature that respects language boundaries in a Jekyll-based multilingual site.

How Lunr Works in Jekyll

Lunr is a JavaScript-based full-text search engine for the browser. In Jekyll, you typically build a search index during the build process, then load that JSON file on the frontend.

We’ll go further by generating multiple indexes—one for each language—and dynamically selecting the correct one based on the current language context.

Folder and Language Structure

Assume a structure like this:


├── _resources
│   ├── en
│   └── es
├── search
│   ├── index.html
│   ├── en.json
│   └── es.json

Each language-specific JSON file contains the index for its respective content.

Step 1: Add Front Matter Metadata

Ensure each post contains:


lang: en
title: Example Title
summary: Short excerpt of the post
tags: [jekyll,search]

Step 2: Create the Language-Aware Search Indexes

Add this code to your Jekyll site, for example in /search/en.json:

{% raw %}
[
  {% assign lang_posts = site.resources | where: "lang", "en" %}
  {% for post in lang_posts %}
  {
    "title": "{{ post.title | escape }}",
    "url": "{{ post.url | relative_url }}",
    "summary": "{{ post.summary | strip_html | escape }}",
    "content": {{ post.content | strip_html | strip_newlines | jsonify }},
    "tags": "{{ post.tags | join: ',' }}"
  }{% unless forloop.last %},{% endunless %}
  {% endfor %}
]
{% endraw %}

Repeat this process for es.json or other languages by changing the language filter.

Step 3: HTML Search Page Template

Create a generic /search/index.html page that reads the language and loads the correct JSON:



    Step 4: Styling Your Search Interface

    
    #searchBox {
      width: 100%;
      padding: 0.75em;
      font-size: 1rem;
      margin-bottom: 1em;
    }
    #searchResults {
      list-style: none;
      padding: 0;
    }
    #searchResults li {
      padding: 0.5em 0;
      border-bottom: 1px solid #ddd;
    }
    

    Step 5: Language Detection and SEO

    Make sure to add hreflang tags in your layout or head:

    
    
    
    

    Also, make sure the search links in the site point to the correct version:

    
    Search (EN)
    Buscar (ES)
    

    Advantages of This Setup

    • Works offline (static)
    • Respects language context
    • Does not rely on third-party services
    • Fast indexing with no page reload

    Limitations and Solutions

    • Large index files: Compress with gzip if using your own server
    • No fuzzy search: Consider adding a Lunr plugin for better tokenization
    • SEO indexing: Prevent search page from being indexed if it's content-light

    Future Improvements

    • Support partial-word matching
    • Highlight matched terms in preview
    • Paginate search results for longer content

    Conclusion

    Jekyll and Lunr make it possible to offer full-text, language-specific search for static sites hosted on GitHub Pages. By splitting indexes by language and loading the correct JSON file at runtime, you create a scalable and accessible search experience for multilingual audiences—all without backend complexity.

    Pada artikel selanjutnya, kita akan membahas bagaimana membuat dynamic table of contents multibahasa yang mendeteksi struktur heading dan menyesuaikan konteks bahasa secara otomatis.

    Localized Tag and Category Navigation in Jekyll

    Why Localized Navigation Matters

    When building a multilingual site, it’s common to translate content. But what about navigation structures like categories and tags? If users switch to a different language, the taxonomy must follow. Showing English tags on a Spanish page creates confusion and breaks consistency.

    In this guide, we’ll show how to create a tag-category navigation structure in Jekyll that automatically adapts based on the current language context. The solution works without JavaScript, external plugins, or third-party CMS—just plain Liquid, YAML, and data-driven design.

    Site Structure for Multilingual Content

    We’ll assume you have a language-aware directory structure:

    
    ├── _config.yml
    ├── _data
    │   └── navigation.yml
    ├── _resources
    │   ├── en
    │   │   └── optimizing-includes.md
    │   ├── es
    │   │   └── optimizacion-includes.md
    

    Each document contains language-specific lang, category, and tags metadata.

    ---
    title: Optimizing Includes
    lang: en
    category: performance
    tags: [includes,html]
    ---
    ---
    title: Optimizando Includes
    lang: es
    category: rendimiento
    tags: [includes,html]
    ---

    Step 1: Localize Tags and Categories in _data

    We create a centralized translation map:

    _data/navigation.yml
    
    en:
      categories:
        performance: Performance
        theming: Theming
      tags:
        includes: HTML Includes
        html: HTML
    
    es:
      categories:
        rendimiento: Rendimiento
        tematica: Temática
      tags:
        includes: HTML Includes
        html: HTML
    

    Step 2: Filter by Language in Your Template

    We assume a layout variable page.lang exists. This allows filtering:

    {% raw %}
    {% assign lang = page.lang %}
    {% assign localized_data = site.data.navigation[lang] %}
    {% assign lang_items = site.resources | where: "lang", lang %}
    {% assign categories = lang_items | map: "category" | uniq | sort_natural %}
    
    {% for cat in categories %}
      

    {{ localized_data.categories[cat] }}

    {% assign cat_items = lang_items | where: "category", cat %} {% assign tags = cat_items | map: "tags" | join: "," | split: "," | uniq | sort_natural %} {% for tag in tags %}

    {{ localized_data.tags[tag] }}

      {% for item in cat_items %} {% if item.tags contains tag %}
    • {{ item.title }}
    • {% endif %} {% endfor %}
    {% endfor %} {% endfor %} {% endraw %}

    Step 3: Language Switching UI (Optional)

    If your pages support language toggles, ensure URLs follow the structure:

    • /en/resources/optimizing-includes/
    • /es/resources/optimizacion-includes/

    Add a toggle at the top:

    
    
    

    Styling Suggestions

    
    .lang-switch {
      display: flex;
      gap: 1em;
      margin-bottom: 1.5em;
    }
    .lang-switch a {
      text-decoration: none;
      color: #007acc;
    }
    

    Key Advantages of This Approach

    • Language-specific navigation
    • No duplication of template code
    • Translation is centralized in one data file
    • Compatible with GitHub Pages, no plugin required

    Common Pitfalls to Avoid

    • Using translated slugs in URLs without fallback links
    • Hardcoding translated titles in front matter
    • Ignoring plural and singular forms in category mapping

    Bonus: Fallback to Default Language

    If a tag or category translation is missing, display the raw key:

    {% raw %}
    {{ localized_data.categories[cat] | default: cat }}
    {% endraw %}

    Testing Your Localized Navigation

    1. Ensure both en and es resources exist with correct tags/categories
    2. Verify the YAML in navigation.yml is free of indentation errors
    3. Open /en/tags/ and /es/tags/ pages and validate output

    Conclusion

    Multilingual sites need more than just translated posts—they need translated structure. With Liquid logic and a data-driven YAML strategy, Jekyll allows you to create fully localized tag and category navigation without heavy frameworks or backend systems.

    In the next article, we’ll explore building a custom multilingual search interface that respects language context while using client-side JavaScript and Lunr.js.