Docs for v0.0.16.v0.0.18 is the current release.

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.