Static product site for the Basilica Audio plugin suite — thirteen
sacred-architecture DSP plugins for heavy music. Zero frameworks, zero
external dependencies: a small Python (stdlib-only) generator renders a
bilingual (English + German) site: an overview page, one product page
per plugin, and one user-manual page per plugin (where a manual exists),
each mirrored under /de/.
Deployed via GitHub Pages on every push to main
(.github/workflows/pages.yml builds dist/ and publishes it — dist/ is
never committed).
build.py Generator + internal link checker (python3 stdlib only)
lib_md.py Vendored, dependency-free Markdown -> HTML renderer (manual pages only)
data/plugins.json Plugin content: names, roles, taglines, features, media
— each plugin may carry a "de" object (see "Translating
plugin content" below); chrome strings live in build.py instead.
data/manuals/<slug>.en.md English user manual, synced verbatim from the plugin repo
data/manuals/<slug>.de.md German user manual (optional — see "Manual sync" below)
templates/base.html Page shell (header incl. language switch, footer)
templates/index.html Overview page (hero + card grid)
templates/plugin.html Product page (about, features, downloads, stubs, links)
templates/manual.html User-manual page (breadcrumb + rendered manual article)
assets/css/style.css Suite stylesheet (charcoal + antique gold, system fonts)
assets/js/releases.js Client-side latest-release download buttons
assets/icons/<slug>.png Plugin icons (256 px)
assets/org.png Org emblem (512 px)
dist/ Build output (gitignored, built in CI)
Every generated page lives at <dir>/index.html under dist/, mirrored once
for English at the site root and once for German under dist/de/:
/ English overview
/<slug>/ English product page
/<slug>/manual/ English user manual (only if data/manuals/<slug>.en.md exists)
/de/ German overview
/de/<slug>/ German product page
/de/<slug>/manual/ German user manual (only if data/manuals/<slug>.de.md exists)
assets/ is shared by both language trees (copied once to dist/assets/) —
nothing is duplicated per language except the HTML pages themselves.
python3 build.py
python3 -m http.server -d dist
# open http://localhost:8000build.py fails the build if any generated page contains a broken internal
link, so a green build means the site is internally consistent — across both
language trees.
The GitHub org login lives in one place: the ORG constant at the top of
build.py. Templates and the client-side release fetcher (via
<body data-org="...">) both receive it from there. When the org is renamed
from metal-up-your-ass to basilica-audio, change only that constant and
rebuild.
URL structure. The existing English tree stays exactly where it was
(site root, /<slug>/) so no existing link breaks. The German tree is a
full mirror under /de/ (/de/, /de/<slug>/), generated by the same
templates with lang="de".
UI chrome strings. Every piece of site chrome — nav labels, the download
button, the "no screenshots yet" empty state, the donations stub, footer
copy, "User manual", breadcrumb back-links, etc. — comes from the
STRINGS = {"en": {...}, "de": {...}} dict at the top of build.py. There
is exactly one exception: the one string assets/js/releases.js renders
client-side ("Release notes & previous versions") is injected per-page via a
data-release-notes-label attribute sourced from the same STRINGS dict, so
it stays localized too. DSP/product terminology (Mix, Bypass, Threshold, dB,
plugin format names, ...) is deliberately left in English in the German
column — that's how German-speaking audio engineers actually talk about
these controls.
Language selector. Every page header carries an "EN | DE" switch
(lang_switch_nav() in build.py) linking to the equivalent page in the
other language. Clicking it stores the choice in localStorage.lang via a
small inline onclick handler — no page-load JS dependency.
Root-index redirect. Only the root English index (/) carries a small
inline script that redirects to /de/ if localStorage.lang === "de", or —
absent a stored preference — if navigator.language starts with de. It
fails open (stays on the English page) if localStorage/navigator are
unavailable. The redirect target is a page-relative de/, so the site stays
relocatable under any deploy path prefix.
hreflang. Every page emits <link rel="alternate" hreflang="en|de|x-default">
tags pointing at its sibling in the other language (hreflang_tags() in
build.py).
Translating plugin content. data/plugins.json schema: each plugin
object may carry an optional "de" object with any of "role", "tagline",
"description", "features":
{
"slug": "overture",
"name": "Overture",
"role": "Guitar tight boost",
"...": "... (all the existing English fields, unchanged) ...",
"de": {
"role": "Gitarren-Tightening-Boost",
"tagline": "...",
"description": "...",
"features": ["...", "..."]
}
}Any field missing from "de" — or a plugin with no "de" object at all —
falls back to the English field (loc() in build.py). The site always
builds cleanly with zero translations present; German product pages simply
show English plugin copy until translated.
Each plugin's manual is synced verbatim from its own repo into
data/manuals/<slug>.en.md, from <plugin-repo>/docs/manual.md. The synced
copy gets one generated-from HTML comment prepended (stripped again before
rendering, so it never shows up in the built page):
<!-- Generated from <repo>/docs/manual.md on YYYY-MM-DD — do not hand-edit; re-run the manual sync described in website/README.md. -->
To re-sync a manual after the source repo's docs change:
cp ../<repo>/docs/manual.md data/manuals/<slug>.en.md
# then prepend the generated-from comment line above (with today's date)<slug> is the plugin's site slug, <repo> is its GitHub repo name — these
differ for one plugin: Crypta ships from the twist-your-guts repo (a
pre-rename holdover), so data/manuals/crypta.en.md is generated from
twist-your-guts/docs/manual.md.
If a plugin repo has no docs/manual.md, it simply has no
data/manuals/<slug>.en.md — build.py skips its manual page entirely and
drops the "User manual" link from that plugin's product page (it never
renders a link to a page that doesn't exist).
German manuals (data/manuals/<slug>.de.md) are optional and mostly
don't exist yet. When one is missing, the German product page's "User
manual" link falls back to the English manual page with a small
"(English)" hint appended, instead of hiding the link. Once a German manual
file is added, build.py picks it up automatically on the next build — no
code change needed.
Rendering. Manual Markdown is rendered by the small vendored renderer in
lib_md.py (stdlib-only — see its module docstring for exactly which
Markdown subset it supports: headings, paragraphs, bold/italic/inline code,
fenced code blocks, links, ordered/unordered lists, GitHub-style tables,
blockquotes, horizontal rules). Raw HTML in a manual's Markdown source is
escaped, never executed. Manual pages get a dedicated .manual article
layout in style.css; wide tables scroll horizontally inside their own
.table-scroll container so they never widen the page itself. A manual's
doc-relative links (e.g. architecture.md) are rewritten to point at that
file on the plugin's GitHub repo, since the manual page itself doesn't host
the rest of docs/.
Each product page ships with a static fallback link to the plugin's GitHub
releases page. On load, assets/js/releases.js queries
https://api.github.com/repos/<ORG>/<Repo>/releases/latest and replaces the
fallback with one styled button per .zip asset (platform, version, size,
publish date). If the API call fails — offline, rate-limited, no release yet —
the fallback link simply stays.
Product pages render honest "coming with the next release" empty states until real media exists. To populate them for a plugin:
-
Drop files into
assets/<slug>/(create the directory), e.g.assets/requiem/main-window.pngorassets/requiem/choir-before-after.mp3.- Screenshots:
.png.jpg.jpeg.webp.gif - Audio:
.mp3.wav.ogg.flac.m4a
- Screenshots:
-
Rebuild. That's it — the template auto-discovers the files (sorted by name; captions derived from the filename,
main-window.png→ "Main window"). -
Optional: curate order and captions explicitly in
data/plugins.json:{ "slug": "requiem", "screenshots": [ { "file": "main-window.png", "caption": "The main window" } ], "audio": [ { "file": "choir-before-after.mp3", "caption": "Choir bus, before/after" } ] }When these fields exist they take precedence over auto-discovery.
The "Support development" section is a deliberate stub: visually present,
buttons disabled, no real payment links. Wire up GitHub Sponsors / Ko-fi /
PayPal in templates/plugin.html (see the HTML comment there) when the
accounts exist.
The website sources are part of the Basilica Audio suite. All plugins are licensed under the GNU AGPL-3.0.