Documents, pages and sites
wdoc renders a WCL document into something a reader opens — a website, a folder of Markdown, a PDF. This chapter is about the HTML build, because that is the one whose structure the other targets follow. The document itself is not a special file format. It is the same tree of fields and blocks that Documents, fields and blocks describes, read by the same parser and checked by the same schema engine. One import is what makes it a wdoc document.
This chapter covers that import, the two blocks that decide the shape of the output — page and site — the navigation blocks a site holds, and the directory a build writes. It is the map of the whole wdoc half of this book: every later chapter fills in what goes inside a page.
Every file, command and listing below was run before it was written down. Type them and compare.
§ 1The entry document
A build starts at one file, the entry document, and follows its import statements. Everything the build can see — pages, sites, styles, your own block types — has to be reachable from there.
The entry document opens with import <wdoc.wcl>. The angle brackets mean a system import: the file is not on your disk, it is the standard library embedded in the wcl binary. That one line brings the whole wdoc vocabulary into scope — the @document schema that makes page and site legal at the top level, every content block, the built-in templates and themes. Namespaces and imports covers the two import forms.
Here is a complete site — three files:
# main.wcl — the entry document.
import <wdoc.wcl>
site handbook {
default_template = :book
title = "Field handbook"
theme = :nord
theme_toggle = true
toc {
chapter "Welcome" { page = index }
chapter "Operations" {
chapter "Deploying" { page = deploying }
chapter "Rolling back" { page = rolling_back }
}
}
}
import "./pages/index.wcl"
import "./pages/operations.wcl"
page index {
title = "Welcome"
start = true
h1 "Field handbook"
p "Start with [Deploying](deploying)."
}
page deploying {
title = "Deploying"
h1 "Deploying"
h2 "Before you start"
p "Check the [rollback plan](rolling_back#the-two-minute-rollback)."
}
page rolling_back {
title = "Rolling back"
h1 "Rolling back"
h2 "The two-minute rollback"
p "Back to [deploying](deploying)."
}
Build it:
$ wcl wdoc build main.wcl --out _site
wrote 3 pages
Note what the two imported files do not carry. No import of their own, no site name, no template. A page block is legal in pages/operations.wcl only because main.wcl imported the wdoc schema, and the pages join the handbook site without saying so because the document declares exactly one. Splitting a document across files is a filing decision, not a structural one — see Namespaces and imports.
One import, or nothing parses
Drop the import <wdoc.wcl> line and the failure is not a missing feature, it is a schema violation: block kind 'page' has no @block or @table declaration. Nothing declares page, so the document has no root schema to stand on. That is the same rule Documents, fields and blocks states — there is no schema-free document, and a wdoc document gets its schema from the standard library.
§ 2Pages
A page block is one rendered page. Its label is the page name, its body is the content, and its fields are the handful of facts the build needs about it.
| Field | Type | What it does |
|---|---|---|
| the label | identifier | The page name: the output filename and the link target. See Page names are routes. |
| title | utf8? | The browser tab title. Falls back to the page name. |
| template | symbol? | Wrap this one page in a named template, overriding the site's default_template. See Templates and layouts. |
| sites | list<symbol>? | Which sites this page belongs to. Required once the document declares more than one. |
| start | bool? | Mark this page as the site's landing page. See The start page. |
| frontmatter | a child block | YAML front matter for the Markdown target only. See Front matter. |
| id | identifier? | Declared, and no built-in target renders it. An id you want in the output goes on a content block, not on the page. |
Everything else inside a page is content. Which blocks are legal there is not a list the page type spells out — it is the interface ContentBlock, so any block that lowers to page content fits, including the ones you declare yourself. Writing your own blocks covers that end of it.
§ 2.1Page names are routes
The page's label is not decoration. It is the route. A page named deploying becomes deploying.html in an HTML build and deploying.md in a Markdown one, and [the plan](deploying) anywhere in the site resolves to it.
Because the name becomes a filename, it is constrained. A route must be non-empty and built only from A-Za-z0-9_-. A bare identifier label always passes, which is why you rarely meet the rule; a quoted label lets you break it:
$ wcl wdoc build a.wcl --out _o
page route "getting started" is not slug-safe — a generated page name must be
non-empty and contain only [A-Za-z0-9_-]; build one with e.g.
`to_lower(replace(s, " ", "-"))`
Write page "getting-started" instead and it builds to getting-started.html. The quotes are needed there only because a hyphen is not legal in a bare identifier — see Blocks and labels.
Routes are unique per site. Two pages of one site sharing a name would silently overwrite each other's file, so the build refuses:
$ wcl wdoc build b.wcl --out _o
site "default": duplicate page "intro"
Two different sites may each have a page named index, because each site writes into its own directory.
Links between pages are ordinary markdown-style links in prose, and the target is a page name rather than a path. Add #anchor to land on a heading; the anchor is the heading text lowercased, with every run of other characters collapsed to a single hyphen — "The two-minute rollback" becomes the-two-minute-rollback. Write site_name:page_name to cross into another site of the same document. Text and formatting covers the inline patterns in full.
A page name is checked; an anchor is not
A link whose page name matches nothing fails the build — link to unknown page 'deployment' — so renaming a page cannot quietly break navigation. The #fragment half is passed through untouched. A link to a heading that no longer exists still builds, and still lands the reader at the top of the right page.
§ 2.2The start page
/ has to serve something. Two mechanisms decide what, in this order:
- The page with start = true is copied to index.html in addition to its own <name>.html. At most one page per site may set it; a second one fails the build, naming both.
- Otherwise a page literally named index already wrote index.html.
The first wins outright: a site with both a start page and a page named index serves the start page at /, because the copy runs after the pages are written. The index page is still there under its own name, and nothing warns you. Pick one mechanism per site.
With neither, a single-site build produces no index.html at all — the pages are there, the directory has no landing page, and a server will show you its own listing. That is worth checking before you deploy. In a multi-site build the missing case is filled instead: any site of that build with no landing page gets a small generated index.html that redirects to its first page, so / and every /<site>/ go somewhere.
§ 2.3Three names for one page
A page carries up to three pieces of text that all read like a title, and they drive different surfaces. Keeping them straight saves an afternoon:
| The name | Where it is written | What it drives |
|---|---|---|
| The route | the page's label | The filename and every […](page) link |
| title | the page's title field | The browser tab. <title> is <page title> — <site title>; the suffix is dropped when the site sets no title, or sets the same one |
| The first h1 | the page body | The label the built-in navigation shows for the page when no toc names it |
Run the difference. Give a page title = "Deploy (tab)" and h1 "Deploying a release", in a book site with no toc: the browser tab reads Deploy (tab) — Handbook, and the sidebar entry reads Deploying a release. Add a toc, and the sidebar reads neither — it reads the chapter label, which is a fourth piece of text under the site's control rather than the page's. That is the point of a toc: the book decides how it lists its own chapters.
§ 2.4Front matter
A frontmatter child block attaches YAML to a page for the Markdown target only. The HTML and PDF targets pass over it. Use it when the .md files feed something that reads a YAML header — a static-site generator, an indexer, an agent.
import <wdoc.wcl>
page release_notes {
title = "Release notes"
frontmatter {
title = "Release notes"
tags = ["release", "changelog"]
draft = false
}
h1 "Release notes"
p "What changed in 1.4."
}
$ wcl wdoc markdown release_notes.wcl --out _md
wrote 1 page
$ cat _md/release_notes.md
---
title: Release notes
tags:
- release
- changelog
draft: false
---
# Release notes
What changed in 1.4.
The block is open: it declares no fields of its own, so any key = value you write is accepted and serialized in source order. Strings, numbers, booleans, symbols, lists and nested records all get proper block YAML. That openness is the one thing to respect — a typo in a key is not a schema violation here, it is a key in your YAML. Output targets covers the Markdown backend.
§ 3Sites
A site block is one output site: a directory of pages that share a template, a title, a theme and a navigation tree. It says how a set of pages is presented; the pages themselves say nothing about presentation.
site handbook {
default_template = :book
title = "Field handbook"
theme = :nord
theme_toggle = true
search = true
}
| Field | What it does | Covered in |
|---|---|---|
| the label | The site name. Optional for one site; required for two or more. | One document, several sites |
| root | Render this site at the output root instead of a subdirectory. | One document, several sites |
| default_template | :book, :webpage, :presentation, or your own. | Templates and layouts |
| title | The site title, shown in the chrome and in every page's <title>. | This chapter |
| theme / accent / theme_toggle | Colour scheme, accent hue, light/dark toggle. | Themes and styling |
| search | Client-side full-text search over this site's pages. | Templates and layouts |
| icon | A favicon path, resolved relative to the document and copied into the output. | This chapter |
| assets | Folders copied verbatim into the site's output. | The assets folder |
| stylesheets / scripts / fonts | Extra <head> links, emitted verbatim. | Websites |
| toc / menu | The navigation tree. | Navigation |
| sidebar_footer / deck | Pinned buttons and a slide grid. | Templates and layouts, Presentations |
That is the shape of the block, not the whole of it. A summary line describes the site to a parent document that lists it, and a ui_theme / ui_accent / ui_mode trio themes the mocked application inside a wireframe separately from the document around it — see Wireframes.
A document with no site block at all still builds. Every page renders bare — no template, no theme, its content straight into the <body>. That is the shortest useful wdoc document there is:
import <wdoc.wcl>
page index {
h1 "Hello"
p "One page, no site block."
}
§ 3.1One document, several sites
One document may declare several site blocks — a marketing front page and a handbook, say, sharing one set of styles and one build. Three rules come with the second site, and all three exist to stop a site you add later from silently changing a page you did not touch.
- Every site must be named. With one site the label is optional; with two, an unnamed one is a build error. The name is the output subdirectory and the thing a page points at.
- Every page must name its sites. sites = [:handbook], or sites = [:marketing, :handbook] for a page both show. A page that names none is a build error, because the site chooses the page's template — an untagged page would be re-templated by a site added later without the page changing. A name matching no site block is an error too.
- At most one site may set root = true. That site renders flat at the output root; the others render under <out>/<name>/. With no root site, the build generates a chooser index.html at the root that links to each one.
# main.wcl — two sites in one document.
import <wdoc.wcl>
site marketing {
root = true
default_template = :webpage
title = "Acme"
theme = :nord
assets = ["brand"]
stylesheets = ["brand/extra.css"]
menu {
item "Home" { page = home }
item "Product" { page = product }
item "Docs" { href = "handbook/" }
item "More" {
item "Source" { href = "https://example.com/acme" }
}
}
}
site handbook {
default_template = :book
title = "Acme handbook"
theme = :nord
toc {
chapter "Welcome" { page = index }
chapter "Operations" {
chapter "Deploying" { page = deploying }
}
}
}
import "./pages/marketing.wcl"
import "./pages/handbook.wcl"
page home {
sites = [:marketing]
start = true
title = "Acme"
h1 "Acme"
p "See the [handbook](handbook:index)."
}
page product {
sites = [:marketing]
title = "Product"
h1 "Product"
}
page index {
sites = [:handbook]
title = "Welcome"
h1 "Acme handbook"
p "Back to [the site](marketing:home)."
}
page deploying {
sites = [:handbook]
title = "Deploying"
h1 "Deploying"
}
One more file, and it is not WCL: brand/extra.css, beside main.wcl. The assets field ships the folder it sits in — The assets folder below.
[handbook](handbook:index) is the cross-site form. It is checked exactly as an in-site link is: an unknown site, or an unknown page within a known site, fails the build. The build works out the relative path between the two directories for you, so the page on the other side links back with a plain [the site](marketing:home) and gets ../home.html.
You can also build one site on its own with --site, which renders it flat at --out as though it were the only one:
$ wcl wdoc build main.wcl --out _one --site handbook
wrote 2 pages
§ 4Navigation
A site holds its navigation tree, not its pages. Two blocks describe one, and they are not alternatives to each other: they belong to different templates.
§ 4.1toc and chapter
A toc is a book's table of contents — the sidebar the :book template draws, and the reading order that gives every page its previous/next links. It holds chapter entries, nested to any depth.
toc {
chapter "Welcome" { page = index }
chapter "Operations" {
chapter "Deploying" { page = deploying }
chapter "Rolling back" { page = rolling_back }
}
}
A chapter's label is the text in the sidebar, and page = <name> links it to a page of this site. Both parts are optional in the useful direction: a chapter with no page is a grouping heading. "Operations" above is a heading with no page behind it — it is a place in the tree, not a route, and clicking it opens its children rather than navigating.
That is the one thing a flat page list cannot express, and it is why a book with more than a handful of pages wants a toc. It also lets the book order and name its chapters independently of the pages themselves: source order stops mattering, and the sidebar reads the chapter label rather than the page's h1.
A chapter naming a page that this site does not have is a build error. Point the handbook's first chapter at home — a page of the other site in the document above — and the build stops:
$ wcl wdoc build main.wcl --out _site
unknown template "toc chapter links to unknown page "home""
§ 4.2menu and item
A menu is a website navbar — the top strip the :webpage template draws. It holds item entries, and an item nests into a dropdown.
menu {
item "Home" { page = home }
item "Product" { page = product }
item "Docs" { href = "handbook/" }
item "More" {
item "Source" { href = "https://example.com/acme" }
}
}
An item's label is its text. It links one of two ways, and they are not equivalent:
- page = <name> names a page of this site. It is checked — an unknown name fails the build — and rendered as <name>.html.
- href = "…" is a raw URL, emitted verbatim and checked by nobody. Use it for an external site, or for a sub-site directory such as "handbook/".
- page wins if you write both.
- An item with children and neither page nor href is a plain dropdown label. An item with none of the three is plain text.
§ 4.3Which navigation block to write
The two blocks look alike and are not interchangeable. This is the comparison:
| Block | Read by | Shape | Page links checked | External links | If absent |
|---|---|---|---|---|---|
| toc | the :book template | A tree; a page-less chapter is a grouping heading | Yes | No — a chapter links a page or nothing | The sidebar falls back to a flat list of every page in the site, labelled by each page's first h1 |
| menu | the :webpage template | A tree; nested items are dropdowns | Yes, for page | Yes, via href | The navbar falls back to a flat list of every page in the site |
Neither is required, and the fallback is genuinely usable for a small site. Reach for the explicit block when source order stops being the reading order, when a heading has to group pages that have no page of their own, or when navigation has to point somewhere the document does not own.
Both blocks live in the site, and describe only that site
A toc and a menu are children of a site block, so a multi-site document writes one of each per site and each names only its own pages. A chapter or an item naming a page that belongs to another site fails, even though the page exists in the document — from this site's point of view it is not there. Cross-site navigation is a link, either site:page in prose or an href in a menu item.
Two more site-level navigation blocks exist, and belong to their own chapters: sidebar_footer pins launcher buttons under the book sidebar (Templates and layouts), and deck arranges pages into a slide grid (Presentations).
§ 5The output tree
Build the two-site document above and this is what lands in --out:
$ wcl wdoc build main.wcl --out _site
wrote 4 pages
$ find _site -type f -not -name '*.woff2' | sort
_site/brand/extra.css
_site/handbook/deploying.html
_site/handbook/index.html
_site/handbook/_wdoc/favicon.svg
_site/home.html
_site/index.html
_site/product.html
_site/_wdoc/favicon.svg
The -not -name '*.woff2' is there to keep the listing readable. Each _wdoc/ also holds thirteen font files: the book typography ships with any site at all, themed or not, and only a document with no site block skips it.
Reading it top to bottom: brand/ is the copied assets folder, handbook/ is the sub-site, and home.html / product.html / index.html are the root site rendering flat beside it — index.html being the copy of home.html that start = true produced. Four rules explain that shape, and they hold for every build:
- One .html per page, named after the page's route, in its site's directory.
- The root site renders flat at --out; every other site renders under <out>/<name>/. A single-site document, and a --site build, are the flat case.
- index.html is a copy, not a redirect, when a start page produced it. The page stays reachable at its own name as well.
- Each site owns a _wdoc/ directory holding what its own pages reference: the favicon, the book typography, the shared icon sprite, the search index, and the small player scripts an interactive block pulls in. The last three are paid for only on use — there are no player scripts in the listing above because no page used one, and no icon sprite because no page drew an icon.
§ 5.1The assets folder
assets on a site names folders to copy into that site's output, recursively and verbatim. It is the seam for anything an external tool built — a bundler's dist/, a folder of downloads, brand images the document does not reference block by block.
That is the pair of fields the marketing site above carries, with everything else stripped away:
site marketing {
root = true
default_template = :webpage
title = "Acme"
assets = ["brand"]
stylesheets = ["brand/extra.css"]
}
Each entry is a path relative to the entry document, and it keeps its name in the output: brand/ beside main.wcl becomes _site/brand/. Nothing rewrites what is inside, so reference a copied file by the output path it landed at — which is what stylesheets = ["brand/extra.css"] does above, adding a verbatim <link rel="stylesheet" href="brand/extra.css"> to every page of the site.
assets is not how you use an image
An image block resolves its own path and copies the file into _wdoc/ for you, with the <img> pointed at the copy. Reach for assets for the files no block references — the bundle, the archive, the robots.txt. See Images, videos and file assets.
§ 5.2The output tree is relocatable
Every href the build emits is relative. A page links a sibling as deploying.html, its assets as _wdoc/favicon.svg, a sub-site as handbook/index.html, and the way back out as ../home.html. Nothing is written from the server root.
So the tree does not care where it is served from. _site works at the domain root, at /docs/, at /preview/pr-142/, and moved to another directory on disk — no rebuild, no base-path setting, because there is no base path to set. Two things still need a server rather than a directory opened straight from disk: full-text search and anything else that fetches a file, since a browser will not fetch from a page that was opened as a local file.
§ 5.3A build never wipes its output
--out is created if it is missing and never cleared. A build writes the files it produced and leaves everything else alone.
That is deliberate — the output directory may hold things the build did not make, and destroying a directory a user named on the command line is not a thing a build tool should do. It has one consequence you have to hold in your head. Go back to the first example, rename rolling_back to rollback — in the page, in the toc entry and in the link that points at it — and rebuild into the same _site:
$ wcl wdoc build main.wcl --out _site
wrote 3 pages
$ ls _site/*.html
_site/deploying.html
_site/index.html
_site/rollback.html
_site/rolling_back.html
rolling_back.html is still there. It is a stale copy of a page that no longer exists, it is still linkable, and no later build will remove it. A deleted page behaves the same way.
Wipe it yourself when it must be exact
Where the output has to match the document exactly — a publish step, a link-checked CI job — delete the directory yourself before you build. One rm -rf in the recipe, above the wcl wdoc build line, is the whole fix. A dev-server rebuild does not need it: a stale page is only a page nothing links to.
§ 6Where to go next
- Templates and layouts — the :book / :webpage / :presentation built-ins, what a template is handed, and writing your own.
- Websites — the website template, its named slots and its <head> assets.
- Themes and styling — theme, accent, and the class system behind them.
- Output targets — one document, three targets: HTML, Markdown and PDF.
- Text and formatting — the inline patterns behind [a link](page), emphasis and inline code.
- Visibility — keeping a block out of one site, or one target.
- The CLI — wdoc build, serve, markdown, pdf and the rest in full.