| Component | Details |
|---|---|
| Static site generator | Hugo extended_0.164.0 |
| Styling | Hand-written CSS on design tokens, cascade layers, no framework |
| Typography | Inter Variable, self-hosted, latin subset, weight axis only |
| JavaScript | ~150 lines, no dependencies, bundled by js.Build, deferred |
| Hosting | Cloudflare Workers static assets |
| Deploy | npx wrangler deploy — not wrangler pages deploy |
Every shipped asset is fingerprinted and carries a Subresource Integrity digest. There is no Node dependency in the build itself; npm is only used for Wrangler.
asdf plugin add gohugo && asdf install gohugo extended_0.164.0
asdf plugin add nodejs && asdf install nodejs 24.19.0
hugo version # must print +extendedCreate a local .tool-versions if you use asdf:
gohugo extended_0.164.0
nodejs 24.19.0
Do not commit it. Cloudflare Workers Builds parses any .tool-versions in
the repository root and tries to install every tool named in it; the asdf
plugin names are not the ones its build image knows, and the build fails at the
Installing stage before it ever reaches Hugo. The file is in .gitignore for
that reason. Hugo's version on Cloudflare comes from the HUGO_VERSION build
variable; CI pins its own copies in .github/workflows/ci.yml.
Hugo's extended edition is pinned because it carries the LibSass transpiler.
The site does not currently use Sass, so a plain build would also work today —
the pin exists so that adding a .scss file later does not silently fail.
Full git history is required: enableGitInfo reads the git author date to
populate .Lastmod for sitemap.xml, and a shallow clone yields no dates
without erroring. The prebuild npm script unshallows for exactly this reason.
npm run server # http://localhost:1313, live reload
npm run build # production build into public/
npm run deploy # build, then wrangler deploysrc/index.html The etalon. Read-only, chmod a-w. Never edit.
hugo.toml Config. The release number lives here and nowhere else.
content/_index.md Front matter for the home page.
data/install.yaml Install commands, one definition per method.
data/adapters.yaml The integrations grid.
layouts/
baseof.html Document shell.
home.html The landing page itself.
404.html
robots.txt Generated, so the sitemap URL follows baseURL.
_partials/ head, meta, schema, header, footer, install-tabs, ...
assets/
css/main.css One stylesheet, layered, token-driven.
js/main.js One script.
fonts/ Inter Variable, woff2.
img/logomark.svg The chevron mark. Inlined into the page.
img/icon.svg Mark on a navy rounded square. Source for every icon.
img/og-image.jpg Social card, 1200x630.
scripts/
generate-icons.mjs Regenerates static/ icons from assets/img/icon.svg.
static/ Generated icons, manifest, _headers. Copied verbatim.
Everything derives from one vector. assets/img/icon.svg carries geometry
extracted from the brand master (Sortie AI Logo_E.svg, Illustrator, viewBox
0 0 2000 2000): the four cyan polygons that form the chevron S. The wordmark
is deliberately not in the SVG — "Sortie" is set as HTML text beside the
mark, so baking it in would duplicate it and make it unselectable.
Placement was measured from the brand's own 256px favicon export rather than
guessed: corner radius 38/256, mark 192px wide, centred at (128, 126.5).
Rendering icon.svg at 256px and comparing against that master gives RMSE
0.04, all of it edge antialiasing.
To regenerate every icon after a brand change:
node scripts/generate-icons.mjs # needs ImageMagick for the .icoThe mark inherits currentColor, so the BRAND token swap recolours it along
with the rest of the page. favicon.svg is real geometry, not a bitmap in an
SVG wrapper.
The site was rebuilt from a hand-written single-file page. That file is kept, read-only, as the reference for the copy and the design. When changing the site, diff the rendered output against it:
hugo --gc --minify
# then compare the visible text of public/index.html against src/index.htmlThe only intended text differences are two factual corrections, documented in
.research/etalon-inventory.md.
The release number. params.version in hugo.toml is the single place it
lives. It feeds the nav chip, the footer, the release link and the JSON-LD.
The etalon had it hardcoded in three places.
Immutable caching. static/_headers marks /css/*, /js/* and /fonts/*
as immutable for a year. That is safe only because Hugo fingerprints those
filenames. Never add a path served from static/ to those rules — filenames
there are stable, and a year-long cache would strand the edit. /img/* is
deliberately not immutable, because the social card's URL must stay constant.
JSON-LD needs safeJS. Go's html/template treats anything inside a
<script> as JavaScript and will escape a jsonify result into a quoted
string. The block then parses as a JSON string, not an object, and every
validator rejects it silently. See the comment in layouts/_partials/schema.html.
The font URL is injected. assets/css/main.css is run through
resources.ExecuteAsTemplate so the @font-face src carries the font's own
fingerprint. A literal relative path would resolve against the fingerprinted
stylesheet's directory and 404.
Order of operations in the pipeline. Minify, then fingerprint. Reversing them hashes the unminified bytes, so the filename does not match what ships.
Cloudflare Workers, not Pages. Cloudflare's guidance since April 2025 is to
start new projects on Workers; Pages remains supported but receives no new
feature investment. wrangler.toml uses [assets] directory, which is the
Workers form — a Pages project would carry pages_build_output_dir instead.
The documentation site at docs.sortie-ai.com runs the same way, from
sortie-ai/docs.
The Sortie product is Apache-2.0.