Configuration
One config.kdl at the project root, in KDL. Every key has a default, so you only write what you want to change.
site "My Site"
url "https://example.com"
author "Ada"
lang "en"
paths {
content "content"
dist "public"
}
links {
style "clean"
}
generate {
sitemap #true
feed {
formats "rss" "atom"
limit 20
}
}
A one-line config.kdl holding site "My Site" is a valid site. --config points at a file somewhere else, --root at a different project directory.
KDL in one minute
// a line comment
prune #true // booleans are #true and #false
serve { port 1821 } // a block groups keys under a name
links { style clean } // a simple word needs no quotes
hooks {
before #"tailwindcss -i "assets/_app.css" -o "assets/style.css""#
}
Booleans are #true and #false. A bare word parses as a string, so style clean and style "clean" are the same; quote anything with spaces, slashes, or punctuation. A raw string #"..."# keeps inner quotes intact, which is what a hook command usually needs, and """ opens a multi-line one.
The shape of the tree
The top level is grouped by concern. Each block answers one question, and the page that answers it in full is on the right.
| Block | Answers | See |
|---|---|---|
site, url, lang, author, description |
The site’s name, canonical base URL, default language, author, and what it is in one line. | reference |
theme |
The templates and assets this site layers over. | themes |
paths |
Where the content, output, asset, template and static trees live. | pages |
content |
What counts as a page, and how pages group. | pages, collections |
languages |
The languages of a multi-language site. | multiple languages |
assets |
How CSS, JavaScript and images are processed. | asset pipeline, images |
html |
What the emitted markup carries. | pages, meta tags |
links |
Permalink shape, and how hard a broken link fails. | pages |
redirects |
Old paths no page owns, where each one moved, and how a host is told. | redirects |
check |
What the build verifies: links, markup, size budgets. | linting & budgets |
security |
Integrity hashes and the content security policy. | integrity & CSP |
generate |
What gets written beside the pages. | search, feeds, static hosts |
artifacts |
What a page is drawn as beyond its HTML. | cards, PDFs |
navigation |
How a visitor moves between built pages. | SPA & single-file export |
prune |
Whether output this build did not produce is deleted, and what is spared. | reference |
cache |
Where incremental build state lives. | incremental builds |
headers |
What a host is told about the built files, Cache-Control included. |
static hosts |
typst |
Compiler features, sys.inputs, fonts, package registry. |
build metadata |
client |
Constants handed to client JavaScript. | JS modules |
hooks |
External commands run around the build. | build hooks |
announce |
Where the site announces its own metadata. | announcing |
deploy |
Where baudelaire deploy uploads the built site. |
deploying |
serve |
The dev server: port, watching, editor. | dev server & preview |
profiles |
Named overlays applied with --profile. |
profiles & environment |
Every key, with its value shape and what it does, is in the config reference. baudelaire reference assets.images prints one subtree of the same thing in the terminal.
The enclosing block is what gives a name its meaning. paths { assets } is the directory your stylesheets are read from; the top-level assets block is the pipeline that processes them. Same word, different question, never the same scope.
A block’s presence is the switch
Writing the block turns the feature on, and a block with nothing to configure can be written bare:
generate {
llms
robots {
disallow "/drafts/"
}
}
That writes both llms.txt and robots.txt. Delete the node and neither is written.
NOTE
Only a block that has a switch to flip accepts the bare spelling.paths or html on their own configure nothing, so they error rather than being read as intent.
Turning one back off
Deleting the node is not always available. A profile and a theme’s theme.kdl both overlay nodes onto a base, so they can set a key but never remove one, and naming the block is exactly what turns it on. Every block of this kind takes #false on its own line:
profiles {
fast {
check #false
artifacts {
cards #false
pdf { pages #false }
}
}
}
#true is accepted too, and means what presence already meant.
One block is not switchable this way: artifacts { bundles } is on whenever it names a target, so naming none is already how it is off.
Some blocks have a setting decided before you write anything, so they carry a bare flag rather than a presence switch, in whichever polarity the default calls for:
content {
markdown #false // `.md` files are pages by default; stop reading them
drafts #true // drafts are skipped by default; build them
}
Each is shorthand for the block’s own flag key, and the two keys differ: markdown #false is markdown { enabled #false }, drafts #true is drafts { build #true }. Write the long form when the block configures something else too.
Commenting a block out
/- in front of any node comments it out whole, settings intact:
generate {
sitemap #true
/-search {
index "terms"
snippet 240
}
}
Search is off, and turning it back on is deleting two characters. This composes with presence-as-a-switch: /-llms is how you keep the llms { } block you tuned without emitting the file.
Hosting under a subdirectory
Give url a path and the whole site is served from it:
url "https://user.github.io/project"
Links, assets, redirects and the search client all resolve under /project, and the absolute URLs in the sitemap, feeds and canonical tags carry it. Only the emitted URLs shift. The files on disk keep their plain layout (public/blog/index.html, never public/project/blog/index.html), so the host maps the path back for you. baudelaire serve previews under the same path, and --base-url overrides it for one build.
NOTE
A root domain has no path, so nothing is prefixed.search.json and the baudelaire:* modules keep root-relative URLs either way; the bundled search client applies the base itself.
Typos are errors
The parser knows every valid key, so a misspelling fails the build instead of silently doing nothing:
unknown config key `prety`
help: did you mean `pretty`?
valid keys: `pretty`, `embed`, `meta`, `anchors`, ...
The suggestion is scoped to the block you are in, so a real key written at the wrong level is caught too.