Visibility
One document renders to three outputs: a website, a PDF and a folder of Markdown. It can also declare several sites, and lay each one out with a different template. A block that belongs in one of those and not in another says so with a decorator, rather than with a duplicate document. @only includes it; @except excludes it.
This chapter covers both decorators, their three axes, the rule that joins the axes, and the one place where visibility is not a preference but a requirement: waiving a native block on a target that cannot render it.
§ 1Two decorators, three axes
@only and @except attach to any block instance. Each takes up to three optional list<symbol> arguments:
@only(sites = [:handbook], templates = [:book], backends = [:html])
callout "Browse the search index" { class = ["tip"] body = "Type in the box." }
@except(backends = [:pdf])
video "assets/tour.mp4" { }
| Axis | Values | The current value is |
|---|---|---|
| sites | the label on a site block | the name of the site being rendered |
| templates | any template name — see below | the site's default_template symbol |
| backends | :html, :pdf, :markdown | the output target this build produces |
Three backends, not four. The :skill target left with the skill folder generator, so the axis names exactly the three outputs wcl wdoc still produces. wcl wdoc serve is an HTML build, so it matches :html.
A block with neither decorator renders everywhere. That is the default and it is what almost every block should be.
§ 2The rule
Two sentences decide every case, and they are worth reading closely because they are not symmetric.
- Within an axis, the values are OR'd. backends = [:pdf, :markdown] matches a PDF build or a Markdown build.
- Across axes, they are AND'd. @only(sites = [:docs], backends = [:html]) matches only when the site is docs and the target is HTML.
An axis you do not write does not constrain. @except(backends = [:pdf]) says nothing about sites, so it applies in every site.
The two decorators then combine like this: a block renders when @only is absent or matches, and @except is absent or does not fully match.
render = (no @only or @only matches)
and (no @except or @except does NOT match)
Write both on one block and @except wins where they overlap, because it is a veto. @only(backends = [:html, :pdf]) with @except(sites = [:internal]) means HTML and PDF, but never in the internal site.
§ 2.1An axis whose value is unknown never matches
This is the one asymmetry to hold on to. A constrained axis with no current value fails to match rather than matching vacuously.
It shows up in two places. A document with one unnamed site has no site name, so @only(sites = [:anything]) hides the block everywhere. And a site that declares no default_template has no template kind, so @only(templates = [:book]) hides the block there too.
The effect differs by decorator, and that follows from the rule rather than from a special case. On an @only an unknown axis hides the block, because @only failed to match. On an @except it shows the block, because @except failed to match either.
§ 3The backends axis
This is the axis you reach for most. Save this two-page document:
import <wdoc.wcl>
site docs {
default_template = :book
title = "Docs"
toc { chapter "Notes" { page = notes } }
}
page notes {
start = true
h1 "Notes"
@only(backends = [:html])
p "HTML-ONLY"
@only(backends = [:markdown])
p "MARKDOWN-ONLY"
@except(backends = [:markdown, :pdf])
p "NOT-MD-NOT-PDF"
}
Build it twice and read the two outputs. The Markdown target keeps one paragraph:
$ wcl wdoc markdown main.wcl --out _md
wrote 1 page
$ cat _md/notes.md
# Notes
MARKDOWN-ONLY
The HTML target keeps the other two:
$ wcl wdoc build main.wcl --out _site
wrote 1 page
$ grep -o 'HTML-ONLY\|MARKDOWN-ONLY\|NOT-MD-NOT-PDF' _site/notes.html
HTML-ONLY
NOT-MD-NOT-PDF
Note the third paragraph. @except(backends = [:markdown, :pdf]) and @only(backends = [:html]) select the same set today, because there are exactly three targets. They do not say the same thing, and a fourth target would separate them. Write @only when the block is for that target, and @except when the block is wrong on that one.
Reach for @except when the reason is a limitation
A live search box, a pan-and-zoom control, an embedded player: each is an HTML idea, and each should carry @except naming the target it fails on rather than @only(backends = [:html]). The @except form keeps working when the block starts rendering somewhere new; the @only form silently keeps hiding it.
§ 4The sites axis
In a multi-site document, sites scopes a block to some of them. The values are site block labels:
site docs { default_template = :book title = "Docs" }
site blog { default_template = :webpage title = "Blog" root = true }
page notes {
sites = [:docs, :blog]
h1 "Notes"
p "This paragraph is in both sites."
@only(sites = [:docs])
callout "Internal detail" { class = ["note"] body = "Only the docs site shows this." }
}
There are two ways to keep content out of a site, and they work at different scales. A page names its sites with the ordinary sites field — a page belongs to sites, so that is data. @only(sites = …) scopes one block inside a page that several sites share. Reach for the field when the whole page is site-specific and for the decorator when one paragraph is.
The styling blocks (class, base, media, keyframes, font_face) carry their own sites field for the same reason. Themes and styling covers that one.
§ 5The templates axis
templates scopes a block to the layout it renders into. Use it for content that only makes sense in one shape: a "press Space for the next slide" note in a deck, a "use the sidebar" note in a book.
The value is a template name, not a fixed vocabulary. Four templates ship — :webpage, :book, :presentation and :website — and a template block of your own is named here the same way. Templates and layouts covers writing one.
The axis reads the site, not the page
The current template kind is the site's default_template. A page that overrides its own layout with template = :book inside a :webpage site still matches templates = [:webpage], because the axis never looks at the page. This is worth checking before you conclude a decorator is broken. Scope by sites when you meant the site, and by templates when you meant every site that uses one layout.
§ 6Waiving a native block on a target that cannot render it
Everything above is preference. This section is the one case where the backends axis is load-bearing, and it is why the axis exists at all.
Some blocks are native: wdoc renders them in Rust rather than through a WCL lower function, because their output is not expressible in WCL. Writing your own blocks covers what that means. A native block declares which targets implement it, and not every one covers all three.
file is the clear example. It ships a file into the output tree and optionally links to it. A PDF is one self-contained document: there is no output folder beside it to copy into, so a rendered link would point at something that was never shipped. file therefore declares :html and :markdown and stops there.
Put one on a page and build to PDF, and the build refuses:
$ wcl wdoc pdf main.wcl --out _pdf
wcl::eval::user_error
× error: `file` has no :pdf implementation (it is native
│ on :html, :markdown); remove the block or waive it here with
│ `@except(backends = [:pdf])`
╭─[main.wcl:12:3]
11 │ h1 "Downloads"
12 │ file "notes.txt" { as = "the notes" }
· ──────────────────┬──────────────────
· ╰── error raised here
13 │ }
╰────
It refuses rather than rendering nothing, and that is the whole design. A block that silently vanished from one of your three outputs is a bug you find months later, from a reader.
The message names the fix. Add the waiver to that instance:
page dl {
h1 "Downloads"
@except(backends = [:pdf])
file "notes.txt" { as = "the notes" }
}
$ wcl wdoc pdf main.wcl --out _pdf
wrote 1 pdf
$ wcl wdoc markdown main.wcl --out _md
wrote 1 page
$ cat _md/dl.md
# Downloads
[the notes](_wdoc/notes.txt)
Capability says can't; intent says don't want to
The block's declaration states what the renderers can do. @except states what you want. The build refuses until the two agree, which turns a silent hole in one output into an author's decision recorded in the source. The waiver is per instance on purpose: the next file block on the next page makes the same decision for itself.
§ 6.1Two targets have to cover the block
A block is checked against two backends, because two are involved. One is the target the build is producing. The other is the renderer actually running, and those are not always the same: a card in a diagram draws its body as HTML in whichever target embeds the SVG.
So a file inside a card must not reach a PDF just because the card body happens to render as HTML. And markdown_source, which taps the Markdown emitter from inside the HTML build, is a rendering question rather than an output one. On an ordinary page the two are the same backend, and this is one check that never surprises you. When they differ, the error says so.
§ 7Where a decorator goes
@only and @except sit on the line above the block instance they apply to, exactly like every other decorator:
@except(backends = [:pdf])
video "assets/tour.mp4" { poster = "assets/tour.jpg" }
They apply to one instance, and they take its whole subtree with it. Hide a callout and its body goes; hide a diagram and every shape inside it goes. There is no way to hide a field, and no way to hide a page. A page belongs to sites through its sites field, and a page you do not want built is a page you do not declare.
One decorator of each kind per block, and that is a rule rather than a convention. A second @except on one instance is a schema violation:
$ wcl wdoc build main.wcl --out _site
wcl::eval::schema_violation
× decorator '@except' may appear at most once on one node
1 schema violation
So put every axis in the one decorator, which is also where the AND rule can read them together.
§ 8Where to go next
- Output targets — the three targets the backends axis names, and what each one can carry.
- Documents, pages and sites — the site block, the sites field on a page, and multi-site output.
- Writing your own blocks — what makes a block native, and how a block declares the targets it covers.
- Themes and styling — the parallel sites field the styling blocks carry.
- Decorators — how a decorator is declared and read back, in the language rather than in wdoc.