Static Site Generation
dune build --static renders every published page to plain HTML files and copies all assets to a dist/ folder. The result can be uploaded to any static host — Netlify, Cloudflare Pages, GitHub Pages, an S3 bucket, or a plain web server.
Quick start
# Full static build
dune build --static
# Deploy (example: Netlify CLI)
netlify deploy --dir=dist --prod
That's it. The dist/ directory contains everything the static host needs to serve your site.
What gets generated
| File | Source |
|---|---|
index.html |
Home page |
{route}/index.html |
Every published content page |
flex/{type}/index.html |
Flex object list pages |
flex/{type}/{id}/index.html |
Flex object detail pages |
search/index.html |
Search page (JavaScript-driven on static hosts) |
sitemap.xml |
Generated from content index, using --base-url or config.site.url |
feed.xml, atom.xml |
RSS and Atom feeds |
robots.txt |
Copied from static/robots.txt, or a sensible default |
404.html |
Custom /404 page if present, otherwise a minimal fallback |
static/ |
Site-level static assets |
themes/{name}/static/ |
Active theme's static assets |
plugins/{name}/ |
Plugin assets |
content-media/ |
All co-located media files (images, PDFs, downloads, etc.) |
Options
dune build --static [options]
| Option | Default | Description |
|---|---|---|
--out <dir> |
dist |
Output directory |
--base-url <url> |
config.site.url |
Canonical base URL for sitemap and feeds |
--no-incremental |
— | Rebuild all pages, ignoring the change manifest |
--concurrency <n> |
8 |
Parallel page renders |
--hybrid |
— | Emit edge deployment routing config (see below) |
--include-drafts |
— | Include unpublished pages |
--verbose |
— | Print each rendered route |
--debug |
— | Verbose bootstrap output |
Examples
# Override base URL (important for correct sitemap URLs)
dune build --static --base-url https://example.com
# Force full rebuild
dune build --static --no-incremental
# High-parallelism rebuild for a large site
dune build --static --concurrency 16
# Verbose output to audit what was rendered
dune build --static --verbose
Incremental builds
By default Dune tracks a SHA-256 hash of each page's source file in dist/.dune-build.json. On subsequent builds, only pages whose source content has changed are re-rendered. This keeps CI builds fast even for large sites.
# First build — renders everything
dune build --static
# Output: 124 pages rendered
# Second build — content unchanged
dune build --static
# Output: 0 pages rendered, 124 pages skipped (unchanged)
# After editing one post
dune build --static
# Output: 1 pages rendered, 123 pages skipped (unchanged)
Disable incremental mode to guarantee a complete rebuild:
dune build --static --no-incremental
The manifest file
dist/.dune-build.jsonshould be committed alongside the rest ofdist/in CI/CD pipelines that preserve the build cache between runs.
Image handling
Image processing (the ?w=800&q=80 query-string resizing) runs on-demand in server mode but is skipped in static builds. Source images are copied as-is to dist/content-media/.
Most static hosts serve the raw file and silently discard query-string parameters, so themes that use image processing URLs (/content-media/hero.jpg?w=1200) continue to work — they simply get the full-resolution source image instead of a resized one. This is acceptable for the vast majority of static deployments.
If your theme relies heavily on server-side image resizing, continue running Dune as a dynamic server (
dune serve) and use a CDN in front for edge caching.
Hybrid mode
Use --hybrid when you want static pages served from a CDN but still need the admin panel and API endpoints to run on a server.
dune build --static --hybrid
In addition to the normal output, this writes three files that instruct edge platforms to route dynamic requests to your running server:
| File | Platform |
|---|---|
dist/_routes.json |
Cloudflare Pages |
dist/_redirects |
Netlify |
dist/_headers |
Security headers for all platforms |
The default dynamic routes are /admin/* and /api/*. Everything else is served statically.
Hybrid deployment workflow
Static files → CDN → served from dist/
API / admin → CDN → proxied to dune serve
For Cloudflare Pages:
- Connect your repository.
- Build command:
dune build --static --hybrid --base-url https://example.com - Output directory:
dist - Add a Pages Function at
/functions/[[path]].tsthat proxies to your Dune server for matched routes.
For Netlify:
- Build command:
dune build --static --hybrid --base-url https://example.com - Publish directory:
dist - The generated
_redirectsfile routes/admin/*and/api/*to a Netlify Function that proxies to your server.
Deployment examples
Netlify
# netlify.toml
[build]
command = "dune build --static --base-url https://example.com"
publish = "dist"
Cloudflare Pages
Configure in the dashboard or via Wrangler:
# wrangler.toml
[pages_build_output_dir]
directory = "dist"
Build command in dashboard: dune build --static --base-url https://example.com
GitHub Pages
# .github/workflows/deploy.yml
- name: Build
run: dune build --static --base-url https://username.github.io/repo
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
AWS S3 + CloudFront
dune build --static --base-url https://example.com
aws s3 sync dist/ s3://my-bucket --delete
aws cloudfront create-invalidation --distribution-id XXXXX --paths "/*"
Known limitations
- Image processing: source images are copied raw;
?w=…&q=…resizing only works in server mode. - Admin panel: not included in the static output; run
dune servealongside if you need it. - Real-time search: the
/searchpage is rendered as a static HTML shell; dynamic search results require JavaScript calling/api/searchon a running server. If no server is available, use a client-side search library (e.g. Pagefind) against the static output. - Forms:
POST /api/forms/*endpoints require a running server; forms in the static output will not submit without a backend.