Sites & Templates
A site block configures one output site — its template, title, theme, and navigation. A document can declare several sites; each page joins one or more via its sites field.
§ 1Fields
| Property | Type | Required | Description |
|---|---|---|---|
| name | identifier | no | Optional site name (the label slot). Required with more than one site; names the output subdirectory referenced by Page.sites. |
| root | bool | no | When true, this site renders at / (others go under /<site>/). |
| default_template | symbol | no | :webpage (header + top nav), :book (sidebar + TOC), :presentation (slide deck), or :ai_skill (skill folder, Markdown-only — build with wcl wdoc skill). |
| title | utf8 | no | Site title shown in the header / sidebar. |
| summary | utf8 | no | One-line site description. Surfaced by included_sites(...) (not rendered by the built-in templates). |
| icon | utf8 | no | Path to a favicon image (svg/png/ico), resolved relative to the document and copied into _wdoc/. Absent ⇒ a default WCL icon is used. |
| stylesheets | list<utf8> | no | External stylesheet hrefs added to every page <head> as <link rel="stylesheet"> (verbatim — URL, copied asset, or shipped file). |
| scripts | list<utf8> | no | Script srcs added to every page <head> as deferred <script src=… defer> (verbatim hrefs). |
| fonts | list<utf8> | no | Web-font stylesheet hrefs added to every page <head> as <link rel="stylesheet"> (e.g. a Google Fonts URL). |
| assets | list<utf8> | no | Folders copied verbatim (recursively) into the site output — e.g. a Vite dist/. Resolved relative to the document; reference copied files by their output path. |
| theme_toggle | bool | no | When true, adds a light/dark toggle button. |
| search | bool | no | When true, adds a client-side full-text search box (book and webpage templates). |
| theme | symbol | no | Symbol naming a colour theme block (:nord …) — see Styling. |
| accent | symbol | no | Symbol naming the accent hue (:red..:pink); default :blue. |
| ui_theme | symbol | no | UI theme for wf_* wireframe elements (the mocked app's theme), separate from the document theme. Falls back to theme. |
| ui_accent | symbol | no | Accent hue for wf_* wireframe elements; falls back to accent. |
| ui_mode | symbol | no | Mode for wf_* wireframe elements — :dark (default) or :light. |
Child blocks
| Slot | Accepts | Multiple | Description |
|---|---|---|---|
| toc | toc | no | Chapter tree for the book template. |
| menu | menu | no | Top navbar entries for the webpage template. |
| sidebar_footer | sidebar_footer | no | Pinned buttons at the bottom of the book sidebar. |
| deck | deck | no | Slide grid for the presentation template. |
| skill | skill | no | Skill metadata for the :ai_skill target — populates SKILL.md's front matter. |
§ 2webpage template
Hugo-style site header, sticky top navbar built from menu, and a reading column.
site marketing {
default_template = :webpage
title = "My project"
root = true
theme = :nord
menu {
item "Home" { page = index }
item "Docs" { href = "docs/" }
item "More" {
item "About" { page = about }
item "Contact" { page = contact }
}
item "Source" { href = "https://github.com/example/proj" }
}
}
Menu items use page = <name> for in-site links (validated against pages in this site) or href = "…" for external or cross-site URLs. Nested items become dropdown groups.
§ 3book template
mdBook-style fixed left sidebar with nested chapters and current-chapter highlight; reading column on the right.
site docs {
default_template = :book
title = "Project Docs"
theme = :nord
theme_toggle = true
toc {
chapter "Intro" { page = index }
chapter "Guide" {
chapter "Setup" { page = setup }
chapter "First run" { page = first_run }
}
}
}
Chapters nest to any depth. A chapter with no page = is a grouping heading. A chapter pointing at an unknown page is a build error.
A sidebar_footer block pins one or more launcher buttons to the very bottom of the book sidebar — handy for a reference index or an external link you want reachable from every page without crowding the chapter tree. Each button takes an inline label, a page (internal, validated → <page>.html) or href (raw/external URL; page wins), and an optional icon (an icon name like lucide.chart-network). The built-in book template renders the button icon-only, using the label as its hover tooltip / accessible name (and as visible text when no icon is given). A button linking to an unknown page is a build error.
site docs {
default_template = :book
title = "Project Docs"
toc { chapter "Intro" { page = index } }
sidebar_footer {
button "Reference" { page = reference_page icon = "lucide.chart-network" }
button "Source" { href = "https://example.com/repo" }
}
}
Templates see the resolved buttons as TemplateCtx.footer (each with its icon already rendered to SVG markup); the built-in book template draws them via the wdoc_part_sidebar_footer(c) part.
§ 4Search
Set search = true on a site to add client-side full-text search. The build writes a per-page text index to _wdoc/search-index.json (page title from its first h1, plus the page body text) and the book and webpage templates render a search box — in the sidebar and the nav respectively — backed by a small bundled script. Typing ranks pages by title and body matches and shows the top hits with a context snippet; Enter opens the first hit, Escape clears.
Served, not opened
The widget fetches the index over HTTP, so search works when the site is hosted (or under wcl wdoc serve), not when a page is opened directly from disk.
§ 5presentation template
A reveal.js-style slide deck: the whole site renders into a single index.html, navigated with the keyboard. The deck block lays out the 2-D grid — each section is a column, its slides are rows — and each slide names a page that belongs to this site.
site talk {
default_template = :presentation
title = "My talk"
theme = :catppuccin
deck {
section "Intro" {
slide title
slide agenda
}
section "Main" {
slide topic
}
}
}
Keyboard navigation
← → move between sections, ↑ ↓ between the slides within a section, Space / PageDown step forward (revealing fragments, then advancing), s toggles speaker notes, f fullscreen. A progress bar, slide counter, and nav-hint arrows update as you go.
Each slide must sit on its own line (like li / chapter). A slide pointing at an unknown page is a build error. Two in-slide blocks are deck-specific:
| Block | Renders |
|---|---|
| fragment { … } | A step-reveal group — its content stays hidden until the presenter advances with Space |
| notes { … } | Speaker notes — hidden in the deck, shown in the overlay toggled with s |
page topic {
h2 "Key points"
fragment { p "Revealed on the first Space" }
fragment { p "…then this one" }
notes { p "Reminder: mention the benchmark numbers." }
}
§ 6Custom templates
The three built-ins are not special: a template is just a function from a TemplateCtx to a list of HTML fundamentals, and the document schema accepts your own. Declare a template <name> { render = fn(c: TemplateCtx) -> list<HtmlFundamental> … } in your document and select it with a site's default_template (e.g. :blog) or a page's template field.
The renderer hands render this context:
| Property | Type | Required | Description |
|---|---|---|---|
| content | utf8 | yes | The page's rendered body HTML — embed it with Raw. |
| title | utf8 | yes | Site title, or the page name when the site sets no title. |
| page_name | utf8 | yes | The current page's name. |
| regions | list<Region> | yes | The page's named content regions (its region "name" { … } blocks), each already rendered to HTML. Pull one out by name with wdoc_region(c, "name"). Empty on pages with no regions. |
| pages | list<PageRef> | yes | Every page in the current site, in source order (for building navigation). |
| toc | list<TocEntry> | yes | The book TOC tree; flat (one entry per page) when the site declares no toc. |
| on_this_page | list<OnPageHeading> | yes | The current page's h2/h3 headings, for the book's 'on this page' rail. Empty when the page has none. |
| menu | list<MenuEntry> | yes | The site's curated navbar menu; empty when the site declares no menu. |
| footer | list<FooterButton> | yes | Pinned sidebar-footer buttons (the site's sidebar_footer); empty when none declared. Each carries its icon already rendered to SVG markup. |
| deck | list<DeckSection> | yes | The resolved presentation deck; empty unless the presentation template renders the whole site at once. |
| theme_toggle | bool | yes | Whether to render a light/dark toggle (the site's theme_toggle flag). |
| search | bool | yes | Whether to render the site search box (the site's search flag). |
| home_href | utf8 | yes | Relative href back to the root site; empty when this IS the root (or a single-site build). |
| home_title | utf8 | yes | The root site's title, for the back-link text. |
You rarely build a template from raw fundamentals. The stdlib exposes its own chrome as composable parts — each a function returning list<HtmlFundamental> — plus one wdoc_*_layout function per built-in that is its whole default composition. Parts resolve by bare name once you import <wdoc.wcl>.
| Part | Returns |
|---|---|
| wdoc_webpage_layout(c) | The whole webpage body: css + header + navbar + content + menu script |
| wdoc_part_webpage_css() | The webpage <style> (header / nav / content card) |
| wdoc_part_header(c) | The site-title <header> |
| wdoc_part_navbar(c) | The top <nav> — back-link, search, and the menu (or a flat page list) |
| wdoc_part_content(c) | The page content in the reading card <main> |
| wdoc_part_menu_tree(c.menu) | Just the nested <ul class="menu">, no surrounding nav |
| wdoc_book_layout(c) | The whole book body: css + sidebar + content-with-pagination |
| wdoc_part_sidebar(c) | The fixed book-sidebar — title, back-link, search, toggle, TOC, footer, scroll |
| wdoc_part_sidebar_footer(c) | The pinned book-sidebar-footer buttons (empty unless sidebar_footer declared) |
| wdoc_part_book_content(c) | The reading column <main> plus prev/next pagination |
| wdoc_part_toc_tree(c.toc) | Just the nested <ul class="book-toc"> |
| wdoc_part_pagenav(c.toc) | The bottom prev/next pagination links |
| wdoc_presentation_layout(c) | The whole deck body: css + slide grid + chrome |
| wdoc_part_deck(c) | The <div class="deck"> slide grid |
| wdoc_part_search_box(c.search) | The search box + its style (empty unless enabled) |
| wdoc_part_theme_toggle(c.theme_toggle) | The light/dark toggle button + script (empty unless enabled) |
| wdoc_part_home_link(c, cls) | The back-to-root link for sub-sites |
§ 6.1Extend a built-in
To keep a whole built-in but add your own chrome, call its layout and append parts. This is the full webpage plus a footer:
template blog {
render = fn(c: TemplateCtx) -> list<HtmlFundamental>
flatten([
wdoc_webpage_layout(c),
[ HtmlFundamental::Element {
tag: "footer", id: none, class: ["site-footer"], attrs: none,
children: [ HtmlFundamental::Raw { html: "© 2026 — built with wdoc" } ],
} ],
])
}
site myblog { default_template = :blog title = "My blog" }
§ 6.2Rebuild from parts
To replace a region, copy the layout's body and swap one part — here a custom masthead in place of wdoc_part_header, keeping the stdlib navbar and content. Omit a part to drop that region entirely (no nav, say, for an app home page).
template app_home {
render = fn(c: TemplateCtx) -> list<HtmlFundamental>
flatten([
wdoc_part_webpage_css(), // keep the stdlib styling…
[ HtmlFundamental::Element { // …a hero masthead instead of wdoc_part_header
tag: "header", id: none, class: ["hero"], attrs: none,
children: [ HtmlFundamental::Raw { html: c.title } ],
} ],
wdoc_part_navbar(c), // …reuse the stdlib nav
wdoc_part_content(c), // …standard content <main>
])
}
Parts resolve by bare name
Template parts are plain functions reached by name through import <wdoc.wcl> — they are not wdoc::-qualified. Don't define a let of your own named wdoc_part_* or wdoc_*_layout, or it will shadow the stdlib one.