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 the position field 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 path and title are specified, both must match
  • If both exact and regex are 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
        }
      ]
    }
  }
}