Local static-site generation showcase for Kujo projects, built around a single entrypoint: build.kujo.
SSG is designed for teams that want deterministic builds, straightforward template overrides, and a transparent content pipeline. Content, templates, assets, metadata, feeds, and validation stay visible in the repository instead of disappearing behind framework abstractions.
It fits the Clarity / Context / Control story by keeping content models, routes, feeds, and validation predictable, surfacing generated artifacts and metadata as context, and making builds and release checks local and reviewable.
- Builds Markdown pages, blog posts, and custom content collections (with per-type taxonomies)
- Renders full SEO metadata from frontmatter: canonical, Open Graph, Twitter Card, JSON-LD, absolute social images, and
article:*tags - Parses and formats dates, with RFC-822 RSS
pubDate/lastBuildDateand sitemaplastmod - Generates paginated home/blog listings with configurable sort order
- Produces
sitemap.xml,robots.txt,feed/index.xml,llms.txt,404.html, andfavicon.svg - Downloads and self-hosts any Google Font as cached
woff2, with an offline bundled fallback - Supports local and remote featured-image processing with deterministic output names
- Includes a reusable docs-site starter with docs templates, local search, DocGen update automation, and package generation
- Validates CLI behavior, config precedence, generated output, and release-gate checks with dedicated project scripts
- Ships with starter content, templates, assets, and lookup files for a real first-run build
- Deterministic content routing for pages, posts, and custom collections
- Config-file support for
yml,yaml, andjsonwith tested CLI override precedence - Simple override model based on files you can inspect and replace directly
- Release workflow backed by contract tests and generated-output validation
- Suitable for teams that want an auditable static-site pipeline rather than a black-box generator
- CMS demonstrates a server-first content application.
- CRUD API Showcase demonstrates a smaller API pattern.
- SSG demonstrates static publishing and docs-site generation.
- Lens and ShipCheck can help review and gate generated results.
- Not a production-certified docs platform
- Not an automatic deployment or hosted publishing service
- Not a guarantee of SEO, accessibility, or performance outcomes
- Not a replacement for manual review
- Not a hosted-service or paid-API dependency by default
- Kujo CLI available on your
PATH
Kujo gates host effects (filesystem, network, processes) behind explicit runtime
capabilities. The default kujo run ./build.kujo runs in trusted mode, where
every capability — including outbound network for font and remote-image downloads
— is granted. No extra flags are needed for normal builds.
If you build in hardened --untrusted mode, capabilities are denied by
default and you opt in per effect. A full build needs at least:
kujo run --untrusted \
--allow-fs-read --allow-fs-write --allow-fs-delete \
--allow-clock --allow-net-client \
./build.kujo -- --site-url https://example.com --fonts "Roboto,Lato"(--allow-net-client is only needed when the build downloads Google Fonts or
remote images; --allow-fs-* and --allow-clock are needed for every build.)
Note: passing any single --allow-* flag switches the runtime out of trusted
mode, so you must then enumerate every capability the build uses (filesystem
included). For most users the default trusted path is the right choice.
Build the bundled starter site from the project root:
kujo run ./build.kujo -- --site-url https://example.comExpected final lines include:
Build complete
Output directory: output
Preview the generated site locally on 127.0.0.1:
kujo serve output --port 8080For large sites, render post shards across CPU cores with the parallel orchestrator (mirrors how multiprocessing SSGs scale). Output is byte-identical to the single-process build (sitemap URL order aside):
# bash scripts/build-parallel.sh <shards|auto> <concurrency|auto> [build args...]
# `auto` sizes shards (~120 posts each) and concurrency (= CPU cores) for you:
bash scripts/build-parallel.sh auto auto \
--content content --output output --site-url https://example.com --posts-per-page 25Use many small shards (~120–300 posts each) and a concurrency near your core
count. Internally this drives build.kujo's --phase setup|posts|finalize and
--shard i --shards N flags; the default kujo run ./build.kujo remains a normal
single-process build. (The per-page render cost is interpreter-bound, so the
speedup is real but memory-bandwidth-limited — see
docs/performance-findings.md.)
Scaffold a starter config file when bootstrapping a new project:
kujo run ./build.kujo -- --init ymlFor a typical release workflow:
kujo run ./build.kujo -- --site-url https://example.com
bash scripts/validate-generated-output.sh output
bash scripts/run_ci_checks.shThe validated execution path for development, CI, and release checks is the standard Kujo VM path: kujo run ./build.kujo -- ...
Validate generated output after a build:
bash scripts/validate-generated-output.sh outputRun the local CI gate:
bash scripts/run_ci_checks.shRun the release gate:
bash scripts/run_release_gate.shThe release gate checks changelog/version alignment first, then runs the full CI gate on the same standard Kujo VM path used for normal builds.
If Kujo is not on your PATH, set a non-default runtime before running the gates:
KUJO_RUNTIME_DIR=/path/to/local-kujo-source-checkout
Use scripts/docgen_ssg_bridge.kujo to turn Kujo DocGen output into normal
reviewable SSG Markdown content. The bridge can run kujo docgen, read the
stable docgen-summary/v1 JSON payload, generate frontmatter-backed Markdown
under a configured content root, remove stale generated files from its manifest,
then build and validate the site.
Typical strict CI/update command:
kujo run scripts/docgen_ssg_bridge.kujo -- \
--target-repo /path/to/repo \
--content-out content/reference \
--docgen-out .docgen/output \
--cache-dir .docgen/cache \
--site-url https://docs.example.com \
--source-link-template 'https://github.com/org/repo/blob/main/{path}#L{line}' \
--strictThe generated content includes an overview page, per-language pages, per-module
pages, and a documentation-gaps page. Frontmatter is emitted in stable key order
with docgen_generated: true, docgen_schema_version: "docgen-summary/v1",
source metadata, and count fields. Re-running the same DocGen output should not
change the generated Markdown.
Gate controls:
--strict: fail when DocGen reports gate failures and default undocumented threshold to zero.--max-undocumented <n>,--max-broken-links <n>,--max-warnings <n>: configure count thresholds.--max-discovery-skips <n>: fail when any discovery skip counter exceeds the threshold.--allow-adapter-low-yield: permitDOCGEN_ADAPTER_LOW_YIELDdiagnostics.--skip-buildand--skip-validation: useful for fixture tests or previewing Markdown conversion only.
The bridge only deletes files listed in
<content-out>/.docgen-ssg-manifest.json, and it refuses content, DocGen output,
or cache paths that escape the SSG root.
The reusable documentation template lives under starters/docs-site/. It packages
the universal documentation features that are useful across projects without
requiring a hosted service:
- Docs-specific page and reference templates
- Structured page frontmatter for section, audience, difficulty, status, version, prerequisites, previous/next links, nav labels, and search exclusion
- Local search index generation through
scripts/docs_search_index.kujo - One-command generated-reference refresh through
scripts/update_docs.kujo - Deterministic generated Markdown and manifest-scoped stale cleanup through the DocGen bridge
- Local assets and JavaScript for search and code-copy controls
Create a downloadable starter packet with:
bash scripts/package-docs-template.shThat writes dist/kujo-ssg-docs-template.tar.gz with the starter content,
templates, assets, build.kujo, and the docs automation scripts.
For a docs-site update loop, run this from a docs starter checkout:
kujo run scripts/update_docs.kujo -- \
--target-repo /path/to/repo \
--site-url https://docs.example.com \
--source-link-template 'https://github.com/org/repo/blob/main/{path}#L{line}' \
--strictFor an exploratory language-site preview that includes internal/example symbols and builtin/native APIs, opt in explicitly:
kujo run scripts/update_docs.kujo -- \
--target-repo /path/to/repo \
--languages kujo \
--include-private \
--include-builtins \
--site-url http://127.0.0.1:4178The update script runs DocGen with an incremental cache, bridges only generated
reference content into content/reference/generated, refreshes the local search
index, builds the site, and validates the generated output.
Core directories in a standard project:
content/: Markdown source content and taxonomy lookup filestemplates/: page, listing, and item template overridesassets/: static assets copied or processed into outputoutput/: generated site artifacts; do not edit by handscripts/: validation and release automation
build.kujo loads config from the first file found in this order:
kujo-ssg.ymlkujo-ssg.yamlkujo-ssg.json
CLI flags always override config values.
Example kujo-ssg.yml:
site_url: https://example.com
site_title: My Site
site_tagline: Built with Kujo SSG
output: output
content: content
templates: templates
assets: assets
blog_slug: blog
posts_at_root: false
posts_per_page: 5
sort_by: date
robots: public
llms: public
watch: false
minify: false
download_remote_images: false
no_index: false
no_aux: falseExample one-off override build:
kujo run ./build.kujo -- \
--site-url https://staging.example.com \
--output preview-output \
--blog-slug updates \
--posts-per-page 1 \
--sort-by dateThat keeps file-based defaults in place while applying the CLI values for the current run only.
--output <dir>: output directory--content <dir>: content directory--templates <dir>: templates directory--assets <dir>: assets directory--posts-per-page <n>: listing pagination size--sort-by <date|title|author|order>: blog listing sort mode--fonts <comma,list>: heading/body font families (first = headings, second = body). Bundled families (Bree Serif,Inter,Quicksand,Open Sans) render offline; any other family is downloaded from Google Fonts aswoff2and cached under.cache/fonts/. See Fonts.--site-title <text>--site-tagline <text>--site-url <url>--robots <public|private>--llms <public|private>--watch: reserved, currently not implemented--minify: emit minified CSS/JS assets--download-remote-images: mirror remotefeatured_imageURLs into output (needs outbound network — see Runtime Capabilities)--drafts: includedraft: truecontent in the build (preview/staging workflow); omitted by default--blog-slug <slug>: blog route base--posts-at-root: keep post permalinks at/<slug>/while retaining the blog listing under/<blog_slug>/--init <yml|yaml|json>: scaffold starter config--no-index: skip index and blog listing pages--no-aux: skip feed, sitemap, robots, and llms outputs--no-aliases: skip flat.htmlredirect aliases (emit cleandir/index.htmlonly); halves per-page write I/O on large sites--version--help
Unknown flags, missing option values, malformed YAML/JSON config, invalid booleans, and invalid enum values fail fast with a nonzero exit.
content/pages/*.md: standalone pagescontent/posts/*.md: blog postscontent/<type>/*.md: custom content collection items
Routes are generated like this:
- Pages:
/<slug>/ - Posts:
/<blog_slug>/<slug>/by default, or/<slug>/withposts_at_root: true - Custom collections:
/<type>/<slug>/ - Collection listing pages:
/<type>/
Frontmatter keys supported across pages, posts, and custom types:
titledescriptionkeywordsseo_titleseo_descriptionauthorlangcanonicalfeatured_imagecustom_urltemplatedraftorderexcerptnav_hidenav_titlesectionaudiencedifficultystatusversionprerequisitespreviousnextlast_updatedsearch_excludetagscategoriestaxonomies
Frontmatter opens and closes only with a line containing ---. Literal ---
text in a quoted value or Markdown body is preserved. An unclosed or malformed
frontmatter block is left out of metadata processing and produces a warning with
the source file and delimiter line.
draft: true excludes content from generated public outputs. Pass --drafts to
include draft content in a build for preview/staging without publishing it by
default.
Lookup files live in content/.
- Global lookup:
content/<taxonomy>.yml - Content-type-specific lookup:
content/<content_type>-<taxonomy>.yml
Resolution precedence:
- Content-type-specific lookup
- Global lookup
That lets you reuse shared taxonomies globally while still overriding them for a single content type when needed.
Example:
# content/storefronts-location.yml
1:
name: Austin
2:
name: Denver---
title: Main Street Storefront
location: [1, 2]
taxonomies:
district: downtown
tags: [flagship, retail]
---Top-level shorthand such as location: [1, 2] is lookup-aware. taxonomies: is the right place for free-text custom taxonomy values.
The repository starter content is meant to be immediately useful, not empty.
- Pages:
about,contact,getting-started - Posts: multiple sample blog entries
- Custom content types: storefronts, tshirts, pants, shorts
- Lookup files: authors, categories, tags, color, and a type-specific storefront location lookup
- Template overrides: dedicated home, blog, storefront listing, storefront item, and custom post templates
Recommended first customization points:
kujo-ssg.ymltemplates/content/
For most teams, the fastest path to a working site is:
- Set
site_url,site_title, andsite_tagline - Replace starter content in
content/ - Override the templates you actually need in
templates/ - Run the validation and release gates before publishing changes
featured_image supports both local and remote sources.
- Local paths are resolved relative to the content file,
content/, andassets/ - Each existing local candidate is canonicalized and must remain under the content root, assets root, or the content file's directory; traversal and symlink escapes are rejected with a source-file warning
- Processed assets are written to
output/images/with deterministic names - Raster images are converted to WebP when possible
- If conversion fails, Kujo SSG falls back to the original extension
- Remote images are only downloaded when
download_remote_images: trueor--download-remote-imagesis enabled
For deterministic CI and release builds, leave remote downloads disabled unless the build explicitly needs mirrored remote assets.
An unresolved or rejected local image is omitted rather than emitted as a raw path in generated HTML.
--fonts "Headings,Body" (or fonts: in config) selects the heading and body
type families. There are two provisioning paths:
- Bundled families —
Bree Serif,Inter,Quicksand,Open Sansship as localwoff2and render fully offline with zero network access. These are the defaults (Bree Serifheadings,Interbody), so a stock build is always deterministic and offline. - Any Google Font — request any family by name (e.g.
--fonts "Roboto,Lato") and Kujo SSG fetches thelatinwoff2files for weights 400/700 directly from Google Fonts, writes them tooutput/assets/fonts/, generates the matching@font-facerules inoutput/assets/css/fonts.css, and caches the downloads under.cache/fonts/(git-ignored). Subsequent builds reuse the cache and stay offline.
Google Font downloads need outbound network access. The default
kujo run ./build.kujo path runs in trusted mode (all runtime capabilities
granted), so downloads work with no extra flags. Only if you run the build in
hardened --untrusted mode do you need to opt in — see
Runtime Capabilities.
If a requested Google Font cannot be provisioned (no network, missing capability
in --untrusted mode, or an unknown family name), the build prints a warning and
falls back to the bundled default without failing — so CI never breaks on a font
typo.
This keeps the project decentralized: you are never locked into a small curated font list, but the default path remains fully self-contained.
Every generated page includes, derived from frontmatter and config:
<title>,description,keywords,author, andlang- Canonical URL (explicit
canonical:or computed fromsite_url+ route) - Open Graph:
og:title,og:description,og:url,og:type(articlefor posts/items,websiteotherwise),og:site_name,og:locale, and an absoluteog:image - Twitter Card:
summary_large_imagewith title, description, and an absolutetwitter:image article:published_time/article:authorfor posts and collection items- JSON-LD structured data (
BlogPostingfor articles,WebSiteotherwise) - An SVG favicon (
/favicon.svg) and RSS autodiscovery<link>
Social image URLs are emitted as absolute URLs (using site_url) so link
unfurlers on Facebook, X/Twitter, LinkedIn, and Slack resolve them correctly.
Post and collection-item date: frontmatter is parsed (ISO YYYY-MM-DD,
YYYY/MM/DD, MM/DD/YYYY, Month DD, YYYY, and DD Month YYYY are all
accepted), rendered for display as Month DD, YYYY, used for stable date
sorting, and emitted as RFC-822 pubDate/lastBuildDate in the RSS feed and
YYYY-MM-DD lastmod in the sitemap.
Built-in override points include:
templates/page-home.html: home listing (/,/page/N/)templates/page-blog.html: blog listing (/blog/,/blog/page/N/)templates/page-<blog_slug>.html: listing override when blog slug is notblogtemplates/page-<content_type>.html: custom collection listing overridetemplates/post-<template>.html: per-post or per-item template override
Listing templates receive placeholders including:
{{title}},{{description}}{{cards}},{{items}},{{posts}},{{posts_html}}{{pagination}}{{page_number}},{{total_pages}}{{blog_slug}},{{content_type}}
Primary outputs include:
output/index.htmloutput/page/N/index.htmloutput/blog/index.htmloutput/blog/page/N/index.htmloutput/feed/index.xmloutput/sitemap.xmloutput/robots.txtoutput/llms.txtoutput/404.htmloutput/favicon.svg
sitemap.xml uses the sitemaps.org schema with absolute URLs, per-route
changefreq/priority, and lastmod from post dates. The RSS feed carries
pubDate/lastBuildDate. Generated HTML includes canonical, Open Graph,
Twitter Card, JSON-LD, and standard metadata derived from frontmatter and config
(see SEO And Social Metadata).
Public llms.txt output includes Posts and Pages sections followed by one
section per built custom collection. Each collection section links to its index
and every non-draft item using absolute URLs; collection sections are ordered by
content directory name, and items follow the configured sort_by order.
- If Kujo is not on your
PATH, setKUJO_BINorKUJO_RUNTIME_DIRbefore running builds or gates. - If a build succeeds but output validation fails, rerun
bash scripts/run_ci_checks.shto recheck CLI contracts, generated-output contracts, the VM build, and HTML validation in one path. - If you only need content pages and item routes, use
--no-auxto skip feed/sitemap/robots/llms generation. - If you want item routes without listing pages, use
--no-index.
- Main pipeline entrypoint: build.kujo
- Agent/contributor guide: AGENTS.md
- Main contract tests:
scripts/test-cli-contract.sh,scripts/test-generated-contract.sh - Primary validation path:
bash scripts/run_ci_checks.sh - Release gate:
bash scripts/run_release_gate.sh