Websites
The website template is a Hugo-style variant of the page template for building marketing and product sites: you bring the *design* (the HTML/CSS — a Figma export, a Claude artifact, a hand-built theme, an externally-built bundle) and slot wdoc-rendered content into it. The split is deliberate — HTML/CSS for the fancy shell, wdoc for the words.
Deploy the built-in template with default_template = :website, or author your own template (recommended for a real design — see below). Scaffold a ready-to-edit project with wcl init website ./my-site.
§ 1Three new pieces
On top of the ordinary template mechanism (Sites & Templates), the website workflow adds:
- Named regions — a page declares region "hero" { … } blocks whose wdoc bodies render independently. A layout pulls each out by name with wdoc_region(c, "hero") (returns "" when the page declares no such region). Everything outside a region is the default c.content.
- Head assets — the site block's stylesheets / scripts / fonts fields add <link> / <script> to every page <head>; the assets field copies a whole folder (a Vite/webpack dist/) into the output verbatim. A layout can also emit head content itself with the wdoc_head_* helpers.
- Raw-HTML layouts — author the shell as a heredoc with ${…} slots and paste your design in, instead of building HtmlFundamental trees by hand.
§ 2Authoring a layout
A template is a function returning fundamentals. For a website, return one Raw fundamental holding the design, with the slots interpolated — ${c.title}, ${c.content}, and ${wdoc_region(c, "name")} for each region. Use the interpolating heredoc form ($<<HTML):
template site_main {
render = fn(c: TemplateCtx) -> list<HtmlFundamental>
flatten([
// Head content the layout owns (the site's `stylesheets` /
// `scripts` add the rest).
wdoc_head_meta("viewport", "width=device-width, initial-scale=1"),
[ HtmlFundamental::Raw { html: $<<HTML
<header class="nav">
<a class="brand" href="index.html">${c.title}</a>
<nav><a href="index.html">Home</a></nav>
</header>
<section class="hero">${wdoc_region(c, "hero")}</section>
<main class="content">${c.content}</main>
<footer class="foot">${wdoc_region(c, "footer")}</footer>
HTML
} ],
])
}
Heredoc terminator
The closing HTML delimiter must sit alone on its line — HTML } ], on one line is an unterminated heredoc. Put } on the next line.
§ 3Authoring content with regions
A page joined to a :website site renders normally; wrap any part you want to slot separately in a region. Everything else becomes ${c.content}.
page index { sites = [:web] start = true
region "hero" {
h1 "Build something great"
p "The design lives in CSS; the words live here."
}
h2 "About this page"
p "Everything outside a region lands in the default content slot."
region "footer" {
p "© Built with WCL + wdoc."
}
}
§ 4Wiring the design's assets
Ship the design's stylesheet and script with the site. assets copies a folder verbatim into the output (resolved relative to the document); stylesheets / scripts / fonts link files into every page <head> (hrefs are emitted verbatim, so they may be a copied asset, a shipped file, or a URL):
site web {
default_template = :site_main
title = "Acme"
root = true
assets = ["assets"] // copies ./assets/ → _site/assets/
stylesheets = ["assets/site.css"] // <link> in every <head>
scripts = ["assets/app.js"] // deferred <script> in every <head>
fonts = ["https://fonts.example/Inter.css"]
}
Because assets copies the folder unchanged, the same field takes an externally-built bundle — point it at a Vite or webpack dist/ and reference the hashed files it emits.
§ 5Head from the layout
When a region or the site fields aren't enough, a layout (or a part it calls) can emit <head> content directly by returning a Head fundamental at the top level of its list. The wdoc_head_* helpers wrap the common cases:
| Helper | Adds to the page head |
|---|---|
| wdoc_head_stylesheet(href) | <link rel="stylesheet" href> |
| wdoc_head_script(src) | deferred <script src> |
| wdoc_head_font(href) | a web-font <link rel="stylesheet"> |
| wdoc_head_meta(name, content) | <meta name content> |
| wdoc_head_raw(html) | verbatim head HTML |
Top level only
A Head is hoisted only when returned at the layout's top level. A Head nested inside the body renders to nothing — it never leaks into <body>.
§ 6The default layout
default_template = :website uses a built-in, theme-aware shell (sticky header with the title + menu, optional banner and hero regions, the content with an optional sidebar region, and a footer region). When the site sets search = true / theme_toggle = true, the header grows a right-side controls cluster with the search box and the standard light/dark toggle; with neither flag the header is unchanged. It's a fine starting point and a reference to copy: wdoc_website_layout(c) is its whole body, composed from wdoc_part_website_css(), website_header(c), and the region parts.
§ 7Scaffold
wcl init website ./my-site generates a complete project: main.wcl (the site + head assets), theme.wcl (a custom raw-HTML layout), components.wcl (starter landing components — hero, feature cards, steps, footer — to edit or replace), content.wcl (the page, built from those components + regions), and assets/site.css + assets/app.js. Build it with wcl wdoc build my-site/main.wcl --out my-site/_site.
§ 8Site fields
| 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. |