Guide
Configuration reference
SSG can be configured with command-line flags or a YAML, TOML or JSON file. This guide explains the configuration model and advanced features. The exhaustive, copyable YAML template is .ssg.yaml.example.
Loading configuration
Select a file explicitly:
ssg --config path/to/site.yaml
Without --config, SSG checks the current directory in this order:
.ssg.yaml .ssg.yml .ssg.toml .ssg.json
ssg.yaml ssg.yml ssg.toml ssg.json
Command-line flags are parsed after the file and override matching file values.
The positional values source, template and domain are read from the file
when all three are present. Otherwise, provide all three positionally —
source itself is optional once content_sources is configured.
Two diagnostics make a misconfigured file obvious instead of silent:
- Unknown keys warn. A YAML key this binary does not know is reported by name and ignored. A config written for a newer ssg therefore still builds, and the version mismatch is visible rather than looking like a missing value.
- Missing required settings are named. Instead of printing usage alone, ssg
reports which of
source/template/domainis missing, which config file it read and what that file provided.
source: my-blog
template: simple
domain: example.com
ssg my-blog simple example.com
Most features are disabled by default. Defaults listed below come from the
current config.DefaultConfig; omitted strings and booleans otherwise use Go's
empty value.
Core and paths
| Key | Default | CLI | Purpose |
|---|---|---|---|
source |
required | positional | Local content collection |
template |
required | positional | Theme name |
domain |
required | positional | Canonical host without scheme |
content_dir |
content |
--content-dir |
Parent of local sources |
content_sources |
empty | --content-source (repeatable) |
Extra Markdown roots merged into the site; see CONTENT.md |
auto_excerpt |
false |
--auto-excerpt |
Derive a missing excerpt from the opening paragraph |
templates_dir |
templates |
--templates-dir |
Parent of themes |
output_dir |
output |
--output-dir |
Generated site destination |
static_dir |
static |
--static-dir |
Verbatim passthrough files |
data_dir |
data |
--data-dir |
YAML/JSON data for .Data |
pages_path |
pages |
config only | Pages directory inside a source |
posts_path |
posts |
config only | Posts directory inside a source |
quiet |
false |
--quiet, -q |
Suppress normal output |
output_dir is generated state. clean: true deletes its old contents before
building. See CONTENT.md for the source directory contract.
Template selection
| Key | Default | CLI | Purpose |
|---|---|---|---|
engine |
Go behaviour | --engine |
go, pongo2, mustache or handlebars |
online_theme |
empty | --online-theme |
GitHub, GitLab or direct ZIP theme URL |
The template core value names the destination/local theme directory. Engine
aliases accepted by the CLI include jinja2/django for Pongo2 and hbs for
Handlebars. Non-Go themes must ship their own templates in the chosen syntax.
See TEMPLATES.md.
Development server
| Key | Default | CLI | Purpose |
|---|---|---|---|
http |
false |
--http |
Start the built-in server after building |
host |
127.0.0.1 |
--host |
Bind address |
port |
8888 |
--port |
TCP port |
watch |
false |
--watch |
Rebuild after local file changes |
watch_runner |
"" |
--watch-runner |
Spawns a background watch runner process |
watch_runner_config |
"" |
--watch-runner-config |
Config file the runner should use |
watch_runner_dir |
"" |
--watch-runner-dir |
Directory the runner starts in |
clean |
false |
--clean |
Remove previous output before builds |
watch_runner coordinates background execution of development emulators (like wrangler or workerd). When configured, ssg automatically monitors files for rebuilds and spawns the runner in parallel, piping its output and terminating it on exit. Spelled --wrangler (for npx wrangler dev) or --workerd (for workerd serve) as CLI convenience flags.
watch_runner_config points the runner at a config file kept anywhere on disk,
so a wrangler.toml does not have to sit in the project root next to .ssg.
The path is passed as --config <path> to wrangler and to custom runners, and
as the positional config argument to workerd serve. A missing file is reported
as a warning; the runner is still started so its own error message is visible.
watch_runner_dir starts the runner in another directory — the monorepo case,
where the Worker lives in booking/apps/api/ while content and templates stay
at the repo root. Without it npx wrangler dev runs where ssg was invoked and
fails with "Missing entry-point to Worker script or to assets directory". A
relative watch_runner_config is resolved against ssg's working directory
before the runner is started, so both options can be combined safely. A
directory that does not exist aborts the runner (the build itself continues).
--wrangler-config=FILE, --wrangler-dir=DIR and the --workerd-* pair are
convenience spellings: each sets its value and selects that runner (so
--wrangler is implied), in any flag order. Use --watch-runner-config=FILE /
--watch-runner-dir=DIR with a custom --watch-runner.
# Worker in a subdirectory of the same repo (issue #35)
ssg --watch --wrangler-dir=booking/apps/api my-site simple example.com
# wrangler config kept in deploy/, not in the project root
ssg --wrangler-config=deploy/wrangler.toml my-site simple example.com
# equivalent, spelled out
ssg --watch-runner=wrangler --watch-runner-config=deploy/wrangler.toml \
my-site simple example.com
watch_runner: wrangler
watch_runner_dir: booking/apps/api
watch_runner_config: booking/apps/api/wrangler.jsonc
Pair it with environment variables in external_sources
to point the same config at the local Worker during development and at the
production API in CI.
watch monitors content, templates and data. Touch-only changes whose bytes are
unchanged do not trigger a rebuild; actual changes still cause a full build.
Use host: 0.0.0.0 only when the preview must be reachable from other machines.
Public TLS and hardening
http: true
port: 443
tls_cert: cert.pem
tls_key: key.pem
http3: true
gzip: true
max_conns: 1024
mem_limit: 512MiB
| Key | Default | CLI | Purpose |
|---|---|---|---|
tls_cert |
empty | --tls-cert |
Manual PEM certificate |
tls_key |
empty | --tls-key |
Manual PEM private key |
tls_auto |
false |
--tls-auto |
Obtain certificates with Let's Encrypt |
tls_domain |
empty | --tls-domain |
Autocert host names, comma-separated |
http3 |
false |
--http3 |
Add HTTP/3/QUIC alongside HTTPS |
gzip |
false |
--gzip |
Compress accepted responses |
max_conns |
0 |
--max-conns |
Connection limit; 0 is unlimited |
mem_limit |
empty | --mem-limit |
Go runtime soft memory limit |
TLS enables HTTP/2 automatically through ALPN. HTTP/3 requires TLS and uses the same UDP port. Manual certificate/key configuration takes priority over automatic certificates. Autocert requires a public domain and access to ports 80/443.
The server automatically applies X-Content-Type-Options, X-Frame-Options,
Referrer-Policy, HSTS under TLS, and cache-control suitable for HTML and
fingerprinted assets.
Output and URLs
| Key | Default | CLI | Purpose |
|---|---|---|---|
sitemap_off |
false |
--sitemap-off |
Disable sitemap.xml |
robots_off |
false |
--robots-off |
Disable robots.txt |
pretty_html |
false |
--pretty-html |
Remove blank lines from HTML |
relative_links |
false |
--relative-links |
Convert absolute site links to relative links |
post_url_format |
date behaviour |
--post-url-format |
date or slug |
page_format |
directory behaviour |
--page-format |
directory, flat or both |
permalinks.post |
empty | --permalink-post |
Tokenised post URL pattern |
permalinks.page |
empty | --permalink-page |
Tokenised page URL pattern |
rewrite_md_links |
false |
config only | Rewrite source .md links to final URLs (anchors and query strings are carried over) |
link_rewrites |
empty | config only | Map an href prefix to a replacement, for links to repository files the site never publishes |
preserve_slug_case |
false |
config only | Do not lowercase slugs |
outputs |
HTML only | --outputs=html,json |
Add per-page JSON output |
The permalinks map contains the optional post and page patterns. Permalink
tokens are :year, :month, :day, :slug and :category.
rewrite_md_links turns in-repository links (CONFIGURATION.md,
./guide.md#section) into the built page URLs, carrying any #anchor or
?query across. link_rewrites covers the other half of a documentation site:
links to repository files that the site never publishes. It maps an href prefix
to its replacement, longest match first, so one rule can cover a folder and
another override a single file:
link_rewrites:
"../examples/": "https://github.com/spagu/ssg/tree/main/examples/"
"../.ssg.yaml.example": "https://github.com/spagu/ssg/blob/main/.ssg.yaml.example"
With both set, check_links on a documentation site can reach zero warnings.
Frontmatter link always has higher priority. Detailed URL rules are in
CONTENT.md.
Minification and assets
| Key | Default | CLI | Purpose |
|---|---|---|---|
minify_all |
false |
--minify-all |
Enable HTML, CSS and JS minification |
minify_html |
false |
--minify-html |
Minify HTML only |
minify_css |
false |
--minify-css |
Minify CSS only |
minify_js |
false |
--minify-js |
Minify JavaScript only |
sourcemap |
false |
--sourcemap |
Emit v3 maps for minified CSS/JS |
fingerprint |
false |
--fingerprint |
Hash CSS/JS names and rewrite references |
scss |
false |
--scss |
Compile SCSS with Dart Sass |
sass_binary |
sass on PATH |
--sass-binary |
Explicit Dart Sass executable |
bundles |
empty | config only | Concatenate named CSS/JS groups |
Example bundles:
bundles:
app.css: [reset.css, layout.css, theme.css]
app.js: [vendor.js, main.js]
Bundles are created before minification and fingerprinting. Fingerprinting
renames CSS/JS to name.<hash8>.ext, emits assets-manifest.json, and rewrites
HTML/CSS references in dependency order. Source maps require corresponding CSS
or JavaScript minification. SCSS is removed from final output after compilation;
if Dart Sass is missing, the step is skipped with a warning.
HTML regions can opt out of minification:
<!-- htmlmin:ignore -->
<pre>Whitespace is preserved here.</pre>
<!-- /htmlmin:ignore -->
Images
| Key | Default | CLI | Purpose |
|---|---|---|---|
webp |
false |
--webp |
Convert copied JPG/PNG images to WebP |
webp_quality |
60 |
--webp-quality |
Quality from 1 to 100 |
webp_keep_original |
false |
--webp-keep-original |
Keep originals next to the .webp files |
reconvert_images |
false |
--reconvert-images |
Ignore existing conversion result |
image_sizes |
empty | --image-sizes |
Responsive widths; no upscaling |
image_sizes_attr |
100vw |
--image-sizes-attr |
Generated HTML sizes value |
WebP encoding requires the optional cwebp executable. Build-time resize,
crop, filter and source-set helpers are covered by IMAGES.md.
By default WebP conversion replaces each original in the output (the
historical behaviour): logo.png becomes logo.webp and <img src>
references are rewritten. Themes that hardcode extensions outside rewritten
attributes — favicons, logos, og:image — 404 in that mode. Set
webp_keep_original: true to emit the .webp next to the original: rewritten
references serve WebP, hardcoded ones keep working (v1.8.5).
Authoring
| Key | Default | CLI | Purpose |
|---|---|---|---|
sanitize_html |
false |
--sanitize-html |
Apply bluemonday's UGC policy to rendered content |
highlight |
false |
--highlight |
Highlight fenced code with Chroma |
highlight_style |
github |
--highlight-style |
Chroma style name |
toc |
false |
--toc |
Expose .TOC; ` |
- Loading configuration
- Core and paths
- Template selection
- Development server
- Public TLS and hardening
- Output and URLs
- Minification and assets
- Images
- Authoring
- Shortcodes
- Blog, feeds and search
- Taxonomies
- External sources
- Server access control
- SEO and validation
- Data and variables
- Internationalisation and timezones
- Build hooks
- MDDB content
- Archives and deployment
- Complete example
also expands | |toc_depth|3|--toc-depth| Maximum TOC heading level | |math|false|--math` | Inject KaTeX on pages containing math |
Math detection recognises display $$...$$ and fenced ```math
blocks (fences are rewritten to display math before rendering, GO-055).
Inline \(...\) is not supported — CommonMark backslash-escaping would
consume the delimiters. Sanitisation is recommended for untrusted remote
content; it is off for trusted local authoring to avoid changing intentional
HTML.
Shortcodes
Shortcodes are configured reusable snippets whose template file is required:
shortcodes:
- name: promo
template: shortcodes/promo.html
type: banner
title: Summer offer
text: Read the terms before continuing.
url: https://example.com/offer
logo: /images/offer.png
legal: Terms apply.
ranking: 4.5
tags: [public, featured]
data:
colour: green
Use `` in Markdown. The template receives .Name, .Type, .Title,
.Text, .Url, .Logo, .Legal, .Ranking, .Tags and .Data.
Enable WordPress-style syntax with:
shortcode_brackets: true
It supports attributes and paired content:
[link url="https://example.com" label="Read more"]
[box type="warning"]Inner Markdown content[/box]
Templates read inline values from .Attrs and paired text from
.InnerContent. Unknown bracket tags remain unchanged.
Site-wide variables: are reachable as .Vars.key / $.Vars.key, the same
spelling page templates use. Page context (.Page, .Site, .Posts, …) is
not in scope — one shortcode instance may render on many pages. The full
scope table is in TEMPLATES.md.
| Key | Default | CLI | Purpose |
|---|---|---|---|
shortcode_errors |
drop |
--shortcode-errors |
What a shortcode that fails to render leaves in the page |
drop— a warning, and the shortcode is removed from the page (historical behaviour, so existing sites build byte-identically).keep— a warning, and the shortcode's raw source (``,[promo a="b"]) stays in the page, so the failure is visible rather than shipping as a silently missing block.strict— askeep, and the build fails once rendering finishes, listing every shortcode that failed. Recommended in CI.
variables:
stripe_public_key: "pk_test_123"
shortcode_errors: strict
Blog, feeds and search
| Key | Default | CLI | Purpose |
|---|---|---|---|
paginate |
0 |
--paginate |
Posts per index page; 0 disables |
feed |
false |
--feed |
Root and category/tag Atom feeds |
feed_items |
20 |
--feed-items |
Maximum feed items |
feed_full_content |
false |
config only | Full rendered body instead of summary |
search_index |
false |
--search-index |
Emit search-index.json |
Pagination writes page 1 at the site root and pages 2 onward under /page/N/.
Themes receive .Pager. The search index contains title, URL, tags, excerpt,
plain text and the per-post taxonomies map, intended for a client-side search
widget.
Taxonomies
category, tag and series are built in. The config-only taxonomies: map
declares additional dynamic taxonomies with per-term archives, metadata files,
optional per-term feeds and template helpers — the full reference (keys,
frontmatter priority, normalization rules, template fallback chains) lives in
TAXONOMIES.md.
External sources
The config-only external_sources: block feeds templates from local files
(YAML/JSON/TOML/CSV/XML), remote HTTP APIs (hardened client + shared disk
cache), read-only SQL queries (MySQL/MariaDB/PostgreSQL/SQLite) and CMS
imports (WordPress, Drupal, Movable Type — merged into the site or exposed as
data). Everything lands under .ExternalData; .Data is unchanged. Secrets
come exclusively from environment variables. CLI: --offline,
--refresh-external-sources, --clear-external-cache,
--external-source=NAME. Full reference:
EXTERNAL_SOURCES.md.
Server access control
| Key | Default | CLI | Purpose |
|---|---|---|---|
server_auth |
empty | config only | basic or jwt (HS256); empty = open |
server_users |
empty | config only | Basic-auth users as login:$PASS_ENV |
jwt_secret |
empty | config only | HS256 shared secret, env reference |
ip_allowlist |
empty | config only | Only these IPs/CIDRs may connect |
ip_blocklist |
empty | config only | These IPs/CIDRs are refused first |
rate_limit |
0 |
config only | Requests/second per client IP |
rate_burst |
0 |
config only | Token-bucket size (default 2×rate) |
The chain runs blocklist → allowlist → rate limiter → auth, before the file
server. Passwords and the JWT secret must reference environment variables;
X-Forwarded-For is not trusted. SSO and LDAP are deliberately not
implemented.
SEO and validation
| Key | Default | CLI | Purpose |
|---|---|---|---|
seo |
false |
--seo |
Inject missing Open Graph, Twitter and JSON-LD metadata |
check_links |
empty | `--check-links[=warn | strict]` |
lastmod_from_git |
false |
--lastmod-from-git |
Use Git commit dates in sitemap |
SEO injection is non-destructive and skips pages that already provide their own
Open Graph tags. The old seo_off/--seo-off setting is a deprecated no-op.
Plain --check-links selects warning mode; strict mode fails the build.
Data and variables
Files below data_dir with .yaml, .yml or .json extensions are loaded by
path into .Data:
data/authors/ada.yaml → .Data.authors.ada
Custom variables are exposed as .Vars and exported to hooks as SSG_*:
variables:
analytics_id: $ANALYTICS_ID
api:
endpoint: https://api.example.com
Values beginning with $ resolve from the current process environment. Nested
keys are flattened for environment names, for example
SSG_API_ENDPOINT. Do not commit secrets to configuration files.
Internationalisation and timezones
languages: [pl, en]
default_language: pl
timezone: Europe/Warsaw
language_timezones:
en: America/New_York
pl: Europe/Warsaw
| Key | Default | CLI | Purpose |
|---|---|---|---|
languages |
empty | --languages=pl,en |
Enable multilingual output |
default_language |
empty | --default-language |
Language kept at the root |
For the opt-in expanded multilingual system, translation dictionaries and
prefix/fallback policies, see I18N.md.
| timezone | empty | --timezone | IANA zone for content dates |
| language_timezones | empty | config only | Per-language zone override |
Non-default languages are written below /<lang>/. Templates receive .Lang,
.Languages, .DefaultLanguage, .Translations and .Hreflang. Timezones
affect permalink calendar tokens and template dates; feeds and sitemap remain UTC.
Build hooks
Hooks execute trusted local commands without a shell:
hooks:
pre_build: [./scripts/prepare.sh]
post_build: [./scripts/report.sh]
post_page: []
| Phase | Timing | Failure behaviour |
|---|---|---|
pre_build |
Before generation | Fails the build |
post_page |
After each page | Logged and non-fatal |
post_build |
After generation | Fails the build |
Commands are argv-split, time-limited to 60 seconds, and never loaded from
content. Hooks receive SSG_OUTPUT_DIR, SSG_PHASE, and for page hooks
SSG_PAGE_PATH, plus exported custom variables.
MDDB content
MDDB replaces local Markdown with remote documents:
template: simple
domain: example.com
mddb:
enabled: true
url: http://localhost:11023
protocol: http
collection: blog
lang: en_US
api_key: "" # optional; prefer --mddb-key from a secret env value
timeout: 30
batch_size: 1000
watch: true
watch_interval: 30
| Nested key | Default | CLI |
|---|---|---|
mddb.enabled |
false |
enabled by --mddb-url |
mddb.url |
empty | --mddb-url |
mddb.protocol |
HTTP behaviour | `--mddb-protocol=http |
mddb.collection |
empty | --mddb-collection |
mddb.lang |
empty | --mddb-lang |
mddb.api_key |
empty | --mddb-key |
mddb.timeout |
30 |
--mddb-timeout |
mddb.batch_size |
1000 |
--mddb-batch-size |
mddb.watch |
false |
--mddb-watch |
mddb.watch_interval |
30 |
--mddb-watch-interval |
HTTP commonly uses http://localhost:11023; gRPC commonly uses
localhost:11024. MDDB watch polls the collection checksum and rebuilds when it
changes. Values beginning with $ are resolved only inside variables, not in
arbitrary configuration fields. In CI, pass an MDDB secret at runtime, for
example --mddb-key="$MDDB_API_KEY". Use sanitize_html when remote content is
not fully trusted.
Archives and deployment
| Key | Default | CLI |
|---|---|---|
zip |
false |
--zip |
targz |
false |
--targz |
tarxz |
false |
--tarxz |
deploy |
empty | --deploy |
deploy_project |
empty | --deploy-project |
deploy_branch |
provider default | --deploy-branch |
deploy_target |
provider-specific | --deploy-target |
Deployment credentials always come from environment variables. Provider details and GitHub Action inputs are in DEPLOYMENT.md.
Complete example
source: my-blog
template: simple
domain: example.com
content_dir: content
templates_dir: templates
output_dir: output
static_dir: static
data_dir: data
clean: true
minify_all: true
fingerprint: true
feed: true
search_index: true
seo: true
check_links: strict
webp: true
webp_quality: 80
image_sizes: [480, 960, 1600]
paginate: 10
outputs: [html]
Before relying on a key in automation, compare it with
.ssg.yaml.example and ssg --help from the installed
version.