Sidebar Configuration
NDG supports flexible sidebar customization through the sidebar configuration
section, allowing you to control the appearance, ordering, and content of
navigation items in your generated documentation.
Configuration Structure
Sidebar behavior is configured through the [sidebar] section in your
ndg.toml file or via the equivalent JSON structure.
[sidebar]
numbered = false
number_special_files = false
ordering = "alphabetical"
[[sidebar.matches]]
# Pattern-based matching rules
[sidebar.toc]
# Per-page table of contents configuration
[sidebar.options]
# NixOS options-specific configuration
Top-Level Options
numbered
Controls whether sidebar items are numbered within each sibling group.
- Type: Boolean
- Default:
false
When enabled, items display as "1. Title", "2. Title", etc. Special files
(index.md, README.md) are excluded from numbering by default unless
number_special_files is enabled. Sub-chapters use hierarchical numbers such as
"2.1. Title" and "2.1.1. Title". Setting numbered = false removes the numbers
without changing the hierarchy.
Example:
[sidebar]
numbered = true
Result:
Getting Started
1. Installation
2. Configuration
3. Usage
number_special_files
Controls whether special files (index.md, README.md) are included in the
numbering sequence. Only has effect when numbered = true.
- Type: Boolean
- Default:
false
Example with number_special_files = false:
Getting Started # index.md - not numbered
1. Installation
2. Configuration
Example with number_special_files = true:
1. Getting Started # index.md - numbered
2. Installation
3. Configuration
ordering
Determines how sidebar items are sorted within each sibling group.
- Type: String
- Default:
"alphabetical" - Options:
"alphabetical"- Sort items alphabetically by title"filesystem"- Preserve filesystem order"custom"- Sort by thepositionfield from pattern matches
Page Table of Contents
The sidebar.toc section controls the generated per-page table of contents used
by both the sidebar Contents section and the desktop On this page navigation.
Use exclude entries to omit headings by exact title or regular expression.
[sidebar.toc]
[[sidebar.toc.exclude]]
exact = "Internal details"
[[sidebar.toc.exclude]]
regex = "^(Inputs|Type|Examples)$"
Exclusions only remove matching headings from the navigation; the headings and
their content remain in the rendered page. Exclusion entries use the same title
matcher semantics as sidebar.matches: exact and regex are both supported,
and both must match when specified together.
Exclusions do not affect heading anchors or duplicate-anchor validation. For
repeated section titles such as Inputs, Type, or Examples, configure
[anchor] on_duplicate = "deduplicate" to create unique IDs. Existing explicit
IDs retain their names whereas generated suffixes skip them. The TOC and search
index use the same deduplicated IDs as the rendered headings.
Pattern Matching
The matches array contains pattern-based rules for customizing specific
sidebar items. Rules are evaluated in order, and the first matching rule
wins.
Match Rule Structure
Each <a href="sidebar.matches.html" class="obsidian-link">sidebar.matches</a> entry supports:
Matching Fields
| Field | Type | Description |
|---|---|---|
path |
String | Exact path match (shorthand for path.exact) |
path.exact |
String | Exact path match (e.g., "getting-started.md") |
path.regex |
String | Regex pattern for path matching (e.g., "^api/.*\\.md$") |
title |
String | Exact title match (shorthand for title.exact) |
title.exact |
String | Exact title match (e.g., "Getting Started") |
title.regex |
String | Regex pattern for title matching (e.g., "^API.*") |
Action Fields
| Field | Type | Description |
|---|---|---|
new_title |
String | Custom title to display in the sidebar |
position |
Integer | Sibling sort key for ordering = "custom" |
parent |
String | Input-relative parent Markdown source path |
Matching Logic
- All specified matching conditions must be satisfied (AND logic)
- If both
pathandtitleare specified, both must match - If both
exactandregexare specified for the same field, both must match - Only the first matching rule is applied to each item
Shorthand Syntax
For exact matches, shorthand syntax is available:
# Shorthand (recommended for exact matches)
[[sidebar.matches]]
path = "installation.md"
position = 1
# Full nested syntax (required for regex)
[[sidebar.matches]]
path.regex = "^api/.*\\.md$"
position = 2
# Mixed styles allowed
[[sidebar.matches]]
path = "getting-started.md"
title.regex = ".*Guide.*"
new_title = "Setup Guide"
Examples
Basic Numbering
[sidebar]
numbered = true
Custom Ordering
[sidebar]
numbered = true
ordering = "custom"
[[sidebar.matches]]
path = "getting-started.md"
position = 1
[[sidebar.matches]]
path = "installation.md"
position = 2
[[sidebar.matches]]
path = "api-reference.md"
position = 3
Sub-chapters
Set parent on a page's matching rule to nest it under another page. Both
path and parent identify Markdown source paths relative to input_dir. They
do not identify page titles or generated HTML URLs, even when an included file
has a custom output path. Pages without a parent remain root items.
For example, create these source files with the titles shown below:
docs/index.md # Introduction
docs/getting-started.md # Getting Started
docs/installation.md # Installation
docs/usage.md # Usage
docs/configuration.md # Configuration
docs/configuration/session.md # Session Management
docs/configuration/deep/advanced.md # Advanced
docs/configuration/profiles.md # Profiles
With input_dir = "docs", configure their relationships as follows:
[sidebar]
numbered = true
ordering = "custom"
[[sidebar.matches]]
path = "getting-started.md"
position = 1
[[sidebar.matches]]
path = "installation.md"
position = 2
[[sidebar.matches]]
path = "usage.md"
position = 3
[[sidebar.matches]]
path = "configuration.md"
position = 4
[[sidebar.matches]]
path = "configuration/session.md"
parent = "configuration.md"
position = 1
[[sidebar.matches]]
path = "configuration/deep/advanced.md"
parent = "configuration/session.md"
position = 1
[[sidebar.matches]]
path = "configuration/profiles.md"
parent = "configuration.md"
position = 2
The sidebar renders clickable parent pages with indented sub-chapters:
Introduction
1. Getting Started
2. Installation
3. Usage
4. Configuration
4.1. Session Management
4.1.1. Advanced
4.2. Profiles
position is an integer sort key, not a displayed chapter number. Each sibling
group has its own numbering sequence. Use parent to define the hierarchy;
decimal positions such as 4.1 are invalid. The first matching rule controls
the page's parent, title, and position, so put specific rules before broad
rules.
Special root pages such as index.md still appear first. A special page with a
parent stays under that parent. Unless number_special_files = true, special
pages have no number and do not consume a sibling number. Their descendants
inherit the nearest numbered ancestor's prefix, or start at 1 beneath an
unnumbered root.
The parent must be a page that appears in navigation. Included fragments cannot
be parents unless they produce their own standalone page. NDG reports an error
for missing parents, self-parenting, and parent cycles. Explicit parents cannot
be combined with sidebar.group_by_dir = true, which groups pages by directory
instead of page relationships.
Custom Titles
[sidebar]
numbered = false
[[sidebar.matches]]
path = "getting-started.md"
new_title = "🚀 Quick Start"
[[sidebar.matches]]
path = "api-reference.md"
new_title = "📚 API Reference"
Regex Pattern Matching
[sidebar]
ordering = "custom"
# Match all API documentation files
[[sidebar.matches]]
path.regex = "^api/.*\\.md$"
new_title = "API Documentation"
position = 50
# Match files with "Release" in the title
[[sidebar.matches]]
title.regex = "^Release.*"
new_title = "What's New"
position = 999
Combined Conditions
[[sidebar.matches]]
path.regex = "^api/.*\\.md$"
title = "API Functions"
new_title = "API: Core Functions"
position = 50
This rule matches files that are both in the api/ directory AND have the exact
title "API Functions".
Options Sidebar
The sidebar.options section provides specialized configuration for NixOS
module options, controlling how options appear in the options table of contents.
Structure
[sidebar.options]
depth = 2
nested = false
nested_depth = 0
ordering = "alphabetical"
[[sidebar.options.matches]]
# Option-specific matching rules
Options Fields
depth
Controls the grouping depth for option categories.
- Type: Integer
- Default:
2
A depth of 2 groups options by their first two components (e.g.,
services.nginx groups all services.nginx.* options).
Example with depth = 2:
- services.nginx (contains nginx.enable, nginx.package, nginx.virtualHosts.*)
- programs.git (contains git.enable, git.package, git.config.*)
Example with depth = 3:
- services.nginx.virtualHosts (contains only virtualHosts.* options)
- services.nginx (contains nginx.enable, nginx.package)
nested
Controls whether child attributes inside each option category are rendered as a recursive tree.
- Type: Boolean
- Default:
false
When disabled, child options are shown as a flat list under each category. When enabled, common child prefixes become nested dropdowns.
# nested = false
rum.programs
├── alacritty.enable
├── alacritty.package
└── beets.enable
# nested = true
rum.programs
├── alacritty
│ ├── enable
│ └── package
└── beets
└── enable
Each dropdown badge counts its immediate rendered children. For example, the
rum.programs badge above is 2 because opening it shows alacritty and
beets; it does not count all descendant options.
collapse_singletons
When nested = true, controls whether categories with one child are rendered as
dropdowns.
- Type: Boolean
- Default:
false
By default, a category with no option of its own and exactly one child is
rendered as a direct link using the option's full name. Set this to true to
render that category as a one-item dropdown instead.
nested_depth
Limits how many child dropdown levels are created below each category.
- Type: Integer
- Default:
0
0 means unlimited nesting. A positive value collapses deeper suffixes into a
single leaf label.
ordering
Determines how options are sorted in the table of contents.
- Type: String
- Default:
"alphabetical" - Options:
"alphabetical","custom","filesystem"
alphabetical sorts option links and groups by name. custom sorts matched
position values first, then by name; a group's position is its lowest matched
member position. filesystem preserves the order from the input options data.
Direct option links are always rendered before collapsible groups; ordering
applies within each section.
Option Pattern Matching
Each <a href="sidebar.options.matches.html" class="obsidian-link">sidebar.options.matches</a> entry supports:
Matching Fields
| Field | Type | Description |
|---|---|---|
name |
String | Exact option name match (shorthand for name.exact) |
name.exact |
String | Exact option name (e.g., "services.nginx.enable") |
name.regex |
String | Regex pattern for option name (e.g., "^internal\\..*") |
Action Fields
| Field | Type | Description |
|---|---|---|
new_name |
String | Custom display name for the option or category |
depth |
Integer | Custom grouping depth for this specific option |
position |
Integer | Custom position (used when ordering = "custom") |
hidden |
Boolean | Hide this option from the TOC (default: false) |
Shorthand Syntax
# Shorthand for exact matches
[[sidebar.options.matches]]
name = "services.nginx.enable"
position = 1
# Full nested syntax for regex
[[sidebar.options.matches]]
name.regex = "^internal\\..*"
hidden = true
Options Examples
Hiding Internal Options
[sidebar.options]
depth = 2
# Hide all internal.* options
[[sidebar.options.matches]]
name.regex = "^internal\\..*"
hidden = true
# Hide module system internals
[[sidebar.options.matches]]
name = "_module.args"
hidden = true
Custom Display Names
[sidebar.options]
depth = 2
[[sidebar.options.matches]]
name = "programs.git"
new_name = "Git Configuration"
[[sidebar.options.matches]]
name = "services.nginx"
new_name = "NGINX Web Server"
Custom Ordering
[sidebar.options]
ordering = "custom"
depth = 2
# Prioritize important options
[[sidebar.options.matches]]
name = "networking.firewall"
new_name = "Firewall Settings"
position = 1
[[sidebar.options.matches]]
name = "services.openssh"
new_name = "SSH Server"
position = 2
# Group remaining services
[[sidebar.options.matches]]
name.regex = "^services\\..*"
position = 100
Per-Option Depth Override
[sidebar.options]
depth = 2
# Use deeper grouping for complex hierarchies
[[sidebar.options.matches]]
name.regex = "^services\\.nginx\\..*"
depth = 3
# Use shallower grouping for simple options
[[sidebar.options.matches]]
name.regex = "^programs\\..*"
depth = 1
Result:
# Default depth = 2:
- services.nginx (all nginx.* options)
# With depth = 3 override:
- services.nginx.virtualHosts (just virtualHosts.* options)
- services.nginx.upstreams (just upstreams.* options)
- services.nginx (remaining nginx.* options)
# With depth = 1 override:
- programs (all programs.* options together)
Multi-Page Options
The top-level [options.pages] section controls whether module options are
split across generated group pages. This is separate from [sidebar.options]:
page routing decides where an option lives, while sidebar options decide how
links are grouped.
[options.pages]
enabled = true
depth = 1
root = "options"
[[options.pages.matches]]
name.regex = "^foo\\.bar\\.baz(\\.|$)"
depth = 3
title = "Foo Bar Baz"
position = 10
With this configuration:
options.html option group index
options/foo.html owns foo.*
options/foo.bar.baz.html owns foo.bar.baz.*
Root-level options, such as enable, stay on options.html unless a page match
routes them elsewhere.
Combined Example
[sidebar.options]
depth = 2
ordering = "custom"
# Hide internals
[[sidebar.options.matches]]
name.regex = "^internal\\..*"
hidden = true
# Important options with custom names and positions
[[sidebar.options.matches]]
name = "networking.firewall"
new_name = "🔥 Firewall"
position = 1
[[sidebar.options.matches]]
name = "services.openssh"
new_name = "🔐 SSH Server"
position = 2
# Deeper grouping for nginx
[[sidebar.options.matches]]
name.regex = "^services\\.nginx\\..*"
new_name = "🌐 NGINX Web Server"
depth = 3
position = 10
# Group other services
[[sidebar.options.matches]]
name.regex = "^services\\..*"
position = 50
JSON Configuration
The sidebar configuration can be specified in JSON format with either shorthand or nested syntax:
Shorthand syntax:
{
"sidebar": {
"numbered": true,
"ordering": "custom",
"matches": [
{
"path": "getting-started.md",
"new_title": "🚀 Quick Start",
"position": 1
},
{
"path": "installation.md",
"new_title": "📦 Installation",
"position": 2
}
]
}
}
Nested syntax (required for regex):
{
"sidebar": {
"numbered": true,
"ordering": "custom",
"matches": [
{
"path": {
"exact": "getting-started.md"
},
"new_title": "🚀 Quick Start",
"position": 1
},
{
"path": {
"regex": "^api/.*\\.md$"
},
"position": 2
}
]
}
}
Options configuration:
{
"sidebar": {
"options": {
"depth": 2,
"ordering": "custom",
"matches": [
{
"name": {
"regex": "^internal\\..*"
},
"hidden": true
},
{
"name": "programs.git",
"new_name": "Git Configuration",
"position": 5
}
]
}
}
}