## Item Ordering

Pages within a category are sorted by the `sidebar_position` frontmatter field. Lower values appear first.

Pages without `sidebar_position` default to `999` and appear at the end of the category, sorted alphabetically by filename.

```mdx
---
title: My Page
sidebar_position: 3
---
```

[Dynamic content unavailable in text export]

Always set `sidebar_position` explicitly to control page order. Without it, pages are sorted alphabetically, which may not match your intended reading order.



## Category Ordering

Categories in the sidebar are ordered by the `sidebar_position` of their **index page** (`index.mdx`). If a category has no index page or no `sidebar_position`, it falls back to alphabetical ordering.

For example, to make "Getting Started" appear first and "Guides" second:

```
getting-started/index.mdx  → sidebar_position: 0
guides/index.mdx            → sidebar_position: 1
reference/index.mdx         → sidebar_position: 2
```

## Category Index Pages

Creating an `index.mdx` inside a directory gives the category a landing page. The category name in the sidebar becomes a clickable link to this page.

A typical category index page uses the `<CategoryNav />` component to automatically list all pages in the category:

```mdx
---
title: Guides
sidebar_position: 1
---

Explore our guides to learn about zudo-doc features.

<CategoryNav category="guides" />
```

The `<CategoryNav />` component renders a grid of links to all pages in the category (excluding the index itself), sorted by `sidebar_position`.

Without an `index.mdx`, the category heading is a toggle-only control — clicking it expands or collapses the category, but does not navigate anywhere.

## Default Open/Closed

Categories containing the current page always open. When `sidebar_initial_expansion` is omitted, ordinary categories retain the existing expanded default; explicitly configured TypeScript `collapsed` values remain exceptions.

Set `sidebar_initial_expansion: collapsed` or `expanded` on a category's `index.mdx` to supply an inherited baseline. The nearest category index applies, and a field on the current page overrides it. This controls disclosure, while `initialPath` continues to control the initial sidebar scope.

```mdx
---
title: Guides
sidebar_position: 1
sidebar_initial_expansion: collapsed
---
```

Only explicit expand and collapse toggles by the reader are saved for the session. Automatically opening the current page's ancestors (on arrival, including Back/Forward and soft navigation, at any viewport width) and opening the root of a focused or broadened scope are not saved. The reader can collapse even the category containing the current page: it stays collapsed for that page, and hash changes, filter typing, and a reload of the same page do not reopen it. Navigating to another page reveals that page's path again, while choices in unrelated branches are kept.

Restore resets disclosure and scope to the current page's author baseline. Restore appears only when those effective values differ. At the highest scope the entire toolbar disappears, while each category's branch-focus control remains available.

[Dynamic content unavailable in text export]

Saved state from earlier versions mixed automatic and manual expansion. On the first load after upgrading, that earlier expand/collapse state is discarded once; a saved scope and the filter text are kept. This does not repeat.



## Sidebar Label

Use the `sidebar_label` frontmatter field to display a different label in the sidebar than the page title. This is useful when the full title is too long for the sidebar.

```mdx
---
title: Getting Started with zudo-doc Installation
sidebar_label: Installation
sidebar_position: 2
---
```

[Dynamic content unavailable in text export]

The `sidebar_label` only affects the sidebar. The page title and heading still use `title`.



## Directory Structure

Directories inside `src/content/docs/` become sidebar categories. The directory name is converted from kebab-case to Title Case automatically.

```
src/content/docs/
  getting-started/         → "Getting Started"
    index.mdx              → category index (sidebar_position: 0)
    introduction.mdx       → sidebar_position: 1
    installation.mdx       → sidebar_position: 2
    writing-docs.mdx       → sidebar_position: 3
  guides/                  → "Guides"
    index.mdx              → category index (sidebar_position: 1)
    configuration.mdx      → sidebar_position: 1
    frontmatter.mdx        → sidebar_position: 2
```

## Nested Categories (Sub-Groups)

Subdirectories within a category directory create nested collapsible groups in the sidebar. Each sub-directory needs an `index.mdx` file with `category_no_page: true` to set its label and position without creating a route:

```
src/content/docs/
  methodology/
    index.mdx
    architecture/
      index.mdx             → title: Architecture, sidebar_position: 1, category_no_page: true
      bem-strategy.mdx
      component-first.mdx
    design-systems/
      index.mdx             → title: Design Systems, sidebar_position: 2, category_no_page: true
      tight-token.mdx
      two-tier-size.mdx
```

With auto-generated sidebar, this renders as **3 levels deep**:

```
Methodology            (depth 0)
  ▸ Architecture       (depth 1, collapsible)
    BEM Strategy       (depth 2)
    Component First    (depth 2)
  ▸ Design Systems     (depth 1, collapsible)
    Tight Token        (depth 2)
    Two-Tier Size      (depth 2)
```

### Flattening with Explicit Sidebar Config

For better readability, use explicit sidebar configuration to promote sub-groups to the top level. This reduces nesting by one level. A configuration file is not auto-consumed: bind it through the `sidebarsConfig` chrome slot.

[Dynamic content unavailable in text export]

`sidebarsConfig` defaults to `{}`, so every section uses filesystem
auto-generation. Point `chromeBindingsModule` at a module that binds your
config. Fresh route stubs already consume it; do not edit them. See
[Custom Components](https://zudo-doc.takazudomodular.com/docs/guides/custom-components) and [Host Chrome Bindings](https://zudo-doc.takazudomodular.com/docs/reference/host-chrome-bindings).



Create a config whose keys exactly match `categoryMatch` values in your `headerNav`, then bind it:

```ts title="src/sidebars.ts"
import type { SidebarsConfig } from "@takazudo/zudo-doc/sidebar-utils";

const sidebars: SidebarsConfig = {
  guides: [
    "guides/introduction",
    { type: "doc", id: "guides/sidebar", label: "Sidebar" },
    { type: "link", label: "Project site", href: "https://example.com" },
    {
      type: "category",
      label: "Architecture",
      collapsed: true,
      sortOrder: "asc",
      items: [{ type: "autogenerated", dirName: "guides/architecture" }],
    },
  ],
};

export default sidebars;
```

The string form is a document id shorthand. The complete item vocabulary is a string
`"doc/slug"`, `{ type: "doc", id, label? }`, `{ type: "link", label, href }`,
`{ type: "category", label, items, collapsed?, sortOrder? }`, or
`{ type: "autogenerated", dirName? }`. A section without a matching key falls back to
filesystem auto-generation.

```ts title="src/chrome-bindings.ts"
import { defineChromeBindings } from "@takazudo/zudo-doc/chrome-bindings";
import sidebars from "./sidebars";

export const chromeBindings = defineChromeBindings({
  sidebarsConfig: sidebars,
});
```

```ts title="zfb.config.ts"
export default defineConfig(
  zudoDoc({
    chromeBindingsModule: "./src/chrome-bindings.ts",
  }),
);
```

This repository's `src/config/sidebars.ts` is a showcase-only reference file. Its exported
object is currently empty, so this showcase intentionally uses filesystem auto-generation for
every section.

```ts title="src/sidebars.ts"

const sidebars: SidebarsConfig = {
  methodology: [
    "methodology",
    {
      type: "category",
      label: "Architecture",
      items: [{ type: "autogenerated", dirName: "methodology/architecture" }],
    },
    {
      type: "category",
      label: "Design Systems",
      items: [{ type: "autogenerated", dirName: "methodology/design-systems" }],
    },
  ],
};

```

This renders as **2 levels**:

```

Methodology            (depth 0, leaf link)
▸ Architecture         (depth 0, collapsible)
  BEM Strategy         (depth 1)
  Component First      (depth 1)
▸ Design Systems       (depth 0, collapsible)
  Tight Token          (depth 1)
  Two-Tier Size        (depth 1)

```

Key points:

- The string `"methodology"` references the category index page as a **leaf link** (children are stripped automatically — the explicit config handles structure)
- `type: "autogenerated"` with `dirName` pulls articles from a specific subdirectory
- Sub-group categories render at depth 0 with bold labels and border separators
- Articles render at depth 1 — one level flatter than auto-generated

[Dynamic content unavailable in text export]

This pattern is recommended when categories have sub-groups. The explicit config keeps the filesystem organized while making the sidebar more readable.



## Sort Order (Ascending / Descending)

By default, items within a category are sorted in ascending order (lowest `sidebar_position` first, then alphabetical). You can reverse this to descending order per category using the `category_sort_order` frontmatter field on the category's `index.mdx`:

```mdx title="changelog/index.mdx"
---
title: Changelog
sidebar_position: 10
category_sort_order: "desc"
---
```

When `category_sort_order` is `"desc"`, items are sorted in reverse — highest position first, then reverse alphabetical. This is useful for date-based content (changelogs, release notes) where the newest items should appear at the top.

Note trays use the same field as their single ordering contract. The selected direction controls how stable ranks are listed in the sidebar, `<NoteTrayIndex>`, previous/next pager, and home-page block; the rank values themselves never change. With `note_tray_sidebar: "year"` or `"month"`, groups are chronological in that direction and items inside each group follow rank. Keep `sidebar_position` monotonic with `date` so grouped dates and reading order agree. See [Note Trays](https://zudo-doc.takazudomodular.com/docs/guides/note-tray) for the complete setup.

[Dynamic content unavailable in text export]

`_category_.json` still works but triggers a zfb build warning. Prefer frontmatter on `index.mdx` for new categories.



You can also set `sortOrder` in a bound sidebar configuration for manual sidebar configurations:

```ts

{
  type: "category",
  label: "Releases",
  sortOrder: "desc",
  items: [
    { type: "autogenerated" },
  ],
}

```

[Dynamic content unavailable in text export]

For changelog-style categories, combine `sortOrder: "desc"` with date-prefixed filenames (e.g., `2026-03-10-release.mdx`) for automatic newest-first ordering without manually setting `sidebar_position`.



## Ordering Recommendations

- Set `sidebar_position` on every page for predictable ordering
- Use `sidebar_position: 0` on category index pages
- Use sequential numbers (1, 2, 3...) for pages within each category
- Leave gaps between numbers (e.g., 10, 20, 30) if you expect to insert pages later
- Use `category_sort_order: "desc"` in `index.mdx` for newest-first categories
- For a note tray, choose `note_tray_sidebar: "index"`, `"year"`, or `"month"`; year and month require dates on every item

## Broader Tree

The controls below the filter change the visible navigation tree without changing the article, reading position, or table of contents. **Broaden tree** moves to the immediate editorial parent. At the highest available tree the whole scope toolbar (Broaden and Restore) is hidden; there is no disabled button or hint. A custom forest can be that highest tree.

Each category has a separate **Show only this branch** target button. Category links still open pages, and disclosure buttons still expand or collapse children. **Restore current page’s local tree** appears while a different scope is selected and returns to the current page’s configured roots and order. These controls also work in the mobile drawer without closing it; page links close it normally.

The filter stays in place when changing scope. A matching category keeps its subtree; a matching descendant keeps its ancestors. Branch expansion choices survive Broaden and branch focus. Restore returns to the configured baseline (including `initialPathMode: "node"`), clears the selected scope and the saved expand/collapse choices, keeps the filter text, and reveals the current page; a filter that excludes that page must be cleared to show it. The desktop sidebar and the mobile drawer update together. Trees remain recursive with no depth limit.

Scope is temporary session state. `initialPath` and `initialPathMode` define only the fresh/Restore baseline. Sections that share an editorial context (the same `contextId` for the same complete `items` tree, or the automatic tree) share saved state, so a broadened or focused scope the reader chose stays in effect across those pages until Restore. Following a link inside the selected tree keeps it; another branch widens to the nearest shared editorial ancestor. A saved scope whose view equals the current page's baseline is treated as the baseline, so no Restore button appears. Locale, version, or independent document-context changes return to the destination’s configured sidebar. Changed topology invalidates stored scopes. URLs and modified-click/new-tab link behavior stay native.

### Explicit Editorial Context

Existing array configurations keep their initial behavior. To supply parents outside a local view, use the object form: `items` describes the complete editorial tree and `initialPath` selects the children of a category by zero-based indexes in the resolved, ordered tree. In this example the initial forest contains Background and Border; the first broader view is Surfaces & borders, followed by Utility reference. These groups need not appear in document URLs.

```ts title="src/sidebars.ts"
import type { SidebarsConfig } from "@takazudo/zudo-doc/sidebar-utils";

const sidebars: SidebarsConfig = {
  utilities: {
    contextId: "utility-reference",
    items: [
      {
        type: "category",
        label: "Utility reference",
        items: [
          {
            type: "category",
            label: "Surfaces & borders",
            items: ["utilities/background", "utilities/border"],
          },
        ],
      },
    ],
    initialPath: [0, 0],
  },
};

export default sidebars;
```

Bind this object through `chromeBindings.sidebarsConfig` as in the configuration example above. `contextId` is optional; use the same value for sections that share the same complete `items` tree but choose different `initialPath` values. Locale, version, and topology revision are added automatically. Invalid paths fail the build rather than inventing parents.

#### Selecting the Node Itself

`initialPathMode` controls what `initialPath` selects. The default, `"children"`, shows the children of the selected category. With `"node"`, the selected node itself is the baseline: its row (a page-backed section keeps its overview link) with its children. `navigation.roots` still holds the complete editorial tree, so Broaden goes to the target's actual enclosing parent and then on up, without a duplicate or invented parent row.

```ts title="src/sidebars.ts"
import type { SidebarsConfig } from "@takazudo/zudo-doc/sidebar-utils";

const sidebars: SidebarsConfig = {
  wind: {
    items: [{ type: "autogenerated", dirName: "wind" }],
    // Utility reference → Surfaces & borders
    initialPath: [0, 0],
    initialPathMode: "node",
  },
};

export default sidebars;
```

Take an autogenerated tree Wind → Utility reference → Surfaces & borders → Background color, with `initialPath` pointing at Surfaces & borders. On Background color, node mode first shows Surfaces & borders (linked to its overview) with its pages; Broaden then shows Utility reference, then Wind. Children mode first shows only the pages of Surfaces & borders, and the first Broaden adds the Surfaces & borders row.

A missing or empty `initialPath` keeps the whole resolved forest in either mode. A valid path ending at a leaf page is allowed in node mode and shows that page alone. Invalid indexes and any other `initialPathMode` value fail the build. Both modes apply identically to the desktop sidebar and the mobile drawer.

An object definition with empty `items` (`items: []`) uses the automatic category-filtered tree, exactly like an omitted entry, and then applies `initialPath` and `initialPathMode` to it. Both `buildSidebarForSection` and `buildSidebarNavigation` agree on the result, and invalid paths or modes fail the same way. A missing entry or an empty array keeps the plain automatic fallback.

An `autogenerated` entry retains the provenance of its omitted directory root. Authored groupings remain authoritative, and duplicate labels or links keep separate occurrence identities. A custom forest without a shared parent stops at that forest. Header dropdown grouping is not document ancestry.

Custom integrations can call `buildSidebarNavigation` from `@takazudo/zudo-doc/sidebar-utils` and pass its `nodes` and `navigation` to `SidebarTree` or `SidebarToggle`. `buildSidebarForSection` and nodes-only island props remain supported; nodes-only integrations keep the existing tree without scope controls.

### Client Navigation Data

The native header and sidebar factories copy only the fields needed by their sidebar islands. Page descriptions remain in server navigation data for category rendering, but are omitted recursively from both visible nodes and the broader tree sent to the browser. Mobile `sidebarNodes` arrays and callback results are copied without remapping URLs; their object identity is not retained.

Custom hosts can call `toSidebarClientData(nodes, navigation)` from `@takazudo/zudo-doc/sidebar` before constructing a `SidebarTree` or `SidebarToggle` island, then pass the returned object to both SSR and hydration. The helper validates input before selecting fields and preserves occurrence IDs, topology, expansion defaults and the existing navigation context ID. Descriptions can still affect that context ID; this is payload minimization, not a privacy boundary or a change to scope persistence.