Configuration
All site configuration lives in config.toml at the project root.
Any option documented as a list of strings also accepts a single string, which is read as a one-item list: sections = "posts" means sections = ["posts"]. An empty string, such as exclude = "${SITEMAP_EXCLUDE:-}" with the variable unset, counts as not set and keeps the default. A value that is neither a string nor a list (fields = true) is ignored with a warning.
Unknown top-level keys are reported instead of silently ignored. A typo'd
[markdonw] or titel = "…" would otherwise disable a feature with no
feedback. Hwaro warns with a suggestion when one is close to a real key:
Unknown key 'markdonw' in config.toml — hwaro does not read it. Did you mean 'markdown'?
The check covers top-level keys only: a misspelled key nested inside a
section ([markdown] emoij = true) is not reported, so double-check nested
keys against the tables below. A known section written in the wrong
shape, such as highlight = false instead of a [highlight] table or
taxonomies = ["tags"] instead of [[taxonomies]] entries, is ignored with a
warning as well. sitemap = true is the one short form still accepted: it
means [sitemap] enabled = true.
Site Settings
title = "My Site"
description = "Site description for SEO"
base_url = "https://example.com"
| Key | Type | Description |
|---|---|---|
| title | string | Site title |
| description | string | Site description |
| base_url | string | Production URL (no trailing slash) |
Environment Variables
You can reference environment variables in config.toml. Values are substituted before TOML parsing.
base_url = "${SITE_URL}"
title = "$SITE_TITLE"
description = "${SITE_DESC:-My awesome site}"
| Syntax | Description |
|---|---|
${VAR} |
Substitute with env var value |
$VAR |
Same as above (bare form) |
${VAR:-default} |
Use default if VAR is unset or empty |
Missing variables without defaults are left as-is and produce a build warning. See Environment Variables for template usage.
Build Options
[build]
output_dir = "public"
drafts = false
parallel = true
cache = false
hooks.pre = ["npm install", "npx tsc"]
hooks.post = ["npm run minify"]
| Key | Type | Default | Description |
|---|---|---|---|
| output_dir | string | "public" | Output directory |
| drafts | bool | false | Include draft content |
| parallel | bool | true | Parallel processing |
| cache | bool | false | Enable build caching |
| template_deps | bool | true | Track template dependencies so a template edit only rebuilds the pages that render it |
| write_stats | bool | false | Write hwaro_stats.json (tags, classes and ids used by the rendered pages) at the project root for Tailwind and similar tools. See Asset Pipeline |
| hooks.pre | array | [] | Commands to run before build |
| hooks.post | array | [] | Commands to run after build |
output_dir, drafts, parallel and cache each back a hwaro build flag, and the flag wins:
command-line flag > config.toml > built-in default
So hwaro build -o dist overrides output_dir in config, and hwaro build --drafts still includes drafts when drafts = false. The reverse does not exist: there is no --no-drafts or --no-cache to turn a config value back off, so set those keys only when you want them on for every build. --no-parallel is the one exception, and it overrides parallel = true.
These apply to hwaro serve as well, which builds into (and serves from) output_dir. hwaro deploy follows it too: when [deployment] source_dir is not set, it deploys output_dir rather than assuming public.
output_dir may be relative to the project or absolute. A path that escapes the project (../out) is refused, because the dev server will not serve from outside the project and the build would otherwise write somewhere serve never reads.
See Build Hooks for error handling and use cases.
Markdown
[markdown]
safe = false
lazy_loading = false
emoji = false
footnotes = true
task_lists = true
definition_lists = true
mermaid = false
math = false
math_engine = "katex"
| Key | Type | Default | Description |
|---|---|---|---|
| safe | bool | false | Strip raw HTML from markdown |
| lazy_loading | bool | false | Automatically add loading="lazy" to images |
| emoji | bool | false | Convert emoji shortcodes (e.g. :smile:) to emoji characters |
| footnotes | bool | true | Enable footnote syntax ([^1]) |
| task_lists | bool | true | Enable task list syntax (- [ ] / - [x]) |
| task_list_classes | bool | false | Add GFM classes (task-list-item, contains-task-list) to task-list markup |
| definition_lists | bool | true | Enable definition list syntax (Term\n: Definition) |
| mermaid | bool | false | Render ```mermaid blocks as <div class="mermaid"> |
| math | bool | false | Enable math syntax ($...$ and $$...$$) |
| math_engine | string | "katex" | Math rendering engine ("katex" or "mathjax") |
| smart_punctuation | bool | false | Typographic quotes/dashes/ellipses ("x" → “x”, -- → –, ... → …) |
| containers | bool | false | :::note Title … ::: custom containers (admonition markup) |
| wikilinks | bool | false | Obsidian syntax: [[wikilinks]], ![[image]] embeds, ![[note]] transclusion and foldable > [!type]- callouts (see Wikilinks & Backlinks) |
| insert_anchor_links | string | "none" | Site-wide heading anchor links: "none", "left", or "right" (page front matter overrides) |
| external_links_target_blank | bool | false | Add target="_blank" rel="noopener" to absolute http(s) links |
| external_links_no_follow | bool | false | Add rel="nofollow" to absolute http(s) links |
| external_links_no_referrer | bool | false | Add rel="noreferrer" to absolute http(s) links |
See Markdown Extensions for syntax details and examples.
Content
[content]
summary_length = 70
summary_ellipsis = "…"
backlinks = false
| Key | Type | Default | Description |
|---|---|---|---|
| summary_length | int | 70 | Automatic summary length for pages without <!-- more --> or description: words for space-delimited text, characters ×2 for CJK-dominant text. 0 disables the automatic summary |
| summary_ellipsis | string | "…" | Appended to an automatic summary only when text was cut |
| backlinks | bool | false | Fill page.backlinks with the pages linking to each page (see Wikilinks & Backlinks) |
See Content Summary for how page.summary is chosen. [content.files], [content.new], [[content.generate]] and [[content.schema]] are documented in the feature reference below.
Permalinks
Rewrite content directory paths to custom URL paths. Useful for site restructuring without breaking links.
[permalinks]
"old/posts" = "posts"
"2023/drafts" = "archive/2023"
| Source (Directory) | Target (URL Path) | Example Effect |
|---|---|---|
content/old/posts/a.md |
posts/ |
/old/posts/a/ -> /posts/a/ |
Rules are evaluated in declaration order and the first source that matches the page's directory (exactly or as a parent prefix) wins. Later rules are never consulted for that page. Declare specific prefixes before broad ones ("posts/tech" before "posts"), or the broad rule shadows the specific one. This applies to token patterns too, and especially to the "" catch-all: put it last, after every other rule.
Token patterns
A target containing :token segments is a Hugo-style pattern that rebuilds the whole URL instead of remapping the directory:
[permalinks]
"posts" = "/:year/:month/:day/:slug/"
With content/posts/hello.md dated 2026-03-05, the page is published at /2026/03/05/hello/.
| Token | Expands to |
|---|---|
:year |
Page date year (2026) |
:month |
Page date month, zero-padded (03) |
:day |
Page date day, zero-padded (05) |
:slug |
Front-matter slug, or the filename stem when unset |
:title |
Slugified front-matter title (falls back to :slug when it slugifies to nothing) |
:section |
The page's section path (posts/tech); empty for root pages, collapsing the segment |
:filename |
The filename stem, ignoring any slug override |
Notes:
- Tokens must be whole path segments; unknown tokens fail the config load.
- Patterns apply to leaf pages only. Section
_indexand bundleindexpages skip pattern rules (they keep their directory URL, or a later plain remap rule). - A page without a
datethat matches a pattern using:year/:month/:dayfails the build. Add a date, set an explicitpathin front matter, or drop the date tokens. Pages that never publish are exempt: drafts (without--drafts), expired/future-dated pages, and headlessrender: falsepages don't block the build. - An explicit
pathin front matter always wins over any permalink rule. - An empty source key (
""or"/") makes a pattern rule a catch-all for every page. Declare it last, since first-match ordering means it would shadow any rule after it. - For non-default languages the
/lang/prefix comes first:/ko/2026/03/05/hello/.
Links
Control how unresolved @/path.md internal links are treated during the build.
[links]
broken_internal = "error"
broken_anchors = "warn"
| Key | Type | Default | Description |
|---|---|---|---|
| broken_internal | string | "warn" | "warn" logs each unresolved @/ link and keeps the raw markup; "error" fails the build (exit code 5) with one aggregated list of every offender |
| broken_anchors | string | "ignore" | "warn" or "error" checks @/page.md#id and same-page #id links in page content against the id/name attributes in the target's rendered HTML (headings, {#id}, shortcodes, templates, footnotes); "error" fails the build listing source → link → missing id. "ignore" skips the check |
See Internal Links for the @/ link syntax and the --cache caveat in strict mode.
The anchor check reads each target page's output file after the render phase, so on --cache and serve rebuilds a target that was not re-rendered is checked as last written (no false positives). Only links in pages rendered in that pass are checked; run a cold build in CI for a complete check.
Taxonomies
[[taxonomies]]
name = "tags"
feed = true
paginate_by = 10
[[taxonomies]]
name = "categories"
feed = true
| Key | Type | Default | Description |
|---|---|---|---|
| name | string | — | Taxonomy name (used in front matter) |
| feed | bool | false | Generate RSS feed for each term |
| sitemap | bool | true | Include taxonomy pages in sitemap |
| paginate_by | int | — | Items per page on term pages |
| sort_by | string | "date" | Order of pages within a term: "date" (newest first), "title", or "weight" |
| reverse | bool | false | Flip whichever order sort_by produced |
| terms_sort_by | string | "name" | Order of the terms list on the taxonomy index: "name" or "count" |
Term feeds stay reverse-chronological regardless of sort_by. See
Taxonomies for the full sorting rules.
Menus
Named navigation menus, rendered in templates via site.menus / get_menu().
[[menus.main]]
name = "Posts"
url = "/posts/"
weight = 1
[[menus.main]]
name = "About"
url = "/about/"
weight = 2
| Key | Type | Default | Description |
|---|---|---|---|
| name | string | — | Required. Entry skipped (with a warning) if missing. |
| url | string | "" | Root-relative or absolute http(s):///// URL. |
| weight | int | 0 | Sort order within the menu. |
| identifier | string | name |
Unique key other entries reference via parent. |
| parent | string | none | Nest this entry under another entry's identifier. |
Pages/sections can also join a menu from their own front matter (menus = ["main"]) without touching this file. A [languages.<code>] block with no menus table inherits this global set; declaring [[languages.<code>.menus.<name>]] replaces it for that language. See Menus for the full reference (hierarchy, per-language behavior, active_path styling).
To fill a menu from the top-level sections automatically (Hugo's sectionPagesMenu), name it under [menus]:
[menus]
auto_sections = "main"
| Key | Type | Default | Description |
|---|---|---|---|
| auto_sections | string | none (off) | Menu that every top-level section joins (identifier = directory name, name = title, url, weight). Draft, unpublished, headless, transparent and off-site redirect_to sections are skipped, as is a directory without _index.md; an explicit entry with the same identifier wins. Reserved key: a non-string or blank value is a config error. See Automatic Section Menus. |
Static Files
Everything under static/ is copied verbatim into the site root, preserving its directory structure, so static/css/app.css is served at /css/app.css. Hidden entries are included too, so static/.well-known/security.txt is published at /.well-known/security.txt. By default Hwaro filters out common OS, editor, and VCS cruft so it never ships to production.
[static]
use_default_excludes = true # filter built-in cruft (default)
exclude = ["*.bak", "drafts/**"] # extra patterns to skip
| Key | Type | Default | Description |
|---|---|---|---|
| use_default_excludes | bool | true | Filter the built-in cruft denylist (.DS_Store, Thumbs.db, desktop.ini, .git, vim swap files, …) |
| exclude | array | [] | Extra patterns to skip. A glob like *.bak matches at any depth, drafts/** scopes a subtree, and a literal name is anchored to an exact file or directory (drafts drops drafts/…) |
The built-in denylist only removes cruft. Legitimate dot-paths such as .well-known/ and .domains are never filtered and are always published, identically for cold and --cache/incremental builds. Set use_default_excludes = false to disable the built-in filtering entirely.
Development Server
Options for hwaro serve only. They never affect hwaro build output.
[serve]
fast = true # always serve in fast dev mode
[serve.headers]
X-Frame-Options = "SAMEORIGIN"
Cache-Control = "no-store"
| Key | Type | Default | Description |
|---|---|---|---|
| fast | bool | false | Serve as if --fast was passed (skips OG image generation and image processing); explicit CLI skip flags still apply |
| headers | table | {} | Custom HTTP response headers added to every dev-server response; CLI --header values win on duplicate keys |
See the serve command for the matching CLI flags.
Feature Configuration Reference
Each feature has its own documentation with full configuration details. Below is a quick reference of all config.toml sections.
| Config Section | Documentation | Description |
|---|---|---|
[feeds] |
SEO | RSS/Atom/JSON Feed generation |
[sitemap] |
SEO | Sitemap XML generation |
[robots] |
SEO | Robots.txt generation (including Content-Signal lines) |
[og] |
SEO | OpenGraph & Twitter Card meta tags |
[og.auto_image] |
Auto OG Images | Auto-generate OG preview images (including lazy_generate for fast dev server) |
[search] |
Search | Client-side search index |
[highlight] |
Syntax Highlighting | Code syntax highlighting |
[pagination] |
Pagination | Section pagination |
[auto_includes] |
Auto Includes | Auto-include CSS/JS files |
[assets] |
Asset Pipeline | CSS/JS minification & fingerprinting, Subresource Integrity (sri) |
[sass] |
Sass/SCSS | Built-in SCSS compilation (pure Crystal) |
[image_processing] |
Image Processing | Image resizing & LQIP |
[image_processing.lqip] |
Image Processing | Base64 blur-up placeholders |
[content.files] |
Content Files | Publish non-Markdown files |
[[content.generate]] |
Content Generation | Materialize site.data records into pages |
[[content.schema]] |
Front Matter Schema | Per-section front-matter types, required fields, enums, bounds and defaults |
[[data.remote]] |
Remote Data Sources | Fetch remote data into site.data at build time |
[privacy] |
Privacy Mode | Self-host third-party fonts, scripts and images at build time |
[csp] |
Content Security Policy | Content-Security-Policy from inline script/style hashes (_headers or <meta>) |
[static] |
Static Files | Filter cruft / exclude paths from the static/ copy |
[serve] |
Development Server | Dev-server response headers & fast mode |
[links] |
Links | Broken internal @/ link handling (warn or fail the build) |
[series] |
Series | Group posts into ordered series |
[related] |
Related Posts | Related content recommendations |
[git] |
Git Metadata | page.git commit info and updated/date fallbacks from history |
[llms] |
LLMs.txt | AI/LLM crawler instructions |
[pwa] |
PWA | Progressive Web App support |
[amp] |
AMP | Accelerated Mobile Pages |
[deployment] |
Deploy | Deploy targets configuration |
[doctor] |
Doctor | Suppress known diagnostic issues |
languages.* |
Multilingual | Multi-language support |
[versions] / [[versions.list]] |
Versioned Docs | Several documentation versions side by side |
[[menus.*]] |
Menus | Named navigation menus |
Plugins
[plugins]
processors = ["markdown"]
Full Example
A complete config.toml with all core sections. Copy and adjust to your needs.
title = "My Blog"
description = "A blog about programming"
base_url = "https://myblog.com"
default_language = "en"
[build]
output_dir = "public"
drafts = false
parallel = true
cache = false
hooks.pre = ["npm ci"]
hooks.post = ["npm run optimize"]
[markdown]
safe = false
lazy_loading = true
emoji = false
footnotes = true
task_lists = true
[permalinks]
"old/posts" = "posts"
"posts" = "/:year/:month/:day/:slug/"
[plugins]
processors = ["markdown"]
[[taxonomies]]
name = "tags"
feed = true
[[taxonomies]]
name = "categories"
[[menus.main]]
name = "Posts"
url = "/posts/"
# Feature sections — see Feature Configuration Reference above
# [feeds], [sitemap], [robots], [og], [search], [highlight],
# [pagination], [auto_includes], [assets], [sass], [image_processing],
# [series], [related], [git], [llms], [pwa], [amp], [deployment], etc.
See Also
- CLI — Command-line options that override config
- Environment-Specific Config — Per-environment overrides (
config.production.toml)