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.

FieldTypeDefaultDescription
site.namestring"My Website"Site title displayed in templates
site.descriptionstring""Site description for meta tags
site.urlstringBase URL for absolute links (required for RSS feed, sitemap, and robots.txt Sitemap directive)
site.base_pathstring""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.languagestring"en"Default language code
site.languageslist[]Supported language codes for i18n. When two or more entries are listed, the site becomes multilingual. The default language must be included.
site.main_sitemapunsetOptional link block back to a main/product site this deployment fronts. When unset, the default theme renders no outbound affordances.
site.main_site.urlstringrequiredMain-site URL the header wordmark and footer link target. Must be non-empty – an empty value is rejected at config load.
site.main_site.labelstringsite.nameWordmark text. Fallback applies only when unset; an explicit empty string renders an icon-only wordmark.
site.main_site.badge_labelstring"Docs"Text of the header chip that links back to this site’s own home page.
site.main_site.cta_labelstringunsetCall-to-action button text. The button renders only when both cta_label and cta_url are set.
site.main_site.cta_urlstringunsetCall-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, the base path is derived from the path component of site.url and logged at INFO. accent build --base-url replaces site.url for the build, so its path is the one derived from, and a --base-url with no path builds for the root. 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.

FieldTypeDefaultDescription
server.addressstring"127.0.0.1"Bind address. Use "0.0.0.0" to listen on all interfaces.
server.portinteger4400Bind port
server:
  address: "0.0.0.0"
  port: 8080

theme

Theme selection and directory settings.

FieldTypeDefaultDescription
theme.namestring"default"Active theme name (must match a directory under theme.directory)
theme.directorystring"./themes"Base directory containing theme directories
theme.embeddedstringunsetUse 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.

FieldTypeDefaultDescription
content.directorystring"./content"Root directory for site content
content.md_extension_redirectsbooltrueAuto-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.permalinksmap{}URL prefix-to-pattern map for date-based or custom URL structures
content.auto_index.enabledboolfalseGenerate 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.ignorelist["node_modules", "target"]Directory names skipped at every depth while auto-indexing
content.auto_index.gitignorebooltrueAlso skip whatever the tree’s .gitignore files skip, with git’s rules

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:

TokenSourceExample
:year4-digit year from frontmatter date2026
:monthZero-padded month from date01
:dayZero-padded day from date25
:slugFilename stem or frontmatter slug overridewelcome
:titleSlugified frontmatter titlewelcome-to-accent
:sectionParent section name from the prefixblog

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 :slug token value (e.g., slug: hello-world produces /blog/2026/01/hello-world from 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.

Where git ignores case for the repository that holds the content directory (core.ignorecase, which git sets when it creates the repository on a file system that ignores case), the .gitignore patterns match in any case, as git matches them: Secrets/ hides secrets/. The ignore list matches names as written on every machine. A request is judged by the names the directories store, so SECRETS/key.png, which a file system that ignores case opens as secrets/key.png, is hidden too, on any system.

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.

FieldTypeDefaultDescription
media.directorystring"./media"Shared media library directory
media.page_localbooltrueEnable page-local media serving (files alongside content .md files)
media.serve_at_page_urlbooltrueAlso 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.

FieldTypeDefaultDescription
media.processing.enabledboolfalseEnable image processing (resizing, format conversion)
media.processing.jpeg_qualityinteger85Default JPEG output quality (1-100)
media.processing.webp_qualityinteger80Default WebP output quality (1-100)
media.processing.max_dimensioninteger4096Maximum resize dimension in pixels
media.processing.responsive_widthslist[320, 640, 960, 1280, 1920]Responsive image breakpoints in pixels

media.cache

Cache for processed media files.

FieldTypeDefaultDescription
media.cache.directorystring"./.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_mbinteger500Maximum cache size in megabytes

media.attachments

Deliberate file downloads for extensions outside the serving allowlist.

FieldTypeDefaultDescription
media.attachments.extensionslist[]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 pdf or html) 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 md is 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-Disposition header for it. See offering file downloads.
  • The list is read per request, so a config reload (SIGHUP or POST /_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 directory whose URL another route answers has no bare-URL alias: /media/, /content-media/, /theme/, /tags/, and a configured taxonomy’s path. Its files stay reachable under /content-media/. With content.directory: ., the theme, models, data, and plugin directories are not page-local media at all, so neither route serves their files and accent build does not publish them.

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.

FieldTypeDefaultDescription
dev.hot_reloadbooltrueEnable file watching for templates, content, theme config, and assets
dev.browser_reloadbooltrueEnable browser auto-refresh via Server-Sent Events at /_dev/reload
dev.debounce_msinteger500Debounce delay in milliseconds before processing file changes
dev.show_draftsboolfalseShow pages with status: draft or status: review in frontmatter
dev.debugboolfalseEnable 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.

FieldTypeDefaultDescription
cache.max_size_mbinteger50Maximum cache size in megabytes
cache.ttl_secondsinteger3600Time-to-idle in seconds before cached entries expire
cache.warmingbooleanfalsePre-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.

FieldTypeDefaultDescription
http_headers.cache.enabledbooleantrueEnable cache headers in production
http_headers.cache.defaults.pagesstring"public, max-age=300, s-maxage=3600"Cache-Control for HTML pages
http_headers.cache.defaults.assetsstring"public, max-age=31536000, immutable"Cache-Control for CSS/JS/WASM whose URL is fingerprinted. Other CSS/JS/WASM, including /_search/*, gets it without immutable, and with a max-age or s-maxage above a day capped at an hour
http_headers.cache.defaults.mediastring"public, max-age=86400"Cache-Control for images/media
http_headers.cache.defaults.fontsstring"public, max-age=31536000, immutable"Cache-Control for font files whose URL is fingerprinted. Other font files get it without immutable, and with a max-age or s-maxage above a day capped at an hour. Files of cdn.font_proxy get public, max-age=31536000, immutable whatever this says, and its stylesheets public, max-age=86400
http_headers.cache.defaults.feedstring"public, max-age=3600"Cache-Control for RSS/Atom feeds
http_headers.cache.routesarray[]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.

FieldTypeDefaultDescription
http_headers.cors.enabledbooleanfalseEnable CORS headers globally
http_headers.cors.allowed_originsarray[]Allowed origins (use ["*"] for any)
http_headers.cors.allowed_methodsarray["GET", "HEAD", "OPTIONS"]Allowed HTTP methods
http_headers.cors.allowed_headersarray["Content-Type", "Accept"]Allowed request headers
http_headers.cors.expose_headersarray["ETag", "Last-Modified"]Response headers exposed to client
http_headers.cors.max_ageinteger86400Preflight cache duration in seconds
http_headers.cors.routesarray[]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.

FieldTypeDefaultDescription
http_headers.security.enabledbooleantrueMaster switch for all security headers
http_headers.security.content_type_optionsbooleantrueEmit X-Content-Type-Options: nosniff
http_headers.security.frame_optionsstring | null"SAMEORIGIN"X-Frame-Options value (null to omit)
http_headers.security.referrer_policystring | null"strict-origin-when-cross-origin"Referrer-Policy value (null to omit)
http_headers.security.content_security_policystring | null"object-src 'none'; base-uri 'self'; frame-ancestors 'self'"Baseline CSP for page responses
http_headers.security.media_content_security_policystring | null"default-src 'none'; sandbox"Strict CSP for served media responses
http_headers.security.script_policyobjectsee belowStrict script-src for pages served by accent serve
http_headers.security.routeslist[]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.

FieldTypeDescription
pathstringGlob pattern for the request path (required)
content_security_policystringCSP for matching responses; replaces the page or media default
frame_optionsstringX-Frame-Options for matching responses
referrer_policystringReferrer-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.

FieldTypeDefaultDescription
enabledbooleantrueAdd the directive. false keeps the baseline CSP only
report_onlybooleanfalseSend the directive in Content-Security-Policy-Report-Only, so browsers report violations without blocking them. The baseline stays enforced
sourceslist[]Extra script-src sources, appended after the nonce, such as https://www.googletagmanager.com or 'strict-dynamic'
report_uristring | nullnullreport-uri for violation reports
excludelist[]Paths, as route globs, that keep the baseline CSP only
report_only_pathslist["/_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-src or default-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, for a site with diagrams.enabled: true, the origin of diagrams.renderers.mermaid.cdn_url is added to sources. 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 search.enabled is true and the server runs the DocFind search backend, 'wasm-unsafe-eval' is added to sources, because the search runtime compiles a WebAssembly module in the browser. accent build decides the same way. The keyword allows WebAssembly compilation only, not JavaScript eval.
  • Paths in exclude, in static output. exclude applies to served pages only. To relax the policy for part of a static site, add a routes entry whose content_security_policy sets script-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 manifest of the last build into that directory, under .accent/build/ beside config.yaml: pass serve-static --config the config the site was built with. An output copied in from elsewhere, or built by another build, has none and is previewed without a script policy. 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.

FieldTypeDefaultDescription
build.fingerprintobjectdisabledAsset fingerprinting for cache busting – see the asset fingerprinting guide
build.header_artifactsarray[]Deploy-target response-header artifacts to emit at the output root

header_artifacts accepts "netlify" (writes _headers, and _redirects when build.routing or a build-derived redirect has anything to write) 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.

FieldTypeDefaultDescription
build.routing.error_documentstringunsetSite-absolute path served for a 404
build.routing.canonical_hoststringunsetThe one host this deployment answers on
build.routing.redirectsarray[]Redirects the host should serve, in order
build.routing.directory_listinghost or denyhostdeny adds Options -Indexes to the Apache artifact, which needs AllowOverride Options; host leaves listings to the host’s configuration
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 Apache target), and 303 as well in the netlify target.

The block is inert when absent, so a site that never sets it produces byte-identical output.

The netlify target writes these rules, plus the redirects the build already knows (a frontmatter redirect: with its code, a permalink or .md-extension move as a 301, each home page’s authoring URL), to a _redirects file beside _headers, with error_document as the /* catch-all on the last line. canonical_host is the exception: Netlify sets a primary domain in its own site configuration, so it has no _redirects form and configuring it with the netlify target fails the build naming the key. directory_listing shapes only the Apache file.

markdown

Toggles for individual pulldown-cmark markdown parser extensions. All extensions are enabled by default. Set any field to false to disable that extension.

FieldTypeDefaultDescription
markdown.tablesbooltrueGFM tables (| A | B | syntax)
markdown.footnotesbooltrueNew-style footnotes ([^label])
markdown.strikethroughbooltrueStrikethrough (~~text~~)
markdown.tasklistsbooltrueTask lists (- [x] / - [ ])
markdown.heading_attributesbooltrueHeading attributes (# Heading { .class #id })
markdown.wikilinksbooltrueWiki-style links ([[page]])
markdown.smart_punctuationbooltrueSmart punctuation (curly quotes, -- en-dash, --- em-dash)
markdown.mathbooltrueLaTeX-style math ($inline$, $$display$$)
markdown.definition_listsbooltrueDefinition lists
markdown.gfmbooltrueMiscellaneous GFM extensions (currently: block-quote alerts like > [!NOTE])
markdown.superscriptbooltrueSuperscript (^text^)
markdown.subscriptbooltrueSubscript (~text~). When enabled, single-tilde is subscript; double-tilde remains strikethrough.
markdown.metadata_blocksbooltrueYAML (---) and pluses (+++) metadata block parsing
markdown.localestring(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.

FieldTypeDefaultDescription
markdown.smart_quotes.inner_space.defaultenum(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_localemap(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.localesmap(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):

  1. markdown.smart_quotes.inner_space.per_locale[<tag>] – exact canonical match
  2. markdown.smart_quotes.inner_space.per_locale[<language>] – language-only fallback
  3. markdown.smart_quotes.inner_space.default
  4. Spec’s locales[<tag>].innerSpace
  5. 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.

FieldTypeDefaultDescription
code.themestring"base16-ocean.dark"Syntax highlighting theme name (uses syntect themes). Set to "" to disable highlighting.
code:
  theme: "InspiredGitHub"

diagrams

Diagram rendering. The diagrams guide covers the render modes, renderers and cache; this is the switch.

FieldTypeDefaultDescription
diagrams.enabledbooltrueRender diagrams. false renders every diagram as its source in a code block: a fenced block, a {% diagram %} tag, and a diagram() call in a template

feed

Feed generation. Only pages with a date field in frontmatter are included. RSS 2.0, Atom (RFC 4287), and JSON Feed 1.1 are supported.

FieldTypeDefaultDescription
feed.enabledbooltrueEnable feed generation
feed.limitinteger20Maximum number of entries in the feed
feed.pathstring"/feed.xml"URL path of the single-format feed
feed.formatrss | atom | jsonrssFormat of the single feed (ignored when outputs is set)
feed.outputslistSeveral feeds at once: each { format, path }. When set, replaces the single format/path feed
feed.includelist of URL prefixesScope the feed to these sections; empty means every dated page
feed.excludelist of URL prefixesSections excluded from the feed, applied after include

For absolute URLs in feed entries, set site.url.

feed:
  enabled: true
  limit: 20
  outputs:
    - { format: atom, path: /feed.xml }
    - { format: json, path: /feed.json }
  include: ["/posts/"]   # only posts, while they stay in the sitemap

On a multilingual site each output is written per language, the default language at its path and the others under their prefix (/de/feed.xml), as the RSS feed already was.

accent serve answers every configured output and every collection feed at the same URLs the build writes, with the format’s MIME type, so a reader subscribed against the dev server sees what the deployed site serves. Adding or removing an output needs a server restart; feed.enabled follows the live config.

Per-collection feeds. A page whose content: block sets feed: true gets its own feed at <its url>/feed.xml (in each configured format), listing that collection newest-first. A posts section page and an events section page each get /posts/feed.xml and /events/feed.xml from the collections they already declare, alongside the site-wide feed.

# posts/default.md frontmatter
content:
  items: "@self.children"
  feed: true

Per-term feeds. A taxonomies: entry with feed: true gets a feed per term at <path>/<slug>/feed.xml (in each configured format), listing the term’s dated pages newest-first under feed.limit, so a reader can follow one category. accent serve answers the same URLs. On a multilingual site each language’s terms get their feeds under the language’s prefix.

taxonomies:
  - name: categories
    singular: category
    path: /themen
    feed: true   # /themen/<slug>/feed.xml per term

Feed response caching. Outside dev mode – under --production, or whenever dev.browser_reload is off – accent serve caches each rendered feed body (the site feed, collection feeds, and per-term feeds) and answers repeated requests from the cache, so a reader that ignores the Cache-Control header no longer triggers a full re-render per poll. A content change, config reload, or hot reload invalidates the cached bodies with the page cache, and the bodies roll over at the UTC date change, so a scheduled publish_date or unpublish_date lands in feeds without a content change. Dev serve always renders fresh.

taxonomies

Classification axes beyond the built-in tags. Each entry gets what tags have: an index route, a per-term route with slugged URLs, taxonomy.<name> in every template, per-language pages on a multilingual site, and static build output. The built-in tags taxonomy is always present and is not listed here.

FieldTypeDefaultDescription
namestringrequiredThe plural name: the frontmatter key, the context key (taxonomy.<name>), the index template (<name>.html.jinja), and the default route (/<name>). Not name, singular, terms, pages, current_term, current_label or current_tag, which the taxonomy object already has, nor a taxonomy’s current_<singular> key, its own included
singularstringrequiredThe singular form: an alternate frontmatter key, the term template (<singular>.html.jinja), and the term-page context key (taxonomy.current_<singular>). Not label: taxonomy.current_label is the term’s printed label, not its name
pathstring/<name>The route the taxonomy’s pages live under; one segment, starting with /
feedboolfalseWrite a feed per term at <path>/<slug>/feed.xml (per configured feed.outputs format), listing the term’s dated pages
taxonomies:
  - name: categories
    singular: category
    path: /themen      # /themen and /themen/{slug}; default /categories

Pages opt in with the frontmatter key, as a single value or a list – categories: praxis and categories: [praxis] are the same page, and the category: spelling works too. A theme without categories.html.jinja or category.html.jinja renders the taxonomy through tags.html.jinja and tag.html.jinja. Those templates then list the categories only if they read taxonomy.terms and link through term_url(taxonomy.name, term), as the starter’s and the default theme’s do; one that lists taxonomy.tags shows the tags. See Configurable taxonomies for the keys a fallback template gets.

Adding or removing a taxonomy requires a server restart: the routes are registered at startup, so a SIGHUP config reload does not pick the change up.

Client-side search index generation. See the Search guide for usage details.

FieldTypeDefaultDescription
search.enabledbooltrueGenerate the search index
search.min_word_lengthinteger2Minimum word length for indexing
search.content_lengthinteger200Maximum characters of plain-text content stored per page
search.excludelist[]URL glob patterns to exclude (e.g., "/404", "/tags/*")

search.fields

Field weights for relevance scoring. Higher values rank matches in that field higher.

FieldTypeDefaultDescription
search.fields.titlefloat3.0Weight for title matches
search.fields.contentfloat1.0Weight for body content matches
search.fields.tagsfloat2.0Weight for tag matches
search.fields.leadfloat1.5Weight 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.

FieldTypeDefaultDescription
robots.enabledbooltrueEnable robots.txt serving at /robots.txt
robots.rulesstring(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/

static

A passthrough directory copied to the site root, for files that must sit at a fixed root path: favicon.ico, a search-engine verification file, humans.txt, ads.txt, a legacy /docs/*.pdf. Every file under it is served at the same path in accent serve and copied to the output root by accent build. The directory existing is what enables it.

FieldTypeDefaultDescription
static.directorystring"./static"Passthrough directory served and published at the site root
static:
  directory: ./static

static/favicon.ico answers at /favicon.ico. Only servable extensions are copied – the usual serving allowlist, your media.attachments.extensions (delivered as downloads), plus js and css. Dotfiles are skipped, and static/.well-known/ is refused in favour of the well_known config. Content pages and generated files (robots.txt, sitemap.xml, the feeds) win over a static file at the same path; a collision is logged and the generated file kept. Under a versioning root, a static file answers at its own path: accent serve and accent build both answer /docs/a.pdf with static/docs/a.pdf, where a version-less URL with no such file redirects to the default version. Files here are not fingerprinted – the point of the directory is stable paths; a file that wants hashing belongs in the theme. accent validate checks links against the directory by the same rule: a link to a file the build publishes here passes, and a link to one it skips is reported as broken. With hot reload on, accent serve watches the directory if it exists when the server starts, so adding or removing a file there updates the link findings in the admin and MCP reports without another edit; a directory created later, or moved by a configuration reload, is watched after a restart.

Incremental builds track the directory: accent build without --clean refreshes a static file you edited and removes one you deleted from the output, so the published tree always mirrors static/. The build records what it copied in its manifest and never touches a file at the same path that a page or a generated pass owns.

well_known

Serve files from a .well-known directory per RFC 8615. Useful for security.txt, ACME challenges, and protocol discovery.

FieldTypeDefaultDescription
well_known.enabledboolfalseEnable .well-known file serving
well_known.directorystring"./.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.

FieldTypeDefaultDescription
plugins.enabledboolfalseEnable plugin loading and execution
plugins.directorystring"./plugins"Directory containing installed plugins
plugins.fuel_limitintegerWASM instruction budget per plugin invocation (omit for unlimited)
plugins.render_budget_msinteger50Wall-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_cachestringDirectory for Wasmtime compilation cache (speeds up repeated loads)
plugins.precompileboolfalseAOT pre-compile plugins at load time for faster execution
plugins.allowed_pathsmap{}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.

FieldTypeDefaultDescription
hub.registry_urlstring(built-in)Registry base URL: the raw-content root of the registry’s default branch
hub.registry_public_keystringEd25519 SPKI public key a self-hosted registry signs with. Required whenever registry_url is not the default
hub.licensesmap{}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.

FieldTypeDefaultDescription
versioning.rootsmap{}Versioning roots keyed by URL prefix (e.g., /docs). Empty means versioning is disabled.
versioning.roots.<prefix>.versionsordered map(required)Version definitions. Iteration order determines dropdown display order.
versioning.roots.<prefix>.versions.<id>.labelstring(required)Human-readable label (e.g., "1.1 (Latest)")
versioning.roots.<prefix>.versions.<id>.badgestringOptional badge hint for templates (e.g., "latest", "lts")
versioning.roots.<prefix>.versions.<id>.fallbackstringVersion ID to fall back to when a page is missing in this version
versioning.roots.<prefix>.versions.<id>.statestringinferreddev (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>.defaultstring(required)Default version served when no version prefix is in the URL
versioning.roots.<prefix>.redirectsmap{}Deprecated version redirects (e.g., v0.9: v1.0)
versioning.roots.<prefix>.canonicalstringselfself: 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>.noindexlist[]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):

  1. Built-in defaults – Hardcoded in Accent CMS
  2. config.yaml – Or the file specified by --config
  3. 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