Config-less navigation plugin for Steno that builds
config.navigation straight from content/'s own folder structure -- no manually maintained nav
list to keep in sync as pages get added, renamed, or removed.
This is for the common case where a site's navigation is really just "every top-level page and
section, in some order" -- maintaining a parallel navigation: list in config means updating two
places every time a page moves. Inspired by
this Zola discourse thread
on doing the same thing without a config-defined nav list.
# content/.steno/config.yml
plugins:
- jsr:@steno/plugin-navNo options are required -- every page already existing under content/ is enough.
plugins:
- package: jsr:@steno/plugin-nav
options:
indexFile: index
excludeField: excludeFromNav
weightField: navWeight| Option | Type | Default | Description |
|---|---|---|---|
contentDir |
string |
config.contentDir (content/) |
Root to scan. |
indexFile |
string | false |
"index" |
Filename stem (no extension) treated as a folder's own page. false disables index-file detection -- every folder then gets an unlinked label node. |
excludeField |
string |
"excludeFromNav" |
Both this and its snake_case form are always checked, regardless of what's configured here. |
weightField |
string |
"navWeight" |
Same dual-form checking as excludeField. |
force |
boolean |
false |
Overwrites an already-set config.navigation instead of leaving a manually configured one alone. |
The plugin hooks into Steno's beforeBuild stage, before Steno renders any page, and walks
contentDir to build a NavigationNode[] tree:
- Every
.mdfile becomes a leaf node -- title from frontmattertitle, falling back to a humanized filename (getting-started.md→ "Getting Started") when absent, matching how Steno itself falls back a page's title to its first heading, then the site title (see content.md). - A folder with an
index.md(or whateverindexFilenames) uses that page's title/url as the folder's own nav node -- a link, not just a label. A folder with no index file still gets a node, humanized from the folder name, just with nourlof its own -- an unlinked category label wrapping its children. - A page is skipped entirely when its frontmatter sets
exclude_from_nav: trueorexcludeFromNav: true(both spellings checked), ordraft: true, in bothsteno devandsteno build-- nav is a structural concern, not a preview concern. - Siblings are sorted by
navWeight/nav_weight(numeric, ascending) when set; anything without a weight falls back to alphabetical-by-title and sorts after every weighted sibling. content/index.mdbecomes a{ title: "Home", url: "/" }entry alongside its siblings -- set its owntitlein frontmatter to rename it.- A folder with no index file and no non-excluded, non-draft pages inside produces no node at all, rather than an empty dead-end category.
- Writes the result to
config.navigation, unless the site already set one itself andforceisn't set -- this plugin never silently fights a site that configuresnavigationby hand.
Given:
content/
├── index.md title: Home
├── blog/
│ └── index.md title: Blog
├── now.md title: Now
├── projects/
│ ├── index.md title: Projects
│ └── widget.md title: Widget
└── plans.md exclude_from_nav: true
Produces:
Home Blog Now Projects
└── Widget
A theme wanting this bundled the same way as
plugin-accent-color (so a site using the theme gets it for free,
no plugins: entry needed) can do so from its own mod.ts:
import nav from "@steno/plugin-nav";
const theme: StenoTheme = {
// ...
plugins: [nav()],
};config.navigation matches Steno's existing NavigationNode[] shape
(types.ts), already exposed to layouts
as site.navigation per the
layout context
-- no new context field needed, this plugin just populates one Steno core already reserves for
navigation.
<nav>
{#each site.navigation as item}
<a href="{item.url | url}">{item.title}</a>
{#if item.children}
{#each item.children as child}
<a href="{child.url | url}">{child.title}</a>
{/each}
{/if}
{/each}
</nav>
deno task test- Steno plugin development guide
- Zola discourse: building nav without config entries -- the linked inspiration
MIT