Configuration Reference
Accent CMS is configured through a config.yaml file in your project root. All fields have sensible defaults, so a minimal config works out of the box. CLI arguments override config values when both are provided.
Minimal Configuration
A working config.yaml needs very little:
site: name: My Website
Everything else uses defaults. See each section below for the full set of fields.
site
Site-level metadata available in templates via the site variable.
| Field | Type | Default | Description |
|---|---|---|---|
site.name | string | "My Website" | Site title displayed in templates |
site.description | string | "" | Site description for meta tags |
site.url | string | Base URL for absolute links (required for RSS feed, sitemap, and robots.txt Sitemap directive) | |
site.base_path | string | "" | URL prefix for sub-path deployments (e.g. a GitHub Pages project site served under /repo). Normalized to a leading slash with no trailing slash. |
site.language | string | "en" | Default language code |
site.languages | list | [] | Supported language codes for i18n. When two or more entries are listed, the site becomes multilingual. The default language must be included. |
site.main_site | map | unset | Optional link block back to a main/product site this deployment fronts. When unset, the default theme renders no outbound affordances. |
site.main_site.url | string | required | Main-site URL the header wordmark and footer link target. Must be non-empty – an empty value is rejected at config load. |
site.main_site.label | string | site.name | Wordmark text. Fallback applies only when unset; an explicit empty string renders an icon-only wordmark. |
site.main_site.badge_label | string | "Docs" | Text of the header chip that links back to this site’s own home page. |
site.main_site.cta_label | string | unset | Call-to-action button text. The button renders only when both cta_label and cta_url are set. |
site.main_site.cta_url | string | unset | Call-to-action target URL (e.g. a pricing page). |
site: name: My Blog description: A personal blog about Rust and web development url: https://example.com language: en languages: [en, de, fr]
site.base_path
When set, every internal URL Accent emits – page links, theme assets, media, search fetch paths and result URLs, llms.txt links, .md redirect stubs, pagination, and tag links – carries the prefix. This lets a static build work correctly when served from a sub-path, such as a GitHub Pages project site (https://user.github.io/repo/).
base_path is normalized at load time: a leading slash is added if missing and a trailing slash is stripped, so repo, /repo, and /repo/ all resolve to /repo.
Auto-derivation: if site.base_path is left unset and site.url (or the --base-url build flag) contains a non-root path component, the base path is derived from it automatically and logged at INFO. For example, site.url: https://username.github.io/repo alone derives a base path of /repo, with no further configuration needed. An explicit site.base_path always wins over derivation, and setting it to an empty string explicitly disables derivation.
site: url: https://username.github.io/repo base_path: /repo # optional -- would otherwise be derived from the path in site.url
Divergence from site.url: an explicit base_path can be set to a value other than the path component of site.url – for example site.url: https://username.github.io/repo with base_path: /other. This is allowed, but absolute URLs (sitemap, feeds, canonical links, JSON-LD) always use site.url as their base and append the resolved base_path to it, so the result is https://username.github.io/repo/other/..., not https://username.github.io/other/.... Accent logs a startup warning when it detects this divergence. Set site.base_path to match the path already in site.url (or omit it and rely on derivation) unless the deployed site genuinely serves content at site.url’s path plus the extra prefix.
site.main_site
Declares the main/product site a satellite deployment (typically a docs
site) belongs to. Templates receive it as site.main_site, including a
derived host field for compact link text. The default theme uses the
block for its header brand split, call-to-action button, and footer
link; see Linking Back to a Main Site
in the Theming Guide for what renders where.
site: main_site: url: https://example.com label: Example # optional; defaults to site.name badge_label: Docs # optional; text of the back-to-home chip cta_label: Get a license # optional; CTA renders only with both cta_* keys cta_url: https://example.com/pricing
server
HTTP server bind settings.
| Field | Type | Default | Description |
|---|---|---|---|
server.address | string | "127.0.0.1" | Bind address. Use "0.0.0.0" to listen on all interfaces. |
server.port | integer | 4400 | Bind port |
server: address: "0.0.0.0" port: 8080
theme
Theme selection and directory settings.
| Field | Type | Default | Description |
|---|---|---|---|
theme.name | string | "default" | Active theme name (must match a directory under theme.directory) |
theme.directory | string | "./themes" | Base directory containing theme directories |
theme.embedded | string | unset | Use a theme built into the binary instead of one on disk. The only value is viewer, the document viewer that accent serve <file> and accent serve <dir> use. When set, theme.name and theme.directory are ignored and the themes directory does not have to exist; --theme and --theme-dir on the command line still win |
theme: name: default directory: ./themes
To serve a plain markdown tree with the built-in viewer and no theme on disk (what accent serve <dir> --write-config writes):
theme: embedded: viewer
content
Content directory and permalink settings.
| Field | Type | Default | Description |
|---|---|---|---|
content.directory | string | "./content" | Root directory for site content |
content.md_extension_redirects | bool | true | Auto-redirect .md URLs (e.g. /blog.md) to their clean URL. Set false to suppress the per-page redirect stubs in accent build and the 301s in accent serve |
content.permalinks | map | {} | URL prefix-to-pattern map for date-based or custom URL structures |
content.auto_index.enabled | bool | false | Generate a section page for every directory that has no README.md, index.md, or default.md, so an arbitrary markdown tree is navigable from the root |
content.auto_index.ignore | list | ["node_modules", "target"] | Directory names skipped at every depth while auto-indexing |
content.auto_index.gitignore | bool | true | Also skip whatever the tree’s .gitignore files skip, with git’s rules |
content.permalinks
Permalink patterns transform page URLs based on frontmatter metadata. Each key is a URL prefix (section), and the value is a pattern string with tokens that expand at load time. Pages whose directory-derived URL starts with a configured prefix get their URL rewritten; all other pages are unaffected.
Section index pages (e.g., default.md at /blog) are never transformed – only child pages.
The pattern replaces the entire URL, it does not extend the prefix. A page
under /blog matched by /blog: "/:year/:month/:slug" becomes
/2026/01/welcome, not /blog/2026/01/welcome. Re-include the prefix with
the :section token when you want to keep it – that is what the examples
below do.
Available tokens:
| Token | Source | Example |
|---|---|---|
:year | 4-digit year from frontmatter date | 2026 |
:month | Zero-padded month from date | 01 |
:day | Zero-padded day from date | 25 |
:slug | Filename stem or frontmatter slug override | welcome |
:title | Slugified frontmatter title | welcome-to-accent |
:section | Parent section name from the prefix | blog |
Date tokens (:year, :month, :day) require a date field in frontmatter. If missing, the page keeps its directory-based URL and a warning is logged.
Frontmatter overrides:
slug– Overrides the:slugtoken value (e.g.,slug: hello-worldproduces/blog/2026/01/hello-worldfrom the pattern above, instead of using the filename).url– Bypasses permalink patterns entirely and sets an absolute URL (e.g.,url: /custom/path).
When a permalink pattern rewrites a page URL, the original directory-based URL automatically returns a 301 redirect to the new URL. In static builds, a small HTML redirect file is generated at the old path.
content: directory: ./content permalinks: /blog: "/:section/:year/:month/:slug" /events: "/:section/:year/:month/:day/:slug" /news: "/:section/:year/:slug"
content.md_extension_redirects
Accent computes clean URLs by stripping the .md extension, then auto-generates a redirect so a GitHub-style link (/blog.md) resolves to the clean URL (/blog) instead of returning 404. In accent serve this is a 301; in accent build it is a small meta-refresh stub carrying a canonical link to the clean URL (e.g. public/blog.md/index.html). The stubs are excluded from sitemap.xml and feed.xml, and the home page is never redirected.
This is useful for documentation sites whose internal links use .md suffixes. If your site only ever links to clean URLs, set the flag to false to skip the redirects entirely – no build stubs, and .md URLs return a normal 404:
content: md_extension_redirects: false
When Markdown View is enabled in HTML mode, .md URLs serve the raw markdown source regardless of this setting.
content.auto_index
A content directory that was never laid out for Accent, such as a repository or a notes folder, has directories with no index file, and those directories have no page of their own. With auto_index on, every directory that holds markdown gets a generated section page: a heading plus a list of its sub-sections and pages. A directory that already has a README.md, index.md, or default.md keeps it. Directories with no markdown anywhere beneath them get no page and are not listed. The generated pages take part in navigation, search, the sitemap, and feeds like any other page, in accent serve and accent build alike.
accent serve <dir> turns this on for the preview; --write-config records it.
content: directory: . auto_index: enabled: true ignore: - node_modules - target gitignore: true
Hidden (dot-prefixed) and underscore-prefixed entries are always skipped. Names in ignore are matched against the bare directory name at every depth.
The same rules decide what is a static asset. A file under a directory on the ignore list, or under a path a .gitignore hides, is not served at its bare page URL or under /content-media/, and accent build does not publish it, so the preview and the deployed site answer the same URLs. With auto_index off no directory is hidden, and every servable sibling file is an asset as before.
With gitignore on (the default), every .gitignore from the content directory down to a file’s directory applies, with git’s own rules: anchored patterns, directory-only patterns, globs, negations, and last match wins. A repository already says what is generated or vendored, and serving it skips exactly that. Global git excludes and .git/info/exclude are not read. Set gitignore: false to serve ignored files too.
In dev accent serve, the file watcher skips hidden entries, the names in ignore, and gitignored paths in the same way, so a write to build output, a dependency tree, or .git does not reload the page.
media
Media serving and image processing settings. Media processing runs in accent build and dev serve with no license; serving in production requires a Standard or Pro license.
| Field | Type | Default | Description |
|---|---|---|---|
media.directory | string | "./media" | Shared media library directory |
media.page_local | bool | true | Enable page-local media serving (files alongside content .md files) |
media.serve_at_page_url | bool | true | Also serve page-local media at the bare page URL (alias for /content-media/...) |
The site’s own config.yaml is never served as page-local media or published by accent build, even when it sits inside the content directory, as it does with content.directory: .. A config.yaml elsewhere in the tree is a page asset like any other file.
media.processing Free
On-demand image processing runs in dev serve with no license; serving in production requires a Standard or Pro license.
| Field | Type | Default | Description |
|---|---|---|---|
media.processing.enabled | bool | false | Enable image processing (resizing, format conversion) |
media.processing.jpeg_quality | integer | 85 | Default JPEG output quality (1-100) |
media.processing.webp_quality | integer | 80 | Default WebP output quality (1-100) |
media.processing.max_dimension | integer | 4096 | Maximum resize dimension in pixels |
media.processing.responsive_widths | list | [320, 640, 960, 1280, 1920] | Responsive image breakpoints in pixels |
media.cache
Cache for processed media files.
| Field | Type | Default | Description |
|---|---|---|---|
media.cache.directory | string | "./.media-cache" | Directory for processed media variants, one subdirectory per schema fingerprint; the subdirectories of previous fingerprints are deleted when the fingerprint moves and at startup |
media.cache.max_size_mb | integer | 500 | Maximum cache size in megabytes |
media.attachments
Deliberate file downloads for extensions outside the serving allowlist.
| Field | Type | Default | Description |
|---|---|---|---|
media.attachments.extensions | list | [] | Extensions (without the leading dot, matched case-insensitively) served as forced downloads |
By default, Accent only serves files whose extension is on a hardcoded
allowlist (images, video, audio, PDF, HTML, and common structured-data
formats); anything else – an install script, an archive, a certificate
– is silently excluded from both accent serve and accent build
output. Listing an extension here makes such files resolvable on every
media path (the shared /media/ library, page-local /content-media/
files, and the bare-URL alias), but they are always delivered as a
download: Content-Type: application/octet-stream with
Content-Disposition: attachment; filename="...". There is no way to
configure inline rendering for a new extension.
Rules:
- Attachment wins on overlap. Listing an extension that is also on
the inline allowlist (for example
pdforhtml) makes those files download instead of rendering. The list can only ever make delivery stricter, never looser. - Hard rejections stay hard. Markdown sources and dotfiles are
never served regardless of this list; listing
mdis a configuration error. - Static builds publish the files but cannot send the headers. A
statically hosted download may render as plain text in the browser
unless your host lets you configure a
Content-Dispositionheader for it. See offering file downloads. - The list is read per request, so a config reload (
SIGHUPorPOST /_admin/reload) applies changes without a restart.
media: directory: ./media page_local: true serve_at_page_url: true # alias /content-media/{path} at the bare page URL processing: enabled: true jpeg_quality: 85 webp_quality: 80 max_dimension: 4096 responsive_widths: [320, 640, 960, 1280, 1920] cache: directory: ./.media-cache max_size_mb: 500 attachments: extensions: ["sh", "zip", "pem"]
Bare-URL alias for page-local media
When media.serve_at_page_url: true (the default), files placed next
to a page’s default.md / README.md / index.md are reachable at
two URLs:
- The canonical
/content-media/<page>/<file>route, and - The bare page URL:
/<page>/<file>.
Page URLs always win over the alias, so a sub-page at /blog/post/
shadows any media file that would resolve to the same URL. Markdown
files are never aliased – the .md/.markdown rejection from the
shared media route is reused. Sites that rely on 404s at bare media
URLs (CDN fallthrough, strict URL contracts) can opt out by setting
media.serve_at_page_url: false.
A single page can also opt its sibling files out of the alias via frontmatter without disabling it site-wide:
--- title: Internal Drafts media: expose: false ---
When media.expose: false is set on a page, the alias returns 404 for
any sibling file in that page’s directory; the file is still reachable
at /content-media/... for theme references.
accent build emits the same two routes, so a bare page-local URL
authored while developing against accent serve is deploy-safe: the
static output carries both output/content-media/<page>/<file> and
output/<page>/<file> for every page-local media file, and the two are
byte-identical. media.serve_at_page_url: false is a single opt-out that
covers both surfaces consistently – it disables the bare-URL alias in
accent serve and stops the build from emitting the bare-URL copy, while
the canonical /content-media/... route is unaffected in either mode. A
rendered page’s own output always wins: if a page-local media filename
would collide with a page’s index.html, the build keeps the rendered
page and skips the media copy at that path (logged as a warning), exactly
like a real page shadows the alias in accent serve.
One layout is a partial exception: pages that live in a numbered sort
directory (e.g. content/01.getting-started/default.md, URL
/getting-started) are never reachable at their bare URL in
either mode, because the bare-URL resolver is a literal filesystem path
join with no numeric-prefix stripping – only the canonical
/content-media/getting-started/... route (which is keyed by the clean
URL, not a filesystem path) works for these pages. This is a pre-existing
property of the serve_at_page_url alias, not something the build
introduces.
dev
Development settings. All dev features are automatically disabled when accent serve --production is used.
| Field | Type | Default | Description |
|---|---|---|---|
dev.hot_reload | bool | true | Enable file watching for templates, content, theme config, and assets |
dev.browser_reload | bool | true | Enable browser auto-refresh via Server-Sent Events at /_dev/reload |
dev.debounce_ms | integer | 500 | Debounce delay in milliseconds before processing file changes |
dev.show_drafts | bool | false | Show pages with status: draft or status: review in frontmatter |
dev.debug | bool | false | Enable the ?acdbg debug panel for inspecting template context |
dev: hot_reload: true browser_reload: true debounce_ms: 100 show_drafts: false debug: true
Hot reload performance
After a content change, the server rebuilds its content index in the background right away, so the browser’s auto-refresh lands on a warm cache instead of waiting for the rebuild itself. Concurrent requests share a single rebuild rather than each rescanning the content directory, and the search index refresh reuses the same shared index.
The rebuild cost scales with site size and build profile. On large
sites (hundreds of pages), run the dev server from a release build –
cargo run --release -- serve ... or a release accent binary –
which rebuilds the index an order of magnitude faster than a debug
build while keeping hot reload fully active.
cache
Page rendering cache settings.
| Field | Type | Default | Description |
|---|---|---|---|
cache.max_size_mb | integer | 50 | Maximum cache size in megabytes |
cache.ttl_seconds | integer | 3600 | Time-to-idle in seconds before cached entries expire |
cache.warming | boolean | false | Pre-render all pages at startup to eliminate cold-start latency |
cache: max_size_mb: 100 ttl_seconds: 7200 warming: true # recommended for production
http_headers
HTTP response header settings for caching and CORS. Applied to all responses via middleware.
These settings apply to accent serve only — a static build writes files, and files carry no HTTP headers. To carry this policy onto a static host, set build.header_artifacts, which generates the host’s own header configuration from this same section.
http_headers.cache
Controls Cache-Control, ETag, and Vary headers per content type. In dev mode, all responses get Cache-Control: no-store regardless of these settings.
| Field | Type | Default | Description |
|---|---|---|---|
http_headers.cache.enabled | boolean | true | Enable cache headers in production |
http_headers.cache.defaults.pages | string | "public, max-age=300, s-maxage=3600" | Cache-Control for HTML pages |
http_headers.cache.defaults.assets | string | "public, max-age=31536000, immutable" | Cache-Control for CSS/JS/WASM |
http_headers.cache.defaults.media | string | "public, max-age=86400" | Cache-Control for images/media |
http_headers.cache.defaults.fonts | string | "public, max-age=31536000, immutable" | Cache-Control for font files |
http_headers.cache.defaults.feed | string | "public, max-age=3600" | Cache-Control for RSS/Atom feeds |
http_headers.cache.routes | array | [] | Per-route cache-control overrides (first match wins) |
The admin UI is the exception: every response under /_admin/ carries Cache-Control: no-store, because every one of them belongs to a signed-in editor. These settings, including routes, do not apply to it.
http_headers.cors
CORS (Cross-Origin Resource Sharing) headers. Disabled by default. When enabled, supersedes the legacy api.cors setting.
| Field | Type | Default | Description |
|---|---|---|---|
http_headers.cors.enabled | boolean | false | Enable CORS headers globally |
http_headers.cors.allowed_origins | array | [] | Allowed origins (use ["*"] for any) |
http_headers.cors.allowed_methods | array | ["GET", "HEAD", "OPTIONS"] | Allowed HTTP methods |
http_headers.cors.allowed_headers | array | ["Content-Type", "Accept"] | Allowed request headers |
http_headers.cors.expose_headers | array | ["ETag", "Last-Modified"] | Response headers exposed to client |
http_headers.cors.max_age | integer | 86400 | Preflight cache duration in seconds |
http_headers.cors.routes | array | [] | Per-route CORS overrides (first match wins) |
http_headers: cache: defaults: pages: "public, max-age=600, s-maxage=3600" routes: - path: "/api/*" cache_control: "no-store" cors: enabled: true allowed_origins: - "https://example.com" - "https://cdn.example.com" routes: - path: "/api/*" allowed_origins: ["*"] allowed_methods: ["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"]
http_headers.security
Baseline security response headers, applied to every response. These harden the site against MIME sniffing, clickjacking, base-URI hijacking, and abuse of uploaded files (notably scriptable SVG). Enabled by default with safe values.
| Field | Type | Default | Description |
|---|---|---|---|
http_headers.security.enabled | boolean | true | Master switch for all security headers |
http_headers.security.content_type_options | boolean | true | Emit X-Content-Type-Options: nosniff |
http_headers.security.frame_options | string | null | "SAMEORIGIN" | X-Frame-Options value (null to omit) |
http_headers.security.referrer_policy | string | null | "strict-origin-when-cross-origin" | Referrer-Policy value (null to omit) |
http_headers.security.content_security_policy | string | null | "object-src 'none'; base-uri 'self'; frame-ancestors 'self'" | Baseline CSP for page responses |
http_headers.security.media_content_security_policy | string | null | "default-src 'none'; sandbox" | Strict CSP for served media responses |
http_headers.security.script_policy | object | see below | Strict script-src for pages served by accent serve |
http_headers.security.routes | list | [] | Per-route header overrides (first match wins) |
The baseline page CSP carries no script-src or default-src; it blocks plugin/embed (object-src), base-tag hijacking (base-uri), and framing (frame-ancestors). On pages that accent serve renders, the script policy described below adds the script restriction. Media responses additionally receive the strict media CSP, and an image/svg+xml response is served with Content-Disposition: attachment so a scriptable SVG downloads instead of rendering inline. A header is only set if a handler did not already set it.
http_headers: security: enabled: true frame_options: "SAMEORIGIN" referrer_policy: "strict-origin-when-cross-origin" content_security_policy: "object-src 'none'; base-uri 'self'; frame-ancestors 'self'" media_content_security_policy: "default-src 'none'; sandbox"
Per-route overrides (http_headers.security.routes)
Scope a different policy to specific paths without touching the site-wide defaults. Each entry matches the request path against a glob pattern; entries are evaluated top-to-bottom and the first match wins, exactly like the cache and CORS route overrides.
| Field | Type | Description |
|---|---|---|
path | string | Glob pattern for the request path (required) |
content_security_policy | string | CSP for matching responses; replaces the page or media default |
frame_options | string | X-Frame-Options for matching responses |
referrer_policy | string | Referrer-Policy for matching responses |
Fields left unset inherit the corresponding global value. A matched route’s CSP replaces whichever default (page or media) would otherwise apply. X-Content-Type-Options: nosniff and the SVG download disposition are site-wide invariants and cannot be overridden per route.
Patterns support three forms: a trailing /* matches the prefix and everything below it (/content-media/designsystem/*), a bare path matches exactly, and general globs use * for one path segment and ** across segments (/content-media/**/*.html).
http_headers: security: routes: - path: "/content-media/designsystem/*" content_security_policy: "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'" - path: "/embed/*" frame_options: "DENY"
Precedence per header, highest first: a value set by the route handler itself, then the first matching route override, then the page/media default. Overrides are read per request, so a config reload (SIGHUP or POST /_admin/reload) applies them without a restart.
The typical use is un-breaking iframe-embedded HTML served through /content-media/, which the strict media CSP otherwise strips of styles, images, and scripts – see the processing-control guide for when an embedded page needs it.
Script policy (http_headers.security.script_policy)
accent serve appends a strict script-src directive to the page CSP of every HTML page:
object-src 'none'; base-uri 'self'; frame-ancestors 'self'; script-src 'self' 'nonce-<random>'
The nonce is 128 random bits, new on every response. A script runs only when it comes from the site’s own origin or carries the nonce; there is no 'unsafe-inline', so an inline <script> without the nonce, an onclick= attribute, and a javascript: URL are blocked. Templates mark their scripts with nonce="{{ nonce }}"; see “Scripts and the content security policy” in the theming guide.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Add the directive. false keeps the baseline CSP only |
report_only | boolean | false | Send the directive in Content-Security-Policy-Report-Only, so browsers report violations without blocking them. The baseline stays enforced |
sources | list | [] | Extra script-src sources, appended after the nonce, such as https://www.googletagmanager.com or 'strict-dynamic' |
report_uri | string | null | null | report-uri for violation reports |
exclude | list | [] | Paths, as route globs, that keep the baseline CSP only |
report_only_paths | list | ["/_admin/*"] | Paths, as route globs, whose directive is sent report-only even when report_only is false |
http_headers: security: script_policy: enabled: true report_only: false sources: [] exclude: [] report_only_paths: ["/_admin/*"]
The admin UI gets the policy too. It runs the Alpine.js CSP build and htmx without eval, so it needs neither 'unsafe-inline' nor 'unsafe-eval', and the editor preview is a page of its own that carries the site’s policy: a <script> in a draft is blocked in the preview exactly as it will be when published. While the policy rolls out, report_only_paths sends it report-only on /_admin/, so anything the admin still needs shows up as a console report instead of a broken control. To enforce it now:
http_headers: security: script_policy: report_only_paths: []
The policy does not apply in these cases:
- A policy that already controls scripts. When the baseline or a matched route override sets
script-srcordefault-src, that policy is sent as written. - Media and non-HTML responses. Media keeps the strict media CSP.
- The dev-mode Mermaid loader. On a server started without
--production, the origin ofdiagrams.renderers.mermaid.cdn_urlis added tosources. The loader imports diagram types from that CDN inside callbacks, where the nonce does not carry over. Production pages render diagrams on the server and never load it. - DocFind search. When the server runs the DocFind search backend,
'wasm-unsafe-eval'is added tosources, because the search runtime compiles a WebAssembly module in the browser. The keyword allows WebAssembly compilation only, not JavaScripteval. - Paths in
exclude, in static output.excludeapplies to served pages only. To relax the policy for part of a static site, add aroutesentry whosecontent_security_policysetsscript-src.
A built page has no response to carry a nonce, so accent build allows scripts by source instead. It records a 'sha256-...' hash of every inline script marked with nonce="{{ nonce }}" and the origin of every marked external script from another host, removes the attribute, and writes the sources into the page CSP of its header artifacts:
object-src 'none'; base-uri 'self'; frame-ancestors 'self'; script-src 'self' 'sha256-...' https://cdn.jsdelivr.net
accent serve-static sends the same policy, read from the build’s .build-manifest.json. An incremental build keeps the sources of the pages it skips. When the site uses the DocFind search backend, the build adds 'wasm-unsafe-eval'. The Netlify artifact limits a header line to 2000 characters; a site with many distinct inline scripts fails the build there, and the fix is fewer inline scripts, not a longer line.
A <script> in page content is blocked on a served page, like any other script without the nonce. Move the script into a theme template, or embed the page in an iframe from /content-media/ with a scoped route override.
A 304 Not Modified response for a page carries no CSP, so the browser keeps the policy that matches the nonce in its cached copy.
build
Static site generation settings, used by accent build.
| Field | Type | Default | Description |
|---|---|---|---|
build.fingerprint | object | disabled | Asset fingerprinting for cache busting – see the asset fingerprinting guide |
build.header_artifacts | array | [] | Deploy-target response-header artifacts to emit at the output root |
header_artifacts accepts "netlify" (writes _headers) and "apache" (writes .htaccess). Each artifact is generated from http_headers and media.attachments, so the served policy and the deployed policy cannot drift. A rule that a target’s syntax cannot express exactly fails the build rather than shipping a weaker artifact.
build: header_artifacts: ["netlify", "apache"]
See response headers on static hosting for what gets translated, the fail-closed contract, and the hosts that have no header mechanism at all.
build.routing
Routing directives written into those same artifacts. The emitter already derives the clean-URL rules on its own, because those express the URL shape the build committed to in its sitemap and canonical tags. This block is the opposite: facts about the deployment that the output tree cannot show.
| Field | Type | Default | Description |
|---|---|---|---|
build.routing.error_document | string | unset | Site-absolute path served for a 404 |
build.routing.canonical_host | string | unset | The one host this deployment answers on |
build.routing.redirects | array | [] | Redirects the host should serve, in order |
build: header_artifacts: ["apache"] routing: error_document: /404.html canonical_host: example.com redirects: - from: /install.sh to: https://example.invalid/install.sh - from: /old-page to: /new-page status: 302 # optional; 301 when omitted
canonical_host emits a 301 from the host’s www/apex counterpart –
www.example.com when set to example.com, and example.com when set to
www.example.com. It deliberately does not redirect every other host, because
that also captures preview and staging deployments, which are legitimately
reachable under another name. The redirect target is always https.
A redirect to starting with / is site-relative and carries the deployment
prefix along with from; anything else is used verbatim, which is how an
off-site target is written. status accepts 301, 302, 307 and 308.
The block is inert when absent, so a site that never sets it produces
byte-identical output. It currently has no expression in the netlify target,
which carries redirects in a separate _redirects file – configuring both is
a build error rather than a deployment silently missing its rules.
markdown
Toggles for individual pulldown-cmark markdown parser extensions. All extensions are enabled by default. Set any field to false to disable that extension.
| Field | Type | Default | Description |
|---|---|---|---|
markdown.tables | bool | true | GFM tables (| A | B | syntax) |
markdown.footnotes | bool | true | New-style footnotes ([^label]) |
markdown.strikethrough | bool | true | Strikethrough (~~text~~) |
markdown.tasklists | bool | true | Task lists (- [x] / - [ ]) |
markdown.heading_attributes | bool | true | Heading attributes (# Heading { .class #id }) |
markdown.wikilinks | bool | true | Wiki-style links ([[page]]) |
markdown.smart_punctuation | bool | true | Smart punctuation (curly quotes, -- en-dash, --- em-dash) |
markdown.math | bool | true | LaTeX-style math ($inline$, $$display$$) |
markdown.definition_lists | bool | true | Definition lists |
markdown.gfm | bool | true | Miscellaneous GFM extensions (currently: block-quote alerts like > [!NOTE]) |
markdown.superscript | bool | true | Superscript (^text^) |
markdown.subscript | bool | true | Subscript (~text~). When enabled, single-tilde is subscript; double-tilde remains strikethrough. |
markdown.metadata_blocks | bool | true | YAML (---) and pluses (+++) metadata block parsing |
markdown.locale | string | (unset) | Override the typographic locale used by smart-quote remapping. ISO 639-1 codes (de, fr, pl, sv, fi, cs, ja, zh, …). When unset, the page’s language (which falls back to site.language) is used. |
markdown: tables: true footnotes: true strikethrough: true tasklists: true heading_attributes: true wikilinks: true smart_punctuation: true math: true definition_lists: true gfm: true superscript: true subscript: true metadata_blocks: true locale: de # optional: forces German curly-quote conventions
markdown.smart_quotes
Deployment-level overrides to the canonical smart-quote spec. Lets a site flip inner-guillemet spacing or replace a locale’s quote characters wholesale without forking the spec JSON. An empty (or omitted) section is a no-op.
| Field | Type | Default | Description |
|---|---|---|---|
markdown.smart_quotes.inner_space.default | enum | (unset) | Site-wide inner-guillemet spacing override. One of none, nbsp, thin. Beats the spec’s per-locale value – set this to enforce one spacing rule across every locale. |
markdown.smart_quotes.inner_space.per_locale | map | (empty) | Per-locale exceptions. Keys are canonical BCP-47 tags (fr-FR, de-CH). Each entry overrides the spec’s value for that exact tag. |
markdown.smart_quotes.locales | map | (empty) | Wholesale quote-character replacement, keyed by canonical BCP-47 tag. Each entry must specify both double and single as { open, close } pairs. Other spec fields (inner spacing, nested-override behaviour, writing direction) are preserved. |
Resolution priority for inner spacing (highest first):
markdown.smart_quotes.inner_space.per_locale[<tag>]– exact canonical matchmarkdown.smart_quotes.inner_space.per_locale[<language>]– language-only fallbackmarkdown.smart_quotes.inner_space.default- Spec’s
locales[<tag>].innerSpace - Spec’s
innerSpace.globalDefault
The same exact -> language resolution applies to the wholesale locales map: a locales: { de: ... } entry covers de-CH, de-AT, and any other German variant.
Non-canonical YAML keys (de_ch, FR-fr) are rewritten into canonical BCP-47 form at config load. Two entries that canonicalise to the same form are rejected as a collision. Unknown locale tags are rejected with a clear error message; region-only fall-throughs are accepted (de-LI validates because de is in the spec).
markdown: smart_quotes: inner_space: default: thin # flip every locale to thin spaces per_locale: fr-FR: nbsp # but keep NBSP for fr-FR de-CH: none # and drop spacing from Swiss German locales: # Editorial-style override: Norwegian site that prefers curly # doubles over the spec's guillemets. nb: double: { open: "\u{201C}", close: "\u{201D}" } single: { open: "\u{2018}", close: "\u{2019}" }
Reload with pkill -HUP accent (or POST /_admin/reload) without restarting the server.
code
Syntax highlighting settings for fenced code blocks.
| Field | Type | Default | Description |
|---|---|---|---|
code.theme | string | "base16-ocean.dark" | Syntax highlighting theme name (uses syntect themes). Set to "" to disable highlighting. |
code: theme: "InspiredGitHub"
feed
RSS 2.0 feed generation. Only pages with a date field in frontmatter are included.
| Field | Type | Default | Description |
|---|---|---|---|
feed.enabled | bool | true | Enable RSS feed generation |
feed.limit | integer | 20 | Maximum number of entries in the feed |
feed.path | string | "/feed.xml" | URL path where the feed is served |
For absolute URLs in feed entries (required by the RSS specification), set site.url.
feed: enabled: true limit: 20 path: /feed.xml
search
Client-side search index generation. See the Search guide for usage details.
| Field | Type | Default | Description |
|---|---|---|---|
search.enabled | bool | true | Generate the search index |
search.min_word_length | integer | 2 | Minimum word length for indexing |
search.content_length | integer | 200 | Maximum characters of plain-text content stored per page |
search.exclude | list | [] | URL glob patterns to exclude (e.g., "/404", "/tags/*") |
search.fields
Field weights for relevance scoring. Higher values rank matches in that field higher.
| Field | Type | Default | Description |
|---|---|---|---|
search.fields.title | float | 3.0 | Weight for title matches |
search.fields.content | float | 1.0 | Weight for body content matches |
search.fields.tags | float | 2.0 | Weight for tag matches |
search.fields.lead | float | 1.5 | Weight for lead/excerpt matches |
search: enabled: true min_word_length: 2 content_length: 200 exclude: - "/404" - "/tags/*" fields: title: 3.0 content: 1.0 tags: 2.0 lead: 1.5
robots
robots.txt generation.
| Field | Type | Default | Description |
|---|---|---|---|
robots.enabled | bool | true | Enable robots.txt serving at /robots.txt |
robots.rules | string | (auto-generated) | Custom robots.txt rules. When omitted, default rules are generated. A Sitemap: directive is appended when site.url is set. |
robots: enabled: true rules: | User-agent: * Allow: / Disallow: /admin/
well_known
Serve files from a .well-known directory per RFC 8615. Useful for security.txt, ACME challenges, and protocol discovery.
| Field | Type | Default | Description |
|---|---|---|---|
well_known.enabled | bool | false | Enable .well-known file serving |
well_known.directory | string | "./.well-known" | Source directory for .well-known files |
well_known: enabled: true directory: ./.well-known
plugins Free
WASM plugin system. The plugin runtime works in accent build and dev serve with no license; serving plugins in production requires a Standard or Pro license.
| Field | Type | Default | Description |
|---|---|---|---|
plugins.enabled | bool | false | Enable plugin loading and execution |
plugins.directory | string | "./plugins" | Directory containing installed plugins |
plugins.fuel_limit | integer | WASM instruction budget per plugin invocation (omit for unlimited) | |
plugins.render_budget_ms | integer | 50 | Wall-clock budget per plugin call, in milliseconds. Meters guest execution: a plugin suspended in a host call (send-mail, a data-store write) is not charged for the wait, so a slow mail relay cannot exhaust it. Not the same key as diagrams.plugins.timeout_ms, which bounds one diagram render. |
plugins.compilation_cache | string | Directory for Wasmtime compilation cache (speeds up repeated loads) | |
plugins.precompile | bool | false | AOT pre-compile plugins at load time for faster execution |
plugins.allowed_paths | map | {} | WASI filesystem paths exposed to plugins. Prefix values with ro: for read-only access. |
plugins: enabled: true directory: ./plugins fuel_limit: 1000000 render_budget_ms: 50 compilation_cache: .cache/plugins precompile: true allowed_paths: /content: "ro:./content" /media: "ro:./media" /data: ./site/data
hub Free
The ecosystem hub: where accent plugin install (and, in future, theme and
template installs) resolve artifacts from. One section serves all three kinds,
because they come from one registry — this replaces the former
plugins.registry_url, which no longer exists.
| Field | Type | Default | Description |
|---|---|---|---|
hub.registry_url | string | (built-in) | Registry base URL: the raw-content root of the registry’s default branch |
hub.registry_public_key | string | Ed25519 SPKI public key a self-hosted registry signs with. Required whenever registry_url is not the default | |
hub.licenses | map | {} | Vendor licence keys for commercial artifacts, keyed by <kind>:<name> |
hub: registry_url: https://raw.githubusercontent.com/AccentCMS/hub-registry/main registry_public_key: null licenses: "plugin:acme-forms": "eyJhbGciOiJFZERTQSJ9..."
Registry documents are always signature-verified; registry_public_key selects
which key, never whether to check. Pointing registry_url at another
registry without supplying its key is a configuration error.
The section parses in every build, including one compiled without the plugin runtime — themes and templates install without it. A binary that cannot act on the section says so at startup rather than rejecting your config.
versioning
Versioned content trees for documentation or any content section. Each root maps a URL prefix to a set of version definitions with fallback chains.
| Field | Type | Default | Description |
|---|---|---|---|
versioning.roots | map | {} | Versioning roots keyed by URL prefix (e.g., /docs). Empty means versioning is disabled. |
versioning.roots.<prefix>.versions | ordered map | (required) | Version definitions. Iteration order determines dropdown display order. |
versioning.roots.<prefix>.versions.<id>.label | string | (required) | Human-readable label (e.g., "1.1 (Latest)") |
versioning.roots.<prefix>.versions.<id>.badge | string | Optional badge hint for templates (e.g., "latest", "lts") | |
versioning.roots.<prefix>.versions.<id>.fallback | string | Version ID to fall back to when a page is missing in this version | |
versioning.roots.<prefix>.versions.<id>.state | string | inferred | dev (the one writable version), released (frozen, errata allowed), or archived (frozen). When no version sets a state, the last one is dev and the rest are released. |
versioning.roots.<prefix>.default | string | (required) | Default version served when no version prefix is in the URL |
versioning.roots.<prefix>.redirects | map | {} | Deprecated version redirects (e.g., v0.9: v1.0) |
versioning.roots.<prefix>.canonical | string | self | self: every page names its own URL as canonical. default: pages in other versions name the default version’s copy, when it exists and can be indexed. |
versioning.roots.<prefix>.noindex | list | [] | Version states whose pages get a noindex robots tag and are left out of the sitemap, such as [dev]. Can’t include the default version’s state when canonical is default. |
versioning: roots: /docs: versions: v1.0: label: "1.0 (LTS)" badge: lts v1.1: label: "1.1 (Latest)" badge: latest fallback: v1.0 default: v1.1 redirects: v0.9: v1.0
Multiple roots are supported. Each root operates independently with its own version set, default, and redirects. Circular fallback chains and missing fallback/redirect targets are detected at startup.
See the Content Workflow guide for setup instructions and folder structure, and Search engines for how canonical and noindex shape the canonical links and the sitemap.
Configuration Loading
Priority Order
Configuration values are resolved in this order (later overrides earlier):
- Built-in defaults – Hardcoded in Accent CMS
config.yaml– Or the file specified by--config- CLI arguments – Highest priority
Config-Relative Paths
When --config points to a file outside the current working directory, relative paths in the config file are resolved relative to the config file’s parent directory, not the working directory.
# Paths in /srv/mysite/config.yaml resolve relative to /srv/mysite/ accent serve --config /srv/mysite/config.yaml
Runtime Reload
Reload config.yaml without restarting the server:
Send SIGHUP to the server:
pkill -HUP accent
Windows has no SIGHUP, so call the reload endpoint. This example assumes accent serve --no-tls; see Accent CMS on Windows for HTTPS and production mode.
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:4400/_admin/reload
POST /_admin/reload works on every platform. In production mode it needs the admin token in an Authorization: Bearer <token> or X-Admin-Token: <token> header. If the new configuration is invalid, the server keeps running with the previous configuration. See Runtime config reload for what a reload does and does not change.
Full Example
site: name: My Website description: A markdown-powered website url: https://example.com language: en # base_path: /repo # only needed for sub-path deployments; otherwise derived from site.url server: address: "127.0.0.1" port: 4400 theme: name: default directory: ./themes content: directory: ./content permalinks: /blog: "/:section/:year/:month/:slug" media: directory: ./media page_local: true processing: enabled: true jpeg_quality: 85 webp_quality: 80 max_dimension: 4096 responsive_widths: [320, 640, 960, 1280, 1920] cache: directory: ./.media-cache max_size_mb: 500 dev: hot_reload: true browser_reload: true debounce_ms: 100 show_drafts: false debug: false cache: max_size_mb: 50 ttl_seconds: 3600 code: theme: "base16-ocean.dark" feed: enabled: true limit: 20 path: /feed.xml search: enabled: true content_length: 200 exclude: - "/404" - "/tags/*" robots: enabled: true well_known: enabled: false directory: ./.well-known plugins: enabled: false directory: ./plugins # versioning: # roots: # /docs: # versions: # v1.0: # label: "1.0 (LTS)" # badge: lts # v1.1: # label: "1.1 (Latest)" # badge: latest # fallback: v1.0 # default: v1.1