codekredo

← All notes

A multilingual static site in plain Python

XCOMBO Codes has close to 1,000 pages in five languages, but no content management system, no database and no framework. One Python script that only uses the standard library turns a handful of data files into finished HTML. This note describes the setup and the small details that turned out to matter.

29 October 2026

Why static

Every visitor sees the same pages, and the content changes about once a month. That's the ideal case for a static site: the server only hands out finished files. It's fast, needs almost no resources, and there's very little to attack – no admin login, no plugins to keep up to date, no database to leak. The only code that runs on the server is the small endpoint behind the submission form (Spam protection without third parties: proof of work).

Data in, HTML out

The content lives in plain Python modules: a list of codes with their function, category and conflict group; translations for each language; a long description for every code; and the guides. The build script loops over the five languages and writes the code list, one page per code, the guides, the sets, the change log, the privacy policy and a sitemap. A full build of roughly 970 pages takes well under a second.

Guides aren't written in HTML or Markdown but as a list of simple blocks – a paragraph, a heading, a list, a note – so every language version has exactly the same structure:

body=[
    ("h2", "All conditions must be true at the same time"),
    "A combo only fires if all its conditions are met. {c:44790023} gets it right …",
    ("ul", ["First point", "Second point"]),
]

Placeholders instead of hand-written links

Inside the text, placeholders stand for links: {c:…} for a code, {g:…} for another guide, {s:…} for a set. The build turns them into links with the right address and the right title in each language, and the code is formatted the same way everywhere. A placeholder for a guide or set that doesn't exist stops the build. Before publishing, a short check walks through all generated pages and reports broken internal links, duplicate titles and duplicate descriptions.

Five languages, done properly

Cache busting without a bundler

The server lets browsers keep stylesheets and scripts for a day. Without precautions, visitors would get new HTML with yesterday's CSS after an update. So every asset is linked with a short hash of its content; when the file changes, the address changes too:

def asset(name):
    h = hashlib.sha256((SRC / "assets" / name).read_bytes()).hexdigest()[:10]
    return f"/assets/{name}?v={h}"

Honest dates in the sitemap

A sitemap can tell search engines when each page last changed. Google says it only uses that date if it's consistently accurate – and a static build that stamps every page with today's date on every run is the opposite of accurate.

So the build keeps a small file with a content hash and the date of the last real change for each page. A page only gets a new date when its content actually changes; the asset hashes above are ignored for this comparison. The file is committed together with the content.

Works without JavaScript

The complete code list is in the HTML. Search, filters, “My combos” and sharing are added on top by a small script, so the site stays usable if scripts are blocked or fail. Our own pages use no inline scripts and no style attributes, which keeps the Content Security Policy simple.

Publishing

One command builds the site, packs it, sends it to the server over SSH and syncs it into place with rsync. The sync deletes files that no longer exist and compares checksums, so only files whose content changed are transferred. There's no build server and no pipeline – for a site of this size, none is needed.

Where this approach ends

Keeping content in code works well for a small team that edits it in a text editor and tracks it in git. It's less suited to editors who expect a visual interface, or to sites where many people publish at the same time. For our projects, the simplicity has been worth more than those features.

You can see the result on xcombos.codekredo.com; this site, www.codekredo.com, is built the same way.

More notes