Skip to content
Blume
Esc
↑↓navigate↵open⌘Jpreview
On this page

Navigation

Blume builds the sidebar from your files, then lets you refine it with frontmatter, folder meta, or config — breadcrumbs and outlines follow along.

Blume builds your sidebar from the file system, then lets you refine it as much — or as little — as you want: page by page, folder by folder, or with one explicit config. Breadcrumbs, previous/next links, and the on-page outline all follow from the same model, with nothing to wire up.

The generated sidebar

By default the sidebar mirrors your content tree:

  • folders become groups, files become pages
  • a page’s label is its frontmatter title; a group’s label is the humanized folder name, with common acronyms like API, CLI, and SDK capitalized (api-reference reads “API Reference”)
  • items sort by numeric prefix, then alphabetically, and a folder’s index page comes first
  • a folder with an index page links its group row to that page, so clicking the section name opens the section’s landing page

That’s enough for many sites — everything below is opt-in.

Page label, icon, and badge

Tune how a single page appears in the sidebar from its frontmatter, under sidebar:

sidebar:
  label: Quickstart # override the title in the sidebar
  icon: rocket # an icon from Blume's built-in set
  badge: New # a small label beside the entry
  order: 1 # sort position within its group

See Frontmatter for the full page schema.

Folder groups

Each folder becomes a sidebar group. Drop a meta.ts beside its pages to set the group’s title, icon, order, and the order of its children:

import { defineMeta } from "blume";

export default defineMeta({
  title: "Guides",
  icon: "book-open",
  pages: ["configuration", "theming", "deployment"],
});

See Folder meta for every field and computing meta at scan time.

A folder’s meta.title and its own index page’s frontmatter title are resolved independently — translating one under i18n and forgetting the other renders a correct sidebar with a stale heading on the landing page itself. Blume reports a BLUME_NAV_INDEX_TITLE_MISMATCH warning when they diverge on an index page that hides its own sidebar row (sidebar.hidden: true), where the folder title is the only sidebar label the page has. The check reads title, which the page’s heading shows; seo.title replaces only the <title>, so it doesn’t count. Untranslated pages filled in from the fallback locale are exempt — their title belongs to the fallback locale, and the fix is translating the page, not editing its frontmatter.

Like every warning, it fails blume validate --strict. Any of these clears it:

  • Change the page’s title or the folder’s meta.title so the two match.
  • Keep the page’s title on purpose by setting its sidebar.label to the folder title, the way any page sets a sidebar label that differs from its title.
  • Show the index row again. The sidebar then shows both titles, so pairing a folder title with a different page title (“CLI” over “Overview”) is fine.

To group pages without adding a URL segment, use a parenthesized folder name — see Pages.

Display modes

navigation.sidebar.display sets how every sidebar group renders:

navigation: {
  sidebar: {
    display: "flat", // "flat" | "group" | "page"
  },
}
  • flat (default) — a non-collapsible header with its pages listed beneath. Pages that aren’t in any group always list first, above the group sections, so they can’t be mistaken for a group’s children.
  • group — a collapsible <details> disclosure per group. Groups start collapsed by default; a group containing the current page always starts open, so only the section you’re in is expanded. A group that is the only row at the top of the sidebar, like a folder that wraps every page, starts open too, so the sidebar never shows a single closed row. Set collapsed: false in folder meta to force a group open regardless, or collapsed: true to keep that lone group closed.
  • page — each group is a single row whose chevron slides the sidebar into a sub-panel showing only that group’s items, with a back arrow at the top. A group with an index page links the row’s name to that page, and a group without one slides the panel in from anywhere on the row. The panel is route-aware, so landing directly on a page inside the group opens straight to it. After the back arrow, the row of the section you’re in stays highlighted.

In both group and page mode, a section that isn’t open on the current page is left out of that page’s HTML and fetched the first time it’s opened (it’s prefetched as soon as the pointer or focus reaches its row, so the open is usually instant, and a section fetched once is kept for the rest of the visit). On a large site this is most of a page’s weight: only the open section’s rows ship with the page. The rows are prerendered fragments under /blume-nav/, so they need no server. Readers without JavaScript see the open section and the group rows; the sitemap, the previous/next links, and the open section keep every page reachable for crawlers.

Per-group overrides

Any generated group can opt out of the global mode — no explicit sidebar required. Set display in the folder’s meta.ts, or — when the folder has an index page — under sidebar in that page’s frontmatter, and only that group changes:

import { defineMeta } from "blume";

export default defineMeta({
  title: "Client SDKs",
  display: "page",
});
---
title: Client SDKs
sidebar:
  display: page
---

A generated group’s effective mode resolves highest priority first:

  1. sidebar.display in the group’s own index page frontmatter
  2. display in the folder’s meta.ts
  3. The global navigation.sidebar.display
  4. The Blume default (flat)

A group’s display applies to that group only — nested subgroups resolve their own value through the same chain. A page-mode group with an index page links its row’s name to that page, so clicking the name opens the landing page with its sub-panel already open, while the chevron beside it slides the panel in without leaving the current page. The panel’s title links back to the landing page, and the index page lists as the panel’s first item unless it’s hidden. A page-mode group without an index page has nothing to open, so its whole row slides the panel in.

sidebar.display means nothing anywhere else — on a non-index page, on the content root’s own index page (the root isn’t a group; use navigation.sidebar.display), or on any page when an explicit sidebar is configured (its items own each group’s mode) — so Blume reports a BLUME_SIDEBAR_DISPLAY_IGNORED warning instead of silently dropping it. collapsed remains specific to group mode; it’s inert when a group resolves to flat or page.

A group in an explicit sidebar overrides the global mode with its own display, exactly as before.

Directory listings

A group’s own page can list the group’s other pages below its content, so a section’s landing page doubles as its table of contents. Set directory in the folder’s meta.ts:

import { defineMeta } from "blume";

export default defineMeta({
  directory: "card",
});
  • card shows each page and subgroup as a card with its icon and description.
  • accordion lists the pages as rows, and each subgroup as a section that opens to its pages.
  • none lists nothing. This is the default.

Only a group with its own page has somewhere to show the listing: a folder’s index page, or an explicit group’s root. The setting carries down to nested folders, so one directory on the content root’s meta.ts gives every section a listing, and a folder can set its own value (none included) to override what it inherits. An explicit sidebar sets directory on its groups the same way.

Ordering

When the sidebar is generated, order is resolved highest priority first:

Config sidebar

An explicit navigation.sidebar replaces the generated tree entirely.

Folder meta

The pages array in meta.ts orders a group.

Frontmatter

sidebar.order on a page.

File system

An index page first, then numeric prefixes, then alphabetical by label.

Two siblings that land on the same explicit or numeric order fall back to alphabetical order between themselves — Blume reports a BLUME_DUPLICATE_SIDEBAR_ORDER warning so the tie doesn’t go unnoticed.

Hidden pages

Hide a page from the sidebar — and from previous/next pagination — while keeping it built and reachable by its URL:

sidebar:
  hidden: true

A hidden page also stays out of the sitemap and llms.txt, and out of site search unless search.indexing.includeHiddenPages is on.

A folder’s index page appears both as the group row’s link and as the first row inside the group. Hide the index page to keep only the linked header: the group row still opens the landing page, and previous/next links still pass through it. Since the navigation still links the page, it isn’t a hidden page anywhere else: it stays in the sitemap, site search, and llms.txt. The same goes for the site’s home page, the root index page: hiding it drops its sidebar row, but the site’s root URL still serves it, so it stays listed too.

Tabs

Render top-level sections as tabs in the header, useful for splitting a large site into distinct areas — say adapters, an API, and AI guides. A tab is highlighted when the current route falls under its path:

navigation: {
  tabs: [
    { label: "Adapters", path: "/adapters", icon: "plug" },
    { label: "API", path: "/api", icon: "rocket" },
    { label: "AI", path: "/ai", icon: "sparkles" },
  ],
}

A tab’s optional icon (a built-in icon name, image path/URL, or inline SVG) shows beside its label, in the header and in the mobile navigation drawer.

An enabled OpenAPI, AsyncAPI, or GraphQL reference mounts at its route but doesn’t add a tab on its own — point a tab at that route to surface it in the header (and, for the native renderer, to scope its operations sidebar), with whatever label you like:

navigation: {
  tabs: [
    { label: "API", path: "/reference" },
  ],
}

A tab’s path is its section prefix, and it doubles as the link target. A section whose path isn’t a page of its own — a folder with no index.mdx — would link to a 404, so the tab falls back to the first page in the section instead. That includes a tab at path: "/" on a site with no root index.mdx: it links to the first page in the sidebar. A static custom page at the tab’s path counts as the section’s own page: with pages/guides.astro, a /guides tab lands on that page while the guides/ folder fills its sidebar. So does the generated changelog index, so a /changelog tab opens the timeline rather than the newest entry.

Set href when you want a tab to land somewhere else — a particular page in the section, say:

navigation: {
  tabs: [
    { label: "Guides", path: "/guides", href: "/guides/getting-started" },
  ],
}

Tabs that don’t set href keep the resolution above.

Give a tab items to make it a dropdown. The tab no longer links anywhere itself: it opens a menu of its items in the header, and expands them in place in the mobile navigation drawer. Its path still scopes the sidebar and marks the tab as current, and href doesn’t apply. Each item takes a label and a path, plus an optional icon, description, and tag, like a selector item:

navigation: {
  tabs: [
    { label: "Guides", path: "/guides" },
    {
      label: "SDKs",
      path: "/sdks",
      items: [
        { label: "JavaScript", path: "/sdks/javascript", description: "Node and the browser" },
        { label: "Python", path: "/sdks/python", tag: "Beta" },
      ],
    },
  ],
}

On an i18n site, a tab’s label (and a dropdown item’s) can be a per-locale map instead of a string — the active locale’s entry wins, then the default locale’s:

navigation: {
  tabs: [
    { label: { en: "Docs", fr: "Documentation" }, path: "/docs" },
    { label: "CLI", path: "/cli" }, // a plain string renders as-is everywhere
  ],
}

A selector’s labels don’t take a map: its label and each of its items’ labels are plain strings.

Tabs also scope the sidebar: when the current route falls under a tab’s path, the sidebar shows only that section’s pages — so /adapters/* lists the adapters and nothing else. The folder at a tab’s path becomes the section, so this needs no extra config beyond the tabs themselves; structure your content into a folder per tab and point each tab at it.

On a route under no tab (or a tab whose path is /), the sidebar shows the pages that don’t belong to a tab — each tab’s folder is hidden from it, since that section already has its own tab in the header. So a root landing page lists your loose top-level pages while the sectioned content stays behind its tab, mirroring Fumadocs’ root folders. If a route has no pages of its own to show this way, the full tree is shown instead, so the sidebar is never left blank.

Selectors

For switching between whole partitions of a site — a product, a version, or any grouped set of destinations — add a selector. Each renders as a dropdown in the header, showing the option whose path matches the current route:

navigation: {
  selectors: [
    {
      kind: "version",
      label: "Version",
      items: [
        { label: "v2 (latest)", path: "/v2", icon: "rocket" },
        { label: "v1", path: "/v1" },
      ],
    },
  ],
}

Each item takes a label, a path, and optional icon, description, and tag. kind (dropdown, product, version, or language) is a hint for how the selector is used; all render the same dropdown.

With versioning configured, Blume renders a version selector automatically — declaring your own kind: "version" selector here replaces the automatic one, so hand-rolled setups keep working.

On screens narrower than the desktop sidebar, selectors, the version selector, and the language switcher move out of the header and into the top of the mobile navigation drawer, where each spans the drawer’s width.

Pin links to the top of the sidebar, above every section — a blog, a changelog, a contact or support page that should always be one click away. Unlike the generated tree, featured links are not scoped by tab: they show on every route, on every breakpoint.

navigation: {
  featured: [
    { label: "Blog", href: "https://example.com/blog", icon: "newspaper" },
    { label: "Contact", href: "/contact", icon: "headphones" },
  ],
}

Each link takes a label, an href, and an optional icon (a built-in icon name, image path/URL, or inline SVG — the same as anywhere else). An href may point anywhere: an external URL opens in a new tab, while an internal route (/contact) is validated against your pages at build time and by blume doctor, warning you if nothing matches. A path to a file in public/ (/spec.pdf) or one Blume generates (/llms.txt) matches too.

Explicit sidebar

For full control, list explicit items in navigation.sidebar — a bare array is shorthand for sidebar.items, and the object form combines them with a global display. When items are set, Blume uses them verbatim and skips file-system generation:

navigation: {
  sidebar: [
    "/", // a page, referenced by route
    {
      label: "Guides", // a group
      collapsed: false,
      items: ["/configuration", "/configuration/theming"],
    },
    { label: "GitHub", href: "https://github.com/owner/repo" }, // an external link
  ],
}

Each item is a page route (a string), a group (label + items), or a link (label + href). Groups can nest, override the global display mode, start collapsed, and link a root page, which can list the group’s pages with a directory.

Blume warns about an item it can’t render as written: a route that matches no page (the item is left out), a root that matches no page (its link would 404), or an item with no route, href, root, or items (left out).

Header actions

navigation.actions puts plain links in the header, left of the icon buttons, and navigation.cta is the one filled button:

navigation: {
  actions: [{ href: "/changelog", label: "Changelog" }],
  cta: { href: "https://example.com/signup", label: "Start free" },
}

Like tab labels, a label here can be a map of locale code to text on a multilingual site.

An action is only a link, so a changelog linked this way still lists its entries in the sidebar. Link it with a tab to keep them out.

cta is singular on purpose — a docs header has room for exactly one thing a reader is being asked to do, and a row of buttons asks for nothing. Secondary links belong in actions, or in featured if they should sit with the sidebar instead.

An http(s) or protocol-relative href opens in a new tab; a route stays in the tab, and is validated against your pages at build time like a featured link — so a page served by another app on the same host (/signup on the product, say) should be written as an absolute URL. actions are hidden below the sm breakpoint, where the header has room for the logo and the navigation toggle and nothing else. cta hides there too, except on a page with no navigation toggle — a landing page on PageLayout with no tabs — where it stays, since nothing else can surface it on a phone.

When you set github in your config, Blume shows a GitHub icon in the footer, ahead of your social profiles, that links to your repository. It’s on by default; hide it with navigation.repo:

navigation: {
  repo: false, // hide the footer's GitHub link (default: true)
}

The link only appears when github is configured, so projects without a repo are unaffected either way.

repo also takes an absolute URL, which points the GitHub mark anywhere on GitHub:

navigation: {
  repo: "https://github.com/acme",
}

That’s for a project whose docs repo is private. github drives the per-page edit link, the GitHub mark and the agent manifest’s repository together, so such a project has to leave github unset — and a URL is what lets it still show a mark pointing somewhere public. A footer.socials.github URL takes the mark’s place, and a link to another host belongs in the footer’s links or actions.

These come for free from the sidebar tree — no configuration:

  • Breadcrumbs show the current page’s parent group above the title.
  • Previous and next links at the foot of each page follow sidebar order, skipping hidden pages. Set pagination: false in a page’s frontmatter to leave them off that page.

On this page

A right-rail outline is generated automatically from each page’s headings — ## and ### by default — so long pages stay scannable. Set toc to change the heading range or turn the outline off. On narrower screens, where the right rail is hidden, it collapses into an “On this page” dropdown above the content, which also holds the page actions.

Page actions

Below the table of contents, every page shows a set of quick actions. In windows at least 1,280px wide they sit in the right-hand column with the outline. In narrower windows and on phones they sit at the bottom of the “On this page” dropdown above the content, which shows on a page with no headings too:

  • Edit on GitHub — links straight to the source file. Appears once you set github in your config, on every page whose file is in the repository: a file git ignores, like a generated API reference written into a gitignored folder, gets no edit link, since GitHub has no file to open.
  • Scroll to top — smoothly returns to the top of long pages. It’s in the right-hand column only, since the dropdown already sits at the top of the page.

Others hand the page to AI tools — Copy as Markdown and Open in chat — covered in Markdown for agents.

Feedback lives at the foot of the page instead: a “Was this page helpful?” yes/no rating that sends a feedback analytics event and doesn’t need github — see Page feedback.

With export on, an Export action also lets readers download the page as a PDF or EPUB.

Last updated on

Was this page helpful?