A static-site template for small informational and marketing sites, built on Eleventy and Node.js (≥22). Pages are assembled from composable, schema-validated content blocks written in YAML frontmatter, so engineering-adjacent authors (and AI assistants) can build and edit pages without touching templates.
Derived from the Chobble Template, relicensed to MIT here by its sole author, and cut down to an informational core: no e-commerce, no forms, no user data handling — just fast, accessible, static pages.
- Content blocks — ~35 block types (hero, FAQs, callouts, image cards,
split layouts, galleries, stats…) declared in frontmatter and validated at
build time with loud, file-specific errors. See BLOCKS_LAYOUT.md
for the generated schema reference, or the deployed site's
/blocks/page, where standalone-previewable types render next to YAML from the same tested fixtures. Collection-restricted contextual blocks show usage guidance instead. - Content types — Pages, News (with Atom feed), Guides (categorised documentation pages), and reusable Snippets.
- Multi-language — publish the same page in more than one language with
hreflangtags, anx-default, and a footer language switcher. See the Languages section below. - Theming — CSS custom properties throughout, ten prebuilt themes, a
visual theme editor at
/theme-editor/with export. - Images — responsive
srcsetvia eleventy-img, base64 LQIP placeholders, aspect-ratio cropping, unused-image detection. - Accessibility —
npm testchecks every built page with axe-core's automated WCAG 2.2 AA rules, including gallery coverage for blocks that support standalone previews. Pages get a skip link, named landmarks, and per-language chrome labels out of the box. Manual review is still required for context, visual contrast, keyboard flow, and other qualities automation cannot settle. - Search — static full-text search via Pagefind.
- SEO — schema.org JSON-LD (WebSite, Organization, BreadcrumbList, BlogPosting, FAQPage), canonical URLs, sitemap, social cards.
- Editing layer — a generated PagesCMS config
(
.pages.yml) wired to the block schemas, plusnpm run customise-cms, an interactive/non-interactive wizard that tailors the editor to the collections a site actually uses.
npm install # install dependencies (Node.js 22+)
npm run serve # dev server with hot reload
npm run build # build to _site/ (includes internal link check)
npm test # full suite: lint, typecheck, build, tests, coverage
npm run check:a11y # WCAG 2.2 AA check over the built _site/The build needs no application secrets or server-side services. Dependency
installation, uncached Iconify icons, and configured remote source images may
require network access. The deployable artifact is the _site/ directory —
publish it with any static host or pipeline.
skills/cfa-static-site-builder/ is a
portable Agent Skill for building and maintaining a
site from this template. Configure a compatible agent client to load that
directory according to the client's skill-discovery instructions. The package
travels with each fork and points agents back to the fork's live schemas,
generators, and checks rather than duplicating them.
Each site is a fork of this repository, not a dependency of it. A site's content, configuration, and theme live in the fork, and the template's own demo content is deleted or replaced there.
After cloning the fork, replace the name, url, and description in
src/_data/site.json before building. Missing or obvious placeholder identity
data fails the build rather than being published.
Replace this README too. Once the fork is the site, a README describing the
template misleads everyone who lands on the repository and hides the choices
the site made. Say what the site is, how it departs from the template's
defaults, and where its schemas and checks live — while keeping the pointers to
BLOCKS_LAYOUT.md, CLAUDE.md, and the /blocks/ gallery
that anyone editing the site will need. The skill's
project setup reference
lists what to cover, including provenance and how the site deploys.
Updates flow one way and only when a site asks for them:
git remote add upstream https://github.com/codeforamerica/cfa-static.git
git fetch upstream
git merge upstream/main # deliberate, reviewed, and never automaticThat is the point of the arrangement. A site that has shipped keeps building exactly as it built yesterday; template changes reach it when someone chooses to merge them, reviews what changed, and re-runs the site's own checks. The quality gates travel with the fork, so a site that pulls an update finds out immediately whether the update broke anything it publishes.
The repo ships a workflow (.github/workflows/pages.yml) that builds and
deploys to GitHub Pages on every push to main. One-time setup: under the
repository's Settings → Pages, set Source to GitHub Actions.
On a fork that keeps the Pages workflow, confirm Pages is enabled the same
way; the jobs run on standard ubuntu-latest runners, so no extra
integration or billing is required.
The workflow handles both hosting shapes automatically: on a project site
(https://<owner>.github.io/<repo>/) it builds with the /<repo>/ path
prefix and rewrites internal URLs to match; with a custom domain or a
user/organization site it builds with no prefix. Canonical URLs, the sitemap,
and feeds pick up the public site base URL via SITE_URL. Keep site.json's
url set to the site's public base URL as the fallback for local and
non-workflow builds.
The repo can also publish the same site unchanged to SharedServices, CfA's
Okta-protected internal hosting. The sharedservices-deploy.yaml workflow
builds _site/ from the same commit as the Pages deployment — differing only
in SITE_URL — then syncs it to the platform's static S3
bucket and invalidates the shared CloudFront distribution. Okta SSO is
enforced at the edge, so the site itself never handles authentication.
Deployment is manual while piloted: run Actions → Deploy to
SharedServices on main.
One-time setup is a DevOps task:
- Register the app by adding a spec to
shared-services-infra/tofu/configs/static-app/specs/and applying it. - Create the
sharedservicesenvironment on this repo with the variablesAWS_REGION,STATIC_BUCKET,STATIC_PREFIX(set tocfa-static),CLOUDFRONT_DISTRIBUTION_ID, andSITE_URL(the app's endpoint URL, with no trailing slash), plus theAWS_ROLE_ARNsecret from the static-app layer.
The public GitHub Pages deployment is unaffected. app.yaml at the repo root
declares the platform registration. SharedServices serves each app at the
root of its own subdomain (https://<name>.apps.<domain>), so the internal
build involves no path prefix — only SITE_URL differs from the public one.
For a step-by-step walkthrough of the one-time
setup — written for a DevOps engineer and whoever coordinates them — see
docs/devops-sharedservices-setup.html.
Both deploy workflows merge docs/ into the built site, so the walkthrough is
also served at /docs/ on each deployment.
src/_data/site.json— site name, URL, social linkssrc/_data/config.json— feature toggles (breadcrumbs, theme switcher, navigation style, search collections)src/_data/strings.json— news/guide label and permalink overridessrc/_data/languages.json/translations.json— languages the site publishes and which pages say the same thing in each
A site is written in one language until it says otherwise, and nothing in the template names a language.
_data/languages.jsonlists every language the site publishes, each with acode,hreflang,og_locale,label,home_urlprefix,home_label,breadcrumb_label,skip_to_content_label, andsearch_label. Exactly one entry hasis_default: true._data/translations.jsonpairs the pages that say the same thing, keyed by language code, e.g.[{ "en": "/about/", "de": "/de/ueber-uns/" }].
A page's language comes from its URL prefix. The template ships one language and no translations, which renders with no hreflang tags and no switcher.
- Biome linting, Knip dead-code detection, jscpd duplicate detection
- TypeScript checking via JSDoc, with a strictness ratchet
- 140+ test files (unit, integration, code-quality) with mutation testing
(
npm run mutation) - Nix flakes with direnv support
MIT.