Carl Henriksson

Firmware, systems, web. Same problems, different layers.

Building RumbleVoice, much of the work had nothing to do with the app itself. Cache busting, service worker versioning, SEO meta tags, Open Graph, sitemap, robots, security.txt, icons, deployment headers.

All of it necessary, none of it the point.

When it came time to build this site, the reusable pieces were obvious. Most of the shared infrastructure ended up duplicated from one project into the other. That worked. It also meant two copies of everything. Then a third site came up. Any revision — to the cache busting logic, the service worker, anything — would have to be applied to each project separately. That was the moment to stop and extract.

The shared pieces became SBFW: a static builder framework.


The config covers the parts a properly deployed site needs:

  • things search engines expect,
  • things browsers require,
  • things deployment targets care about,
  • things offline and PWA behavior depend on,
  • and things humans should not be hand-copying between repositories.

SEO metadata, Open Graph, schema.org, security.txt, manifests, cache-versioned service workers, deployment headers, icon generation.

Templates use named sentinels populated from config values and generated outputs. Assets are resolved, MIME-validated, minified, and either embedded inline or referenced with cache-busted URLs. Output is validated before it deploys. Broken local references, unreplaced sentinels, missing declared assets — all fail the build explicitly rather than shipping broken. A complete configuration looks like this:

CACHE_VERSION="1"

SITE_NAME="My Site"
SITE_URL="https://example.com"
SITE_URL_DOMAIN="example.com"
LANG="en"

# Optional. Default: UTF-8
CHARSET="UTF-8"
# Optional. Default: width=device-width,initial-scale=1,viewport-fit=cover
VIEWPORT="width=device-width,initial-scale=1,viewport-fit=cover"
# Optional. Default: index, follow
ROBOTS="index, follow"

AUTHOR_NAME="Your Name"
DATE_PUBLISHED="2026-01-01"
# Required for deterministic builds.
DATE_MODIFIED="2026-01-01"
# Must be an ISO 8601 timestamp with timezone and a future date.
SECURITY_TXT_EXPIRES="2027-01-01T00:00:00Z"
# Optional. Defaults to DATE_MODIFIED year when omitted.
COPYRIGHT_YEAR="2026"

PAGE_TITLE="My Site"
META_DESCRIPTION="Short site description."
# Optional. Default: empty
META_KEYWORDS=""

OG_TITLE="My Site"
OG_DESCRIPTION="Short site description."
# Optional. Default: /og-image.png
OG_IMAGE="/og-image.png"
# Optional. Default: en_US
OG_LOCALE="en_US"

# Optional. Defaults for JSON-LD/schema output.
# SCHEMA_TYPE="WebApplication" enables app schema; set SCHEMA_CATEGORY and SCHEMA_PRICE.
SCHEMA_TYPE="WebSite"
SCHEMA_CATEGORY=""
# SCHEMA_PRICE="0"
APP_CATEGORIES=""

THEME_COLOR_LIGHT="#ffffff"
THEME_COLOR_DARK="#000000"

CONTACT_EMAIL="[email protected]"

# Optional build flags.
# MONOLITH embeds the entire site - HTML, CSS, JavaScript, assets - into a
# single self-contained index.html. This is how RumbleVoice is delivered as
# a complete app in one request.
# MONOLITH requires exactly one template rendering to /.
MONOLITH="false"
GENERATE_FOOTER="false"
GENERATE_NOSCRIPT="true"

# Optional content/routing defaults.
HOME_SLUG="index"
SKIP_LINK_TARGET="#main"
SITEMAP_URLS="/"

The shared infrastructure that had been copied between projects was deleted.

What remained was a second problem: layout, typography, the visual layer. The same pattern applied — a framework built on top of SBFW, concerned only with how things look. SBFW handles deployment. The visual framework handles presentation.

Every site is now a config and a folder of Markdown files.