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.wclwcl
# 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"
pages/index.wclwcl
page index {
  title = "Welcome"
  start = true

  h1 "Field handbook"
  p "Start with [Deploying](deploying)."
}
pages/operations.wclwcl
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:

text
$ 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.

FieldTypeWhat it does
the labelidentifierThe page name: the output filename and the link target. See Page names are routes.
titleutf8?The browser tab title. Falls back to the page name.
templatesymbol?Wrap this one page in a named template, overriding the site's default_template. See Templates and layouts.
siteslist<symbol>?Which sites this page belongs to. Required once the document declares more than one.
startbool?Mark this page as the site's landing page. See The start page.
frontmattera child blockYAML front matter for the Markdown target only. See Front matter.
ididentifier?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:

text
$ 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:

text
$ 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 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 nameWhere it is writtenWhat it drives
The routethe page's labelThe filename and every […](page) link
titlethe page's title fieldThe 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 h1the page bodyThe 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.

release_notes.wclwcl
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."
}
text
$ 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.

wcl
site handbook {
  default_template = :book
  title            = "Field handbook"
  theme            = :nord
  theme_toggle     = true
  search           = true
}
FieldWhat it doesCovered in
the labelThe site name. Optional for one site; required for two or more.One document, several sites
rootRender 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
titleThe site title, shown in the chrome and in every page's <title>.This chapter
theme / accent / theme_toggleColour scheme, accent hue, light/dark toggle.Themes and styling
searchClient-side full-text search over this site's pages.Templates and layouts
iconA favicon path, resolved relative to the document and copied into the output.This chapter
assetsFolders copied verbatim into the site's output.The assets folder
stylesheets / scripts / fontsExtra <head> links, emitted verbatim.Websites
toc / menuThe navigation tree.Navigation
sidebar_footer / deckPinned 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:

hello.wclwcl
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.

main.wclwcl
# 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"
pages/marketing.wclwcl
page home {
  sites = [:marketing]
  start = true
  title = "Acme"

  h1 "Acme"
  p "See the [handbook](handbook:index)."
}

page product {
  sites = [:marketing]
  title = "Product"

  h1 "Product"
}
pages/handbook.wclwcl
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:

text
$ wcl wdoc build main.wcl --out _one --site handbook
wrote 2 pages

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.

wcl
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:

text
$ wcl wdoc build main.wcl --out _site
unknown template "toc chapter links to unknown page "home""

A menu is a website navbar — the top strip the :webpage template draws. It holds item entries, and an item nests into a dropdown.

wcl
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:

§ 4.3Which navigation block to write

The two blocks look alike and are not interchangeable. This is the comparison:

BlockRead byShapePage links checkedExternal linksIf absent
tocthe :book templateA tree; a page-less chapter is a grouping headingYesNo — a chapter links a page or nothingThe sidebar falls back to a flat list of every page in the site, labelled by each page's first h1
menuthe :webpage templateA tree; nested items are dropdownsYes, for pageYes, via hrefThe 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:

text
$ 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:

§ 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:

wcl
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:

text
$ 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