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) |
| default_language | string | Language code of unsuffixed content files (default "en"). See Multilingual |
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.
drafts, parallel and cache apply to hwaro serve as well. output_dir does not: serve always builds into its own .hwaro/serve/ and leaves output_dir untouched (see the serve command). hwaro deploy does follow output_dir: 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 relative path that escapes the project (../out) or an empty value is ignored with a warning, and the default public is used instead.
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") |
| admonitions | bool | true | GitHub-style > [!NOTE] blockquotes become admonition blocks |
| heading_ids | bool | true | Custom heading IDs (## Heading {#custom-id}) |
| ins | bool | false | Inserted text (++text++ → <ins>text</ins>) |
| mark | bool | false | Highlighted text (==text== → <mark>text</mark>) |
| sub | bool | false | Subscript (~text~ → <sub>text</sub>) |
| sup | bool | false | Superscript (^text^ → <sup>text</sup>) |
| attributes | bool | false | Generalized {#id .class key=val} blocks on headings and inline images |
| 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), "published" (newest first, ignoring updated), "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.new] |
Archetypes | Default front matter and bundle style for hwaro new |
[[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 |
[outputs] |
Output Formats | Extra per-page/per-section output formats (JSON, XML, TXT, CSV) |
[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)