Building a manual-mode site#
Manual mode is the simplest pipeline. You write Markdown; Ovellum produces HTML, CSS, and a tiny bit of JavaScript. There's no source parsing, no merge engine, no orphan archive — just rendering.
The minimum project#
my-docs/
ovellum.config.json
content/
index.md
getting-started.md
Config:
{
"mode": "manual",
"input": "./content",
"output": "./dist",
"site": {
"title": "My docs"
}
}
Build:
npx ovellum build
Result:
dist/
index.html
getting-started/index.html
assets/
ovellum.css
ovellum.js
Pretty URLs are the default. Every page becomes <slug>/index.html so the
URL is /<slug>/. No server-side rewrites needed; works on any static
host.
Adding navigation#
The sidebar nav is built automatically from your file tree. Page titles come from:
- The frontmatter
title:field, if set. - The first
# H1in the body, otherwise. - The filename, as a last resort (
getting-started.md→Getting started).
For ordering and group titles, drop a _meta.json into any directory:
content/
guides/
_meta.json
install.md
configure.md
deploy.md
{
"title": "Guides",
"order": ["install", "configure", "deploy"]
}
order is a list of slugs (file or subdirectory names without .md).
Anything not listed sorts alphabetically after the explicit set.
Adding the right-side ToC#
There's nothing to enable — the right column populates automatically from
every page's ## h2 and ### h3 headings. Each heading also gets a
clickable # anchor on hover, so readers can deep-link.
You don't need a single configuration line for any of this. Write Markdown, get a working ToC.
Static assets#
Anything in content/ that isn't a .md file passes through verbatim:
content/
images/
architecture.svg
screenshot.png
hello.md
Reference assets with relative paths in your Markdown:

After build:
dist/
images/
architecture.svg
screenshot.png
hello/index.html
Landing page #
Manual mode has an opt-in landing template for the root URL. Disabled by
default — if you don't set site.landing.enabled: true, / renders
whatever content/index.md says, with the regular doc layout.
When you do enable it, / becomes a Material for MkDocs-inspired
homepage: hero, feature grid, optional prose body, optional trust strip.
The topbar gains a "Docs" link so readers always have a path into the
documentation proper.
{
"site": {
"landing": {
"enabled": true,
"docsHref": "/getting-started/",
"hero": {
"title": "My project",
"subtitle": "What it does in one sentence.",
"ctas": [
{ "label": "Get started", "href": "/getting-started/" },
{ "label": "GitHub", "href": "https://github.com/me/proj", "style": "secondary" }
]
},
"features": [
{ "title": "Fast", "description": "Builds in seconds." },
{ "title": "Themed", "description": "Auto/light/dark out of the box." }
]
}
}
}
If you have a content/_landing.md file, its prose body renders between
the feature grid and the trust strip. Treat it as the "Why" section.
Full landing reference: config → site.landing.
Theme switching#
Three themes ship in the default template: auto (follow OS),
light, and dark. The topbar toggle cycles between them; the choice is
remembered in localStorage and applied before paint, so there's no theme
flash on subsequent loads.
If you want to ship a different default for first-time visitors, set
site.defaultTheme to light or dark. See
Theming for restyling beyond the defaults.
Static-site essentials#
The default template ships with sensible defaults for the things that matter:
- Light + dark themes from the same OKLCH palette.
- System fonts only — no
@font-face, no FOIT. - Build-time syntax highlighting via shiki; zero runtime JS for code.
- Pre-paint theme script (no flash on reload).
- Copy buttons injected client-side onto every code block.
- Responsive grid: sidebar drops first, then collapses on narrow viewports.
- Accessible: focus rings, semantic landmarks (
<header>,<main>,<aside>witharia-labels), and proper heading levels.
Everything is generated; nothing here is configurable for now. The theming guide covers what's customisable today.