SSG is a fast static site generator written in Go. It turns Markdown with YAML frontmatter into a complete website with clean URLs, templates, feeds, search, image processing and optional native deployment.
It works especially well for blogs and WordPress migrations, but can also build documentation, company sites, portfolios and landing pages.
Quick start · Content model · Configuration · Templates · Deployment · Documentation
- Fast, deterministic builds with a single Go binary
- Markdown content with YAML frontmatter
- Built-in
simpleandkrowythemes - Go, Pongo2, Mustache and Handlebars template engines
- Sitemap, robots.txt, Atom feeds, search index and SEO metadata
- WebP conversion, responsive images, SCSS, minification and fingerprinting
- Local server with automatic rebuilds
- Redirects engine with chain flattening and a
redirects()-config importer - Cloudflare Pages Functions integration for payments, forms and dynamic endpoints
- Native deployment to Cloudflare Pages, GitHub Pages, Netlify, Vercel, FTP and SFTP
- GitHub Action and multi-architecture Docker images
Most advanced features are opt-in. A basic command only reads content, renders
HTML and writes the result to output/.
Linux and macOS:
curl -sSL https://raw.githubusercontent.com/spagu/ssg/main/install.sh | bashOther supported installation methods:
| Method | Command or location |
|---|---|
| Homebrew | brew install spagu/tap/ssg |
| Snap | snap install static-site-generator && sudo snap alias static-site-generator ssg |
| Release binaries and packages | GitHub Releases |
| Docker Hub | docker pull tradik/ssg:latest |
| GitHub Container Registry | docker pull ghcr.io/spagu/ssg:latest |
| Build from source | make build |
For platform-specific instructions, see docs/INSTALL.md. Already running an older SSG? docs/UPGRADING.md lists every step between your version and this one — most upgrades need none.
The fastest way is ssg init, which scaffolds a ready-to-build project —
config, a content source with a sample page and post, a static/ folder and a
.gitignore — without overwriting anything that already exists:
ssg init my-blog --domain blog.example.com
ssg --config .ssg.yaml --http --watchOr lay it out by hand:
content/
└── my-blog/
├── metadata.json
├── pages/
│ └── about.md
└── posts/
└── general/
└── hello.md
content/my-blog/metadata.json:
{
"title": "My Blog",
"description": "Thoughts and notes",
"url": "https://example.com",
"language": "en",
"categories": [{ "id": 1, "name": "General", "slug": "general" }],
"users": [{ "id": 1, "name": "Editor", "slug": "editor" }],
"media": []
}content/my-blog/pages/about.md:
---
title: About
slug: about
status: publish
type: page
---
This is my first page.content/my-blog/posts/general/hello.md:
---
title: Hello World
slug: hello-world
status: publish
type: post
date: 2026-04-01
categories: [General]
author: 1
---
This is my first post.ssg my-blog simple example.com --http --watchOpen http://127.0.0.1:8888. SSG rebuilds the site when its files change.
The generated site is written to output/; do not edit that directory by hand.
The simple and krowy themes are embedded in the binary and scaffolded when
first used, so this example does not require a local templates/simple/ folder.
ssg <source> <template> <domain> [options]
| Argument | Meaning | Default location or use |
|---|---|---|
source |
Content collection name | content/<source>/ |
template |
Theme name | templates/<template>/ or an embedded theme |
domain |
Canonical host without a scheme | Canonical URLs, feeds, sitemap and SEO |
Example:
ssg my-blog krowy example.com --clean --minify-allThis reads content/my-blog/, uses the krowy theme, treats
https://example.com as the canonical site root and writes to output/.
All three values may instead be provided by a configuration file. In MDDB mode, content is fetched remotely and the source argument is optional.
SSG uses explicit locations and predictable output rules. These rules are useful both when authoring a site manually and when generating one programmatically.
project/
├── .ssg.yaml # optional configuration
├── content/
│ └── <source>/
│ ├── metadata.json # required for a local content source
│ ├── pages/ # recursively loaded pages
│ ├── posts/
│ │ └── <group>/ # at least one directory below posts/
│ │ └── post.md # deeper nesting is allowed
│ └── media/ # optional content media
├── templates/
│ └── <template>/ # optional when using an embedded theme
├── data/ # optional YAML/JSON template data
├── static/ # optional files copied verbatim
└── output/ # generated; safe to delete and rebuild
Important invariants:
- Pages are loaded recursively from
pages/. - Posts must be inside at least one subdirectory of
posts/. Files directly inposts/are ignored. Below the first subdirectory, nesting is recursive. - A post's category comes from its
categoriesfrontmatter, not its directory. - Local builds require
metadata.jsonat the root of the selected source. - Files with frontmatter are rendered only when
status: publishis present. - A plain Markdown file without frontmatter is treated as published content.
output/contains generated artifacts and must not be used as source content.
The directory names can be changed with pages_path, posts_path,
content_dir, templates_dir, data_dir, static_dir and output_dir.
For predictable results, pages and posts should define title, status and
type. Posts should additionally define date.
| Field | Type | Meaning |
|---|---|---|
title |
string | Display title |
slug |
string | URL segment; defaults to the Markdown filename |
status |
string | Only publish is rendered when frontmatter exists |
type |
string | page or post; affects templates and URL generation |
date |
date | Post publication date in YYYY-MM-DD form |
modified |
date | Last modification date |
categories |
list | Category IDs, names or slugs from metadata.json |
author |
integer/string | Author ID, name or slug from metadata.json |
tags |
list | Free-form tags; generates /tag/<slug>/ listings |
series |
string | Generates a /series/<slug>/ listing and navigation |
excerpt |
string | Summary for listings, feeds and metadata |
description |
string | SEO description; falls back to the excerpt |
link |
string | Explicit URL path; overrides normal URL rules |
canonical |
string | Explicit canonical URL |
aliases |
list | Previous paths that 301 to this item (and, by default, get a redirect stub) |
alias_stubs |
bool | Per-page override: false = 301 only (no duplicate copy), true = force a stub |
schema |
map | Override/extend this page's generated JSON-LD (deep-merged, per-page wins) |
featured_image |
string | Hero image; also the og:image, twitter:image and JSON-LD image (follows WebP conversion) |
layout |
string | Page layout name; redirect also marks sitemap exclusion |
template |
string | Page template file override |
robots |
string | Robots directive; noindex also excludes from sitemap |
sitemap |
string | Set to no to exclude from sitemap.xml |
lang |
string | Content language for multilingual builds |
Unknown frontmatter keys are preserved and exposed to templates. Author and category matching is case-insensitive; unresolved values are ignored.
Markdown can use explicit export-style sections:
## Excerpt
A short description used in listings.
## Content
The complete article starts here.Without these exact markers, all Markdown after the frontmatter becomes content.
SSG automatically detects .ssg.yaml, .ssg.toml or .ssg.json:
ssgAn explicit file can be selected with:
ssg --config path/to/site.yamlMinimal .ssg.yaml:
source: my-blog
template: simple
domain: example.com
clean: true
minify_all: trueCommon options:
| Goal | Configuration key | CLI flag |
|---|---|---|
| Development server | http: true |
--http |
| Automatic rebuilds | watch: true |
--watch |
| Cloudflare Wrangler | watch_runner: wrangler |
--wrangler |
| Cloudflare Workerd | watch_runner: workerd |
--workerd |
| Custom watch command | watch_runner: "cmd" |
--watch-runner="cmd" |
| Runner config outside the project | watch_runner_config: deploy/wrangler.toml |
--wrangler-config=deploy/wrangler.toml |
| Worker in a monorepo subdirectory | watch_runner_dir: booking/apps/api |
--wrangler-dir=booking/apps/api |
| Clean output first | clean: true |
--clean |
| Minify HTML/CSS/JS | minify_all: true |
--minify-all |
| Convert images to WebP | webp: true |
--webp |
| Responsive images | image_sizes: [480, 960] |
--image-sizes=480,960 |
| Fingerprint CSS/JS | fingerprint: true |
--fingerprint |
| Compile SCSS | scss: true |
--scss |
| Generate Atom feeds | feed: true |
--feed |
| Generate search index | search_index: true |
--search-index |
| Add SEO metadata | seo: true |
--seo |
| Validate frontmatter contracts | content_schemas: {post: {required: [title, date]}} |
config only |
| Fail the build on any violation | strict: true |
--strict |
Emit a route manifest (routes.json) |
route_manifest: true |
--route-manifest |
| Validate internal links | check_links: strict |
--check-links=strict |
| Fail on unrenderable shortcodes | shortcode_errors: strict |
--shortcode-errors=strict |
| Pull Markdown from other folders | content_sources: [{path: docs}] |
--content-source=docs |
| Derive missing excerpts | auto_excerpt: true |
--auto-excerpt |
| Rewrite links to repository files | link_rewrites: {"../examples/": "https://…"} |
config only |
| Create ZIP package | zip: true |
--zip |
WebP output requires the optional cwebp executable. SCSS compilation requires
the optional Dart Sass sass executable. Other native image operations use Go,
but selecting WebP as their output format also requires cwebp.
The canonical configuration reference is .ssg.yaml.example. The CLI also provides an installed-version reference:
ssg --help| Task | Command |
|---|---|
| Preview while editing | ssg my-blog simple example.com --http --watch |
| Production build | ssg my-blog simple example.com --clean --minify-all |
| WebP and responsive images | ssg my-blog simple example.com --webp --image-sizes=480,960,1600 |
| Feed, search and SEO | ssg my-blog simple example.com --feed --search-index --seo |
| Immutable asset names | ssg my-blog simple example.com --minify-all --fingerprint |
| Strict link validation | ssg my-blog simple example.com --check-links=strict |
| Create deployment archives | ssg my-blog simple example.com --zip --targz --tarxz |
| Use a Pongo2 theme | ssg my-blog my-theme example.com --engine=pongo2 |
| Use only configuration | ssg --config .ssg.yaml |
Options are composable unless a specific option documents otherwise.
This is a discovery index, not a second configuration reference. Exact defaults and accepted values live in .ssg.yaml.example.
| Area | Available capabilities |
|---|---|
| Authoring | Shortcodes, table of contents, syntax highlighting, KaTeX math, raw HTML sanitization |
| Blog | Pagination, tags, categories, series, reading time, Atom feeds, related content |
| Taxonomies | Custom dynamic taxonomies with term archives, metadata, per-term feeds and template helpers (docs/TAXONOMIES.md) |
| SEO and migration | Sitemap, robots.txt, aliases, configurable permalinks, canonical URLs, link checking, .md link rewriting |
| Redirects | redirects: → real Cloudflare/Netlify _redirects (splats, chain flattening, aliases as 301s), ssg import redirects from a JS redirects() config (docs/DEPLOYMENT.md) |
| Dynamic endpoints | Cloudflare Pages Functions via worker: + ssg new worker templates (contact form, Stripe, dynamic pricing, conversions proxy, cookie consent, comments, republish trigger), configurable _headers (docs/WORKERS.md) |
| Assets | WebP, responsive variants, build-time image helpers, SCSS, bundles, minification, source maps, fingerprinting |
| Data | YAML/JSON data files, custom variables and static passthrough files |
| External sources | Unified .ExternalData from local files (YAML/JSON/TOML/CSV/XML), HTTP APIs with a hardened client + disk cache, read-only SQL (MySQL/MariaDB/PostgreSQL/SQLite) and CMS imports (WordPress, Drupal, Movable Type) (docs/EXTERNAL_SOURCES.md) |
| Localisation | Full i18n: translation keys, dictionaries + t, language routing, hreflang/x-default, per-language feeds and search (docs/I18N.md) |
| Content sources | Local Markdown or MDDB over HTTP/gRPC, including watched remote content |
| Output | Directory/flat pages, JSON output, feeds, search index, ZIP, tar.gz and tar.xz |
| Server | File watching, gzip, TLS, automatic certificates, HTTP/2, HTTP/3, resource limits, basic/JWT auth, IP allow/block lists and per-IP rate limiting |
| Automation | Lifecycle hooks, Git-derived modification dates, GitHub Action and native deployment |
| AI assistance | Build-time [ai …] shortcode (models + agents with rules/skills, cached answers), ssg mcp development server with designer and content-manager roles and an approve-then-PR git flow (docs/CONFIGURATION.md) |
| Engine | Value | Syntax family |
|---|---|---|
| Go templates | go |
Go html/template; default and full helper support |
| Pongo2 | pongo2 |
Jinja2/Django |
| Mustache | mustache |
Logic-less Mustache |
| Handlebars | handlebars |
Handlebars blocks and helpers |
Select an engine with --engine=<value> or engine: <value>. Non-Go themes must
contain templates authored in their selected syntax; they do not receive the Go
template FuncMap or Go block inheritance.
A typical Go theme contains:
templates/my-theme/
├── base.html
├── index.html
├── page.html
├── post.html
├── category.html
├── css/
├── js/
├── layouts/
└── partials/
Missing standard templates receive built-in fallbacks. Themes may also be
downloaded with --online-theme=<URL>.
Common Go template values:
| Value | Meaning |
|---|---|
.Title, .Content, .Excerpt |
Current page/post content |
.URL, .CanonicalURL |
Relative and canonical URLs |
.Date, .Modified |
Content dates |
.Site.Pages, .Site.Posts |
Site collections |
.Data |
Data loaded from data/ |
.Vars |
Custom configuration variables |
.Pager |
Pagination state when enabled |
For collection helpers, conditionals and image functions, see docs/TEMPLATE_HELPERS.md and docs/IMAGES.md.
Depending on enabled features, output/ can contain:
output/
├── index.html
├── <page-slug>/index.html
├── <year>/<month>/<day>/<post-slug>/index.html
├── category/<category-slug>/index.html
├── tag/<tag-slug>/index.html
├── series/<series-slug>/index.html
├── css/
├── js/
├── media/
├── sitemap.xml
├── robots.txt
├── feed.xml
└── search-index.json
URL layouts can be changed with page_format, post_url_format, explicit
permalink patterns or an item's link field.
SSG can deploy the generated output without provider-specific CLIs. Credentials are read from environment variables, never from content files.
| Provider | Flag | Required environment |
|---|---|---|
| Cloudflare Pages | --deploy=cloudflare |
CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID |
| GitHub Pages | --deploy=github-pages |
GITHUB_TOKEN or SSH credentials |
| Netlify | --deploy=netlify |
NETLIFY_AUTH_TOKEN |
| Vercel | --deploy=vercel |
VERCEL_TOKEN (optional VERCEL_ORG_ID for team scope) |
| FTP | --deploy=ftp |
FTP_USERNAME, FTP_PASSWORD |
| SFTP | --deploy=sftp |
SSH_USERNAME + SSH_PASSWORD or SSH_KEY_FILE |
Example:
CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... \
ssg my-blog simple example.com \
--deploy=cloudflare --deploy-project=my-siteDeployment runs after generation and post-processing.
name: Build site
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: spagu/ssg@v1
with:
source: my-blog
template: simple
domain: example.com
clean: "true"
minify: "true"
For a site configured by .ssg.yaml — redirects, workers, variables — point the
action at the file instead; source/template/domain then come from it:
- uses: spagu/ssg@v1
with:
config: .ssg.yaml
deploy: cloudflare
deploy-project: my-siteAll supported inputs and outputs are defined in action.yml. Deployment workflow examples are available in examples/workflows.
Building SSG itself requires Go 1.26.5 or newer. Earlier Go 1.26 releases contain standard-library vulnerabilities relevant to this project.
git clone https://github.com/spagu/ssg.git
cd ssg
make allUseful targets:
| Command | Purpose |
|---|---|
make build |
Build build/ssg |
make test |
Run tests |
make test-coverage |
Run tests and generate coverage |
make lint |
Run static checks |
make security |
Run security scanners |
make golden |
Check generated output against the recorded baseline |
make determinism |
Check the worker count does not change the output |
make bench |
Measure build throughput on a synthetic corpus |
make all |
Dependencies, lint, tests and build |
make install |
Install the binary and manual page |
make golden and make determinism guard the two properties the test suite
cannot express as unit tests: that a change did not alter generated output, and
that parallel rendering produces the same site as a sequential build. Both run in
CI. After an intended output change, re-record the baseline with
make golden-update and commit it.
Development workflow and review requirements are in CONTRIBUTING.md. Please follow CODE_OF_CONDUCT.md. Existing contributors are listed in CONTRIBUTORS.md.
| Document | Scope |
|---|---|
| .ssg.yaml.example | Complete configuration reference |
| docs/INSTALL.md | Platform installation guide |
| docs/UPGRADING.md | Version-to-version upgrade steps, with a picker for your current version |
| docs/CONTENT.md | Content structure, frontmatter and URL rules |
| docs/CONFIGURATION.md | Configuration and advanced feature guide |
| docs/I18N.md | Internationalisation: translations, dictionaries, language routing |
| docs/TAXONOMIES.md | Dynamic taxonomies: definitions, term metadata, archives, helpers |
| docs/EXTERNAL_SOURCES.md | External data: files, HTTP APIs, SQL, CMS imports, cache, security |
| docs/TEMPLATES.md | Theme files, engines and rendering contexts |
| docs/TEMPLATE_HELPERS.md | Go template helper reference |
| docs/IMAGES.md | Build-time image processing |
| docs/DEPLOYMENT.md | Native providers, archives and GitHub Actions |
| docs/STYLES.md | Built-in theme style guide |
| examples/README.md | Example projects and workflows |
| CONTRIBUTING.md | Development and contribution workflow |
| CHANGELOG.md | Release history and migration notes |
| SECURITY.md | Vulnerability reporting policy |
SSG is distributed under the BSD 3-Clause License.