Configuration
grimoire.toml sits next to the book, and every key in it is optional - a directory of Markdown files builds with no configuration file at all. Point --config elsewhere to use a different one.
Two rules hold throughout:
- A command-line flag wins over the file, so a one-off build needs no edit.
- A key of the wrong type keeps its default rather than failing the build, and an unknown key is ignored. A half-written table degrades to the defaults instead of aborting a build that would otherwise have succeeded.
The whole file
Every key, with its default:
[book]
title = "Documentation"
description = ""
authors = []
authorsLabel = "Written by"
language = "en"
src = "docs"
[build]
out = "site"
jobs = 0 # 0 = one render task per CPU
[html]
theme = "grimoire"
mode = "auto" # auto | light | dark
tocDepth = 3
sectionNumbers = true
footer = "Rendered with <a href=\"...\">Grimoire</a>"
repoUrl = ""
repoLabel = "Source"
editUrl = ""
favicon = ""
logo = ""
[highlight]
enabled = false
[highlightjs]
enabled = false
cdn = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1"
style = "github"
styleDark = "github-dark"
languages = ["bash", "go", "json", "yaml", "xml", "ini", "nginx"]
[search]
enabled = true
bodyChars = 1200
[pdf]
enabled = false
output = "book.pdf"
paper = "a4" # a4 | letter
bookmarkLevel = 3
pageNumbers = false
footerLeft = ""
titlePage = true[book]
| Key | Type | Default | Meaning |
|---|---|---|---|
title | string | "Documentation" | shown in the sidebar and in every page title; also the PDF cover and Title |
description | string | "" | emitted as a description meta tag, and as the PDF Subject |
authors | list of string | [] | the author meta tag, the PDF Author field, and the reader-facing credit |
authorsLabel | string | "Written by" | what introduces those names where a reader sees them; "" prints them alone |
language | string | "en" | the BCP 47 tag on the html element |
src | string | "docs" | the directory of Markdown to build, relative to the working directory |
src is where the outline comes from. If it holds a SUMMARY.md, that file is the outline; if not, the directory tree is walked instead.
authors is used two ways, and only one of them is labelled. The author meta tag and the PDF Author field want the names alone, because those are read by software. The page footer and the PDF cover want a credit, because a name standing on its own says nothing about what it is - so authorsLabel goes in front of it:
authors = ["Ada Lovelace", "Grace Hopper"]
authorsLabel = "Built with love by" # -> Built with love by Ada Lovelace, Grace Hopper
authorsLabel = "" # -> Ada Lovelace, Grace Hopper[build]
| Key | Type | Default | Meaning |
|---|---|---|---|
out | string | "site" | where the site is written |
jobs | int | 0 | chapters rendered in parallel; 0 means one task per CPU |
The output directory is created if it is missing. Files already there are left alone unless the build writes over them, so an unrelated file in site/ survives - but so does a chapter you deleted from the book, which is worth knowing before publishing.
jobs above the chapter count simply idles the extra workers. See Performance for what raising it actually buys.
[html]
| Key | Type | Default | Meaning |
|---|---|---|---|
theme | string | "grimoire" | one of the ten themes; an unknown name falls back to grimoire |
mode | string | "auto" | the colour mode a first-time reader gets: auto, light, dark |
tocDepth | int | 3 | deepest heading level in the per-page contents; clamped to 1-6 |
sectionNumbers | bool | true | number the chapters in the sidebar |
footer | string | the Grimoire credit | HTML placed in the page footer; "" for no footer |
repoUrl | string | "" | a source-repository link in the top bar; "" for none |
repoLabel | string | "Source" | the label on that link |
editUrl | string | "" | an edit-this-page URL with a {path} slot; "" for none |
favicon | string | "" | a favicon path, copied into the site |
logo | string | "" | a logo shown beside the title, relative to src |
mode only decides the first visit. Once a reader touches the selector, their choice is remembered and this setting no longer applies to them. Whatever it resolves to is stamped on the document before the first paint, so navigating a dark site never flashes white.
editUrl substitutes the chapter's source path for {path}:
editUrl = "https://github.com/me/book/edit/main/docs/{path}"logo pointing at an SVG gets inlined into the page, so it inherits the colour mode through currentColor and costs no extra request; any other format is linked as an img. The path is resolved against src, and ../ out of it is fine:
logo = "../assets/wordmark.svg"footer is emitted verbatim, so it can carry HTML:
footer = "© 2026 Example Ltd · <a href=\"/imprint\">Imprint</a>"That is deliberate - the footer is your own configuration, not untrusted input. Everything else that renders text, including the book and chapter titles, goes through the Markdown renderer and is escaped.
[highlight] and [highlightjs]
Highlighting comes in two layers, and they are two tables because they have very different consequences.
[highlight] is the switch; [highlightjs] describes the CDN layer, so every key that only means something to highlight.js lives there.
| Key | Type | Default | Meaning |
|---|---|---|---|
highlight.enabled | bool | false | the master switch. On its own: the built-in Jennifer highlighter |
highlightjs.enabled | bool | false | additionally load highlight.js from a CDN |
highlightjs.cdn | string | cdnjs 11.11.1 | the CDN base URL, with no trailing slash |
highlightjs.style | string | "github" | the highlight.js stylesheet used in light mode |
highlightjs.styleDark | string | "github-dark" | the stylesheet used in dark mode |
highlightjs.languages | list of string | ["bash", "go", "json", "yaml", "xml", "ini", "nginx"] | extra highlight.js language packs to load |
[highlight] alone highlights Jennifer code while the site is built. The spans are in the HTML that gets written, so there is no CDN, no JavaScript, and nothing to load; it works with scripting off and over file://. This is the layer to enable for a book that must make no third-party requests.
[highlightjs] on top pulls highlight.js from cdn and highlights the languages the built-in highlighter does not know, loading the languages packs and swapping style and styleDark with the mode selector. Blocks Grimoire already highlighted are marked so highlight.js leaves them alone.
With highlightjs.enabled = false, the other three keys do nothing - there is no stylesheet to choose and no grammar to fetch - which is exactly why they sit in this table rather than beside the master switch.
highlight.enabled is the master switch:
highlight | highlightjs | Result |
|---|---|---|
| off | off | no highlighting (the default) |
| on | off | Jennifer, at build time; nothing fetched |
| on | on | Jennifer at build time, everything else from the CDN |
| off | on | no highlighting, and the build says so |
The last row is a contradiction rather than an intent, and it resolves to off: a book that says "no highlighting" should not start making third-party requests because a second table was left enabled. The build reports the combination on stderr instead of silently picking one of the two readings.
The default CDN URL pins a version rather than tracking latest. A documentation build should render the same today and in a year, and a silent major-version bump on a CDN is exactly the kind of change that breaks a language grammar.
[search]
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled | bool | true | build the search index and ship the search UI |
bodyChars | int | 1200 | body text kept per indexed section; floored at 120 |
Indexing is per section, not per page, so a hit lands on the paragraph rather than at the top of a long chapter. bodyChars trades index size against how deep into a section a match can still be found. --no-search turns it off for a single build without touching the file.
[pdf]
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled | bool | false | grimoire build also renders the PDF |
output | string | "book.pdf" | the PDF path, relative to build.out |
paper | string | "a4" | a4 or letter |
bookmarkLevel | int | 3 | bookmark headings down to this level; 0 disables the outline |
pageNumbers | bool | false | print page/total at the outside edge of every page footer |
footerLeft | string | "" | a template for the other side of that footer; "" leaves it empty |
titlePage | bool | true | open the book with a title page; off starts it at the first chapter |
enabled is what --pdf sets, and grimoire pdf renders the PDF regardless of it. Expect the PDF to dominate the build - see Performance.
pageNumbers needs the total page count, which does not exist until the whole book has been laid out, so the build renders the document and then stamps the footer on every page. The number is drawn inside the bottom margin in the theme's muted colour - a footer is for placing yourself, not for reading.
footerLeft fills the other side, and carries two slots read from the book's own git checkout:
{version} | the tag the build sits on, with any leading v removed |
{commit} | the short commit id - only when there is no tag |
Exactly one of the two is ever filled, which is what lets a single template cover a release and a working build. This manual uses:
footerLeft = "Grimoire {version} Manual {commit}"giving Grimoire 1.0.0 Manual on a tagged commit and Grimoire Manual 0e173c1 on anything else. The gap the empty slot leaves is closed before the line is drawn. Outside a git checkout - or where git is unavailable, including on jennifer-tiny, which ships no os/exec - both slots come back empty and the rest of the template still prints.
titlePage off drops the cover entirely and starts the PDF at the first chapter, for a book that would rather supply its own front matter as a prefix chapter.
A worked example
The grimoire.toml in this repository builds these pages - Grimoire renders its own documentation:
[book]
title = "Grimoire"
description = "Build a documentation website, and a printable PDF, from a directory of Markdown files."
authors = ["mplx <jennifer@mplx.dev>"]
authorsLabel = "Written by"
language = "en"
src = "docs"
[build]
out = "site"
[html]
theme = "grimoire"
mode = "auto"
footer = 'Rendered with <a href="https://grimoire.jennifer-lang.dev/">Grimoire</a>, by itself.'
repoUrl = "https://github.com/jennifer-language/grimoire"
repoLabel = "Source"
editUrl = "https://github.com/jennifer-language/grimoire/edit/main/docs/{path}"
favicon = "favicon.ico"
[highlight]
enabled = true
[pdf]
enabled = true
output = "grimoire.pdf"
paper = "a4"
pageNumbers = true
footerLeft = "Grimoire {version} Manual {commit}"
titlePage = trueNote the single-quoted footer: TOML's literal strings take no escapes, which makes an HTML attribute far easier to write than the \" of a basic string.