automated navigation menus from data files
Why Manual Navigation Is a Problem
As your documentation grows in scale and complexity, manually updating navigation menus becomes error-prone and time-consuming. Every time you add a new page or rename a section, you must update multiple navigation structures, often across several files or versions. This redundancy is a breeding ground for inconsistency.
To solve this, Jekyll allows the use of YAML data files to drive content generation dynamically. This enables you to define navigation structures once and reuse them across multiple versions, templates, or layouts.
Setting Up Data-Driven Navigation
Step 1: Create a Data File
Create a new file called _data/navigation.yml in your Jekyll root. Here's a basic structure:
v1:
- title: Getting Started
url: /v1/getting-started/
- title: API Reference
url: /v1/api/
v2:
- title: Introduction
url: /v2/introduction/
- title: Usage
url: /v2/usage/
- title: API Reference
url: /v2/api/
This organizes navigation by version. Each version has an array of pages, each with a title and url.
Step 2: Render Menus Dynamically in Layouts
In your default.html or sidebar.html layout, add the following Liquid logic to pull navigation dynamically based on the current page’s version:
{% assign current_version = page.version %}
{% assign menu = site.data.navigation[current_version] %}
<nav class="sidebar-nav">
<ul>
{% for item in menu %}
<li><a href="{{ item.url }}">{{ item.title }}</a></li>
{% endfor %}
</ul>
</nav>
This approach ensures each version displays its relevant navigation without having to manually hard-code anything.
Supporting Nested Navigation
To support deeper structures (like categories with children), modify your YAML file like this:
v2:
- title: Introduction
url: /v2/introduction/
- title: Guides
children:
- title: Installation
url: /v2/guides/installation/
- title: Configuration
url: /v2/guides/configuration/
- title: API
url: /v2/api/
Then render it with recursive Liquid logic:
{% assign menu = site.data.navigation[page.version] %}
<ul>
{% for item in menu %}
<li>
<a href="{{ item.url | default: '#' }}">{{ item.title }}</a>
{% if item.children %}
<ul>
{% for child in item.children %}
<li><a href="{{ child.url }}">{{ child.title }}</a></li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
This method can handle multiple layers of nesting, such as sections and subsections. You can also add icons or custom attributes for advanced rendering.
Using Includes for Clean Code
To avoid cluttering your layout, move the navigation logic to an include:
_includes/sidebar.html
{% assign menu = site.data.navigation[page.version] %}
<ul>
{% for item in menu %}
<li>
<a href="{{ item.url | default: '#' }}">{{ item.title }}</a>
{% if item.children %}
<ul>
{% for child in item.children %}
<li><a href="{{ child.url }}">{{ child.title }}</a></li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
Then call it in your layout with:
{% include sidebar.html %}
Making the Navigation Responsive to Active Page
You can highlight the current page in your navigation by comparing URLs:
{% if item.url == page.url %} class="active" {% endif %}
Apply it inside your anchor tag to style the current page link.
Version-Independent Navigation Structures
If your documentation shares structure across versions, you can avoid repeating navigation trees by abstracting common parts into a base YAML file like _data/nav_common.yml. Then, use a version-specific override if needed.
Example:
{% assign menu = site.data.nav_override[page.version] | default: site.data.nav_common %}
Case Study: Combining Search, Versions, and Navigation
Imagine you're running documentation for a multi-version SaaS API. Here's how all components integrate:
- Search: You index all versions using a build-time script, injecting
versionin each JSON entry. - Navigation: You define shared structure in
nav_common.ymland override where necessary. - Page Metadata: Each Markdown file includes
versionandslugfront matter for dynamic linking.
This results in a fully dynamic, consistent, and scalable static documentation system.
Benefits of Data-Driven Menus
- Consistency: All versions follow the same structure unless specifically overridden
- Maintainability: Updating navigation requires only one file edit
- Automation: You can generate or validate YAML with CI pipelines
Going Further
To push automation further, consider using YAML anchors for reuse within files, or even generating YAML programmatically from directory structures during build time with custom scripts.
Conclusion
Using data files to manage Jekyll navigation allows you to maintain consistency, reduce errors, and adapt quickly to structural changes—especially across versioned documentation. This approach is highly scalable and aligns well with static site philosophies like those behind GitHub Pages.
Next, we’ll explore how to integrate client-side search (like Lunr.js) into your documentation site, combining structured content with dynamic discovery to create a complete documentation platform.
