Configuration
All site configuration lives in config.toml at the project root.
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; keys nested inside a section are validated by that section's own loader.
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 |
| 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) |
| 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.
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"
| 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 |
See Internal Links for the @/ link syntax and the --cache caveat in strict mode.
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).
Static Files
Everything under static/ is copied verbatim into the site root, preserving its directory structure — 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 feed generation |
[sitemap] |
SEO | Sitemap XML generation |
[robots] |
SEO | Robots.txt generation |
[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 |
[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 |
[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 |
[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 |
[[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], [llms], [pwa], [amp], [deployment], etc.
See Also
- CLI — Command-line options that override config
- Environment-Specific Config — Per-environment overrides (
config.production.toml)