Your Content Folder Is the Application: Recursive Navigation with WBPress

A content folder, frontmatter, and explicit nodes becoming one WBPress content graph
This is the canonical, maintained edition. Community platforms receive adapted versions that link back here.
Release availability — November 2026: this tutorial targets the release after
WBPress 3.0.0-rc.2, which adds recursive directory navigation. Until it reaches npm, continue
writing explicit navigation nodes with rc.2.

Most content frameworks eventually make authors maintain the same hierarchy twice: once in the

filesystem and again in route or sidebar configuration.

That duplication looks harmless until a page moves, a section is renamed, or navigation order

changes. Then the files, routes, sidebar, search index, and sitemap begin to disagree.

WBPress removes that duplication without giving up explicit control.

Declare a real content directory with items: './content/'. WBPress recursively expands it into its

canonical content graph. That graph then drives routes, nested navigation, search, metadata,

previous/next links, and publishing artifacts. When convention is not enough, mix explicit Vue,

WBC, media, or custom-route nodes into the same graph.

real content folder + frontmatter + explicit exceptions
                           ↓
              items: './content/'
                           ↓
                        WBPress
                           ↓
 routes + navbar + sidebar + content + search + metadata + sitemap

The folder is the authoring source. The normalized graph is the application contract. Both remain

visible, portable, and testable.

This tutorial builds a small documentation application from scratch to show how simple that process

can be.

What we are building

Imagine an open-source product called SignalForge: a lightweight webhook development toolkit.

Its documentation needs to answer four questions:

  1. What problem does SignalForge solve?
  2. How do I install it?
  3. How do I send my first event?
  4. Where can I inspect the API and examples?

The folder hierarchy expresses the intended information architecture:

/
├── guide/
│   ├── installation/
│   └── quickstart/
├── reference/
│   └── configuration/
├── examples/
└── playground/

The site should have a responsive sidebar, dark mode, full-content search, previous/next links,

metadata, and a sitemap. The playground should be a Vue component; the rest can remain Markdown.

Step 1 — make the content folder the source of truth

Start with the part that matters most: the information.

src/content/
├── index.md
├── guide/
│   ├── index.md
│   ├── installation.md
│   └── quickstart.md
├── reference/
│   ├── index.md
│   └── configuration.md
└── examples.md

For example, src/content/index.md might begin with:

# SignalForge

Develop, replay, and inspect webhooks without rebuilding the same local tooling for every project.

## From event to evidence

SignalForge receives an event, records its delivery attempt, and lets you replay it against a local
or remote endpoint.

[Install SignalForge](/guide/installation) or [send your first event](/guide/quickstart).

And src/content/guide/quickstart.md can be ordinary technical Markdown:

# Quickstart

Start the local receiver:

npx signalforge listen --port 4100


Send a test event:

curl -X POST http://localhost:4100/events \

-H 'content-type: application/json' \

-d '{"type":"invoice.paid","id":"evt_001"}'

These files remain portable Markdown, and their actual locations—not a second hand-written route

list—define the ordinary site hierarchy.

Step 2 — make the folder executable

A real filesystem folder can become the ordinary part of the content graph directly. Declare it

once with the explicit trailing-slash directory marker: items: './content/'.

The interactive playground remains an explicit Vue page. This hybrid is intentional: discovery

handles conventional content while one node handles the application-specific exception.

WBPress applies straightforward conventions:

  • index.md becomes its directory route;
  • nested directories become nested children;
  • filenames become route segments;
  • numeric prefixes and frontmatter order control sibling order;
  • labels and metadata come from frontmatter;
  • frontmatter can override labels, descriptions, order, icons, and sidebar visibility;
  • the explicit playground node is merged with the discovered graph.

The tree printed above is documentation for the reader, not duplicated configuration. When files

move inside src/content/, the discovered routes and sidebar follow the folder on the next build.

Step 3 — declare the directory once

The central declaration in src/config/navigation.js is intentionally small:

import { defineNavigation } from '@wbc-ui/press';

export default defineNavigation({
  items: [
    './content/',
    { name: 'Playground', path: '/playground', item: './pages/Playground.vue', label: 'Playground' },
  ],
  nav: ['sbarTrigger', 'logo', 'brand', 'VSpacer', 'search', 'dark'],
  side: true,
  search: true,
  prevNext: true,
  toc: true,
  headingAnchors: true,
  darkMode: { default: 'auto' },
});

The trailing slash is the opt-in. ./content/guide.md remains a normal WBC file item;

./content/ requests recursive discovery. A ../content/ reference works too when the supplied

context uses the same relative base.

Step 4 — provide a build-safe content inventory

WBPress does not ask the browser to read a filesystem. Vite supplies a static inventory through

createGlobContext, and WBPress expands only the declared directory:

import { createGlobContext, createWbPress } from '@wbc-ui/press';
import navigation from './config/navigation.js';

const context = createGlobContext({
  raw: import.meta.glob('./content/**/*.{md,mdx,html,json}', {
    eager: true,
    query: '?raw',
    import: 'default',
  }),
  lazy: import.meta.glob('./content/**/*.{vue,js,ts}'),
});

createWbPress(navigation, { context, vuetify }).mount('#app');

Before Vue Router, the sidebar, or search sees anything, WBPress converts the directory into ordinary

navigation nodes. The resulting graph is inspectable and can be validated like a hand-written one.

Step 5 — understand what WBPress derives from the graph

From the same nodes, WBPress can produce:

  • Vue Router routes;
  • the navbar and nested sidebar;
  • selected and expanded navigation state;
  • previous/next links in graph order;
  • page titles and metadata;
  • searchable records joined to Markdown body content;
  • sitemap and route-manifest entries;
  • feed entries when enabled;
  • localized page selection when each item is a language map.

The application does not need six disconnected configuration files that can drift away from each

other. The graph is the shared source.

Step 6 — run and verify the result

Run the same checks used for an explicitly authored graph:

npm run dev
npm run build
npm run preview

Then verify behavior, not only compilation:

  1. Every path opens directly and through the sidebar.
  2. Searching for a phrase inside a paragraph finds the correct page.
  3. Mobile navigation opens, closes, and restores focus correctly.
  4. Light and dark themes remain readable.
  5. Previous/next links follow the intended tree order.
  6. The playground is a real Vue page inside the same content graph.
  7. Metadata, sitemap, robots file, and search index contain the public URLs.
  8. A missing Markdown leaf or duplicate route name fails visibly.

What did we actually get?

We started with:

  • one real folder containing seven small Markdown files;
  • one explicit Vue playground;
  • one directory declaration.

We finished with:

  • a normal Vue 3 + Vite project;
  • a convention-generated but reviewable route graph;
  • responsive navigation and nested sidebar;
  • Markdown and Vue pages in one application;
  • full-content search;
  • light and dark themes;
  • page metadata and canonical URLs;
  • sitemap, route manifest, search index, and optional feed;
  • production build evidence;
  • source code that remains understandable without a proprietary project database.

That last property matters most: moving or adding ordinary content does not require maintaining a

parallel route tree.

Where an explicit graph still belongs

An explicit graph remains useful when public URLs deliberately differ from content folders, when a

migration must preserve legacy paths, or when most pages are dynamic WBC/Vue nodes. In those cases,

write complete defineNavigation(...) nodes.

Do not maintain both a copied route tree and a matching content folder for an ordinary docs site.

Two sources that express the same hierarchy can drift. Prefer the folder as the source; add explicit

nodes only for genuine exceptions.

Why directory discovery does not become an invisible router

WBPress does not ask the browser to scan a disk. A statically declared Vite context inventories the

content, and the explicit trailing-slash marker enables discovery. WBPress expands that inventory

into its normal graph before routing.

A WBPress node can render:

  • Markdown;
  • a Vue component;
  • an image or video;
  • an external URL or embed;
  • a localized item map;
  • an array or object rendered through WBC;
  • a context-aware dynamic descriptor.

The graph also carries labels, ordering, icons, metadata, visibility rules, and children. A folder

name cannot express all of those decisions.

So the workflow combines the best part of file routing—low ceremony—with the best part of WBPress:

an explicit programmable graph for every exception.

The capability in one sentence

**Your folder supplies the convention; WBPress supplies the application graph; explicit nodes
preserve control wherever convention stops.**

This is more than filesystem routing. The same normalized graph coordinates rendering, navigation,

search, metadata, and publishing without forcing every page into the same content type.


Resources


</div>