Source
The two files that make the homepage
site.toml is the content. build.py is the compiler: it reads the
data, fetches the three numbers that go stale, and writes index.html. It runs
nightly on cron. Last build: 2026-08-04 18:53 UTC.
site.toml
# obercode.com — the entire site, as data.
#
# This file is the source of truth for the homepage. Edit it, run `build.py`,
# and index.html is regenerated. Nothing about the page is maintained by hand.
#
# Anything that can go stale is NOT written here. GitHub star counts, the
# App Store version, and the Neon Starship high score are fetched live at
# build time by build.py, which runs nightly on cron. That is the whole point:
# the last version of this site rotted because updating it was a manual act.
#
# Fields marked "live:" below are placeholders filled in at build time.
[site]
domain = "obercode.com"
title = "Nate Ober — obercode"
description = "Native Mac and iOS apps by Nate Ober — Render for Shortcuts, MarkdownEditor, SuperIcon — plus open-source agent tooling: the Product Team's Tool Chest, ceremony-clip, image-kit, and eval-harness."
og_blurb = "Native Mac and iOS apps, and the agent systems that run the rest of the week."
author = "Nate Ober"
email = "nate@obercode.com"
github = "https://github.com/nateober"
linkedin = "https://linkedin.com/in/nateober"
writing = "https://nateober.com/articles/"
year = 2026
[hero]
kicker = "Nate Ober"
headline = "Make tools, not tasks."
tagline = """
Native Mac and iOS apps, and the agent systems that run the rest of my week.
The output is never the durable thing — <b>the written procedure is.</b>"""
meta = "Minnesota"
# ---------------------------------------------------------------- apps
[apps]
label = "Apps"
title = "Things I've shipped"
note = "Small, native, and fast enough that you stop noticing them."
[[apps.item]]
name = "Render"
status = "App Store" # live: version appended from the iTunes lookup API
status_live = "appstore_version"
appstore_id = "6756882945"
shot = "/assets/render-screen.jpg"
shot_alt = "Render on iPhone showing a Markdown document rendered in dark mode with headings, a bulleted feature list, and a syntax-highlighted Swift code block"
blurb = """
Displays HTML and Markdown from inside a Shortcut. Three actions — View HTML,
View Markdown, Convert Markdown to HTML — plus PDF export, a share extension,
and dark mode that follows the system."""
links = [
{ text = "App Store", url = "https://apps.apple.com/us/app/render-for-shortcuts/id6756882945" },
{ text = "Details", url = "/render/" },
]
[[apps.item]]
name = "MarkdownEditor"
status = "Free · open source"
shot = "/assets/markdowneditor-hero.jpg"
shot_alt = "MarkdownEditor on macOS in dark mode: Markdown source on the left, rendered preview with math and a diagram on the right"
repo = "nateober/markdown-editor"
blurb = """
A Mac-native Markdown editor: vim motions, live preview, KaTeX math, Mermaid
diagrams, and export to PDF, single-file HTML, or Word. A 5 MB Swift binary,
not a browser booting up. macOS 14+, signed and notarized."""
links = [
{ text = "Download", url = "https://github.com/nateober/markdown-editor/releases/latest/download/MarkdownEditor.dmg" },
{ text = "Source", url = "https://github.com/nateober/markdown-editor" },
{ text = "Details", url = "/markdowneditor/" },
]
[[apps.item]]
name = "SuperIcon"
status = "macOS · free download"
shot = "/supericon/theicon.png"
shot_alt = "The SuperIcon app icon"
shot_pad = true
blurb = """
Drop in one 1024×1024 PNG and get back every icon size React, Android, Windows,
and iOS ask for — correctly named, in the folder layout each platform expects."""
links = [
{ text = "Download", url = "/supericon/SuperIcon.zip" },
{ text = "Details", url = "/supericon/" },
]
[apps.footnote]
text = """
<b>Also built, not shipped:</b> <a href="/studentdriver/">StudentDriver</a> — an iOS
permit-hour tracker with sunrise/sunset-based day/night classification, WeatherKit
logging, and DMV-style PDF export. Finished enough to use, never submitted to the
App Store."""
# ---------------------------------------------------------------- tools
[tools]
label = "Tools"
# Star badges appear only at or above this count. A "1★" badge reads weaker than
# no badge at all, so small numbers are simply not shown.
min_stars = 10
title = "Written procedures, run by agents"
note = """
Each of these is mostly a document. The model is the engine; the discipline is the
asset, and it survives the model getting replaced."""
[[tools.item]]
name = "Product Team's Tool Chest"
repo = "aws-samples/sample-kiro-cli-prompts-for-product-teams"
blurb = """
Steering files that take a product team from an idea to deep market research, an
Amazon-style PRFAQ, a PRD, and a clickable prototype — in a single working session.
Research before PRFAQ, PRFAQ before requirements, a check at every seam. Runs in
Kiro, Claude Code, and Cursor. Open sourced through AWS Samples."""
[[tools.item]]
name = "ceremony-clip"
repo = "nateober/ceremony-clip"
blurb = """
Hand it a two-hour graduation recording and a name. It finds the name being called,
confirms the right kid is actually on screen, cuts a ~35-second clip, adds a title
card, and scores it with a music model running on the same Mac — then re-transcribes
its own mix to prove the name is still audible through the strings. Nothing uploaded."""
[[tools.item]]
name = "image-kit"
repo = "nateober/image-kit"
blurb = """
A local image toolkit for agents on the Mac: inspect (dimensions, EXIF/GPS, sharpness,
faces), upscale with Real-ESRGAN, repair faces with GFPGAN, cut backgrounds with rembg,
and strip metadata before you share. No uploads, no subscriptions."""
[[tools.item]]
name = "eval-harness"
repo = "nateober/eval-harness"
blurb = """
The scripts from an eval campaign that caught a "perfect" 100% score hiding a 70.8%
reality. Repeat-trial runs at production config, per-item flip rates, exact
Clopper-Pearson intervals, and episode-level recall — because a perfect score on 30
items is "at least 88.4%, with 95% confidence." """
[tools.footnote]
text = """
<b>I write about all of this at <a href="https://nateober.com/articles/">nateober.com</a></b>
— essays on second-order work, choosing models honestly, and what happens when you
write your own process down."""
# ---------------------------------------------------------------- play
[play]
label = "Play"
title = "Things that run in a browser"
[[play.item]]
name = "Neon Starship"
url = "/neon-starship/"
featured = true
score_live = "neon_highscore" # live: top score pulled from the SQLite leaderboard
blurb = """
A twin-stick arcade shooter in a canvas — waves of neon enemies, power-ups, and a
high-score board that has been quietly accumulating other people's initials. Works
on a desktop keyboard or a phone thumbstick."""
[[play.item]]
name = "Newscast model bake-off"
url = "/bakeoff/"
blurb = "Seventeen models wrote the identical briefing against one rubric. Thinking mode made them worse."
[[play.item]]
name = "Coordinate Reverser"
url = "/bingo.html"
blurb = "Give it a bingo coordinate like G4 and it returns the mirrored square on a grid you configure."
[[play.item]]
name = "IP Lookup"
url = "/ip/"
blurb = "Tells you the IP address you're coming from. That's it."
build.py
#!/usr/bin/env python3
"""
build.py — compile site.toml into obercode.com's homepage.
Run it: python3 build.py [--out /var/www/obercode.com]
Everything on the page comes from site.toml except the numbers that go stale,
which are fetched live here:
* GitHub star counts api.github.com
* Render's App Store version itunes.apple.com lookup
* Neon Starship high score /var/lib/neon-starship/scores.db (SQLite, read-only)
If a live fetch fails, the build keeps the previously cached value from
live-cache.json and reports it. A network blip must never blank out the page or
replace a real number with a zero.
"""
import argparse
import html
import json
import os
import sqlite3
import sys
import tomllib
import urllib.request
from datetime import date, timezone, datetime
from pathlib import Path
HERE = Path(__file__).resolve().parent
CACHE = HERE / "live-cache.json"
TIMEOUT = 20
# ----------------------------------------------------------------- live data
def _get_json(url):
req = urllib.request.Request(url, headers={"User-Agent": "obercode-site-build"})
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
return json.load(r)
def fetch_live(cfg, warnings):
"""Fetch the volatile numbers. Fall back to cache on any failure."""
cache = {}
if CACHE.exists():
try:
cache = json.loads(CACHE.read_text())
except (ValueError, OSError):
pass
live = {"stars": dict(cache.get("stars", {}))}
# --- GitHub stars, for every repo mentioned anywhere in the file
repos = set()
for group in ("apps", "tools"):
for item in cfg.get(group, {}).get("item", []):
if item.get("repo"):
repos.add(item["repo"])
for repo in sorted(repos):
try:
live["stars"][repo] = _get_json(f"https://api.github.com/repos/{repo}")["stargazers_count"]
except Exception as e:
warnings.append(f"github stars {repo}: {e} (using cached {cache.get('stars', {}).get(repo, '?')})")
# --- App Store version
app_ids = [i["appstore_id"] for i in cfg.get("apps", {}).get("item", []) if i.get("appstore_id")]
for app_id in app_ids:
try:
results = _get_json(f"https://itunes.apple.com/lookup?id={app_id}")["results"]
if results:
live[f"appstore_{app_id}"] = results[0]["version"]
else:
raise ValueError("app not found in App Store — is it still published?")
except Exception as e:
warnings.append(f"appstore {app_id}: {e} (using cached {cache.get(f'appstore_{app_id}', '?')})")
if f"appstore_{app_id}" in cache:
live[f"appstore_{app_id}"] = cache[f"appstore_{app_id}"]
# --- Neon Starship high score
db = "/var/lib/neon-starship/scores.db"
try:
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True, timeout=5)
row = con.execute("SELECT name, score FROM scores ORDER BY score DESC LIMIT 1").fetchone()
con.close()
if row:
live["neon_highscore"] = {"name": row[0], "score": row[1]}
except Exception as e:
warnings.append(f"neon score: {e} (using cached)")
if "neon_highscore" in cache:
live["neon_highscore"] = cache["neon_highscore"]
# carry forward anything we failed to refresh
for k, v in cache.items():
live.setdefault(k, v)
CACHE.write_text(json.dumps(live, indent=2, sort_keys=True))
return live
# ----------------------------------------------------------------- helpers
def tidy(s):
"""TOML multi-line strings arrive with hard newlines; collapse to flowing text."""
return " ".join((s or "").split())
def e(s):
return html.escape(s or "", quote=True)
# ----------------------------------------------------------------- template
CSS = """
:root {
--ink: #14161a;
--panel: #1e2126;
--panel-edge: #2a2e35;
--text: #e8eaf0;
--dim: #8b93a5;
--faint: #565e6e; /* decorative only — fails AA as body text */
--green: #63c583;
--blue: #5ea1ff;
--mono: "SF Mono", "JetBrains Mono", ui-monospace, Menlo, monospace;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
background: var(--ink); color: var(--text); font-family: var(--sans);
font-size: 17px; line-height: 1.6; -webkit-font-smoothing: antialiased;
}
a { color: var(--blue); text-decoration: none; }
a:hover { text-decoration: underline; }
a:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; border-radius: 4px; }
img { max-width: 100%; }
.wrap { max-width: 1060px; margin: 0 auto; padding: 0 24px; }
/* page grammar: markdown source label, rendered heading */
.src { font-family: var(--mono); font-size: 15px; color: var(--green); display: block; margin-bottom: 10px; }
.src::before { content: "## "; color: var(--faint); }
h2.rendered { font-size: 34px; font-weight: 700; letter-spacing: -0.6px; margin-bottom: 8px; }
.sec-note { color: var(--dim); font-size: 16px; margin-bottom: 32px; max-width: 640px; }
header { border-bottom: 1px solid var(--panel-edge); }
header .row { display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap; padding: 20px 0; }
.wordmark { font-family: var(--mono); font-size: 17px; font-weight: 600; color: var(--text); letter-spacing: -0.3px; }
.wordmark:hover { text-decoration: none; color: var(--green); }
nav ul { list-style: none; display: flex; gap: 26px; }
nav a { font-family: var(--mono); font-size: 14.5px; color: var(--dim); }
nav a:hover { color: var(--text); text-decoration: none; }
.hero { padding: 92px 0 76px; text-align: center; }
.hero .src { font-size: 16px; }
.hero .src::before { content: "# "; }
.hero h1 { font-size: clamp(42px, 6.6vw, 68px); font-weight: 800; letter-spacing: -2px; line-height: 1.03; margin-bottom: 20px; }
.hero .tagline { font-size: clamp(18px, 2.4vw, 21px); color: var(--dim); max-width: 660px; margin: 0 auto 26px; }
.hero .tagline b { color: var(--text); font-weight: 600; }
.hero .meta { font-family: var(--mono); font-size: 13.5px; color: var(--dim); }
.hero .meta a { color: var(--dim); }
.hero .meta a:hover { color: var(--text); }
section { padding: 30px 0 56px; }
.grid { display: grid; gap: 18px; }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.card {
background: var(--panel); border: 1px solid var(--panel-edge); border-radius: 12px;
padding: 24px; display: flex; flex-direction: column;
transition: border-color 0.12s ease, background 0.12s ease;
}
.card:hover { border-color: #39404a; background: #21252b; }
.card h3 { font-size: 19px; font-weight: 650; letter-spacing: -0.2px; }
.card p { color: var(--dim); font-size: 15px; line-height: 1.55; margin-top: 8px; }
.card .links { margin-top: auto; padding-top: 18px; display: flex; gap: 16px; flex-wrap: wrap; align-items: baseline; }
.card .links a { font-size: 14.5px; font-weight: 550; }
/* app cards */
.shot {
height: 190px; margin: -4px 0 18px; border-radius: 9px; overflow: hidden;
background: var(--ink); border: 1px solid var(--panel-edge);
display: flex; align-items: center; justify-content: center;
}
.shot img { height: 100%; width: 100%; object-fit: cover; object-position: top center; }
.shot.pad { padding: 22px; }
.shot.pad img { object-fit: contain; height: 100%; width: auto; }
.app-head { display: flex; align-items: center; gap: 13px; }
.app-head img { width: 40px; height: 40px; border-radius: 9px; object-fit: contain; flex-shrink: 0; }
.status { font-family: var(--mono); font-size: 12px; color: var(--dim); margin-top: 3px; display: block; letter-spacing: 0.2px; }
.status.live { color: var(--green); }
/* tool cards */
.repo { font-family: var(--mono); font-size: 12.5px; color: var(--green); display: block; margin-bottom: 12px; overflow-wrap: anywhere; }
.stars { font-family: var(--mono); font-size: 13px; color: var(--dim); }
.note-box {
margin-top: 20px; padding: 18px 22px; border: 1px solid var(--panel-edge);
border-radius: 12px; color: var(--dim); font-size: 15px; line-height: 1.55;
}
.note-box b { color: var(--text); font-weight: 600; }
/* play */
.play-feature {
display: grid; grid-template-columns: 1fr auto; gap: 24px; align-items: center;
background: var(--panel); border: 1px solid var(--panel-edge); border-radius: 12px;
padding: 26px; margin-bottom: 18px; color: inherit;
}
.play-feature:hover { text-decoration: none; border-color: #39404a; background: #21252b; }
.play-feature h3 { font-size: 21px; font-weight: 650; margin-bottom: 8px; }
.play-feature p { color: var(--dim); font-size: 15px; line-height: 1.55; max-width: 60ch; }
.hiscore { text-align: right; font-family: var(--mono); white-space: nowrap; }
.hiscore .n { display: block; font-size: 30px; font-weight: 700; color: var(--green); letter-spacing: -1px; }
.hiscore .l { display: block; font-size: 11.5px; color: var(--dim); letter-spacing: 0.4px; margin-top: 2px; }
.list { border: 1px solid var(--panel-edge); border-radius: 12px; overflow: hidden; }
.list a { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; padding: 15px 22px; color: var(--text); border-bottom: 1px solid var(--panel-edge); transition: background 0.12s ease; }
.list a:last-child { border-bottom: 0; }
.list a:hover { background: var(--panel); text-decoration: none; }
.list .name { font-family: var(--mono); font-size: 15px; font-weight: 500; }
.list .what { color: var(--dim); font-size: 15px; }
/* the colophon — this page's own build stamp */
.colophon {
border: 1px dashed var(--panel-edge); border-radius: 12px; padding: 20px 22px;
font-family: var(--mono); font-size: 13.5px; color: var(--dim); line-height: 1.7;
}
.colophon .t { color: var(--green); }
footer { border-top: 1px solid var(--panel-edge); padding: 28px 0 44px; margin-top: 30px; }
footer .row { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; font-size: 14.5px; color: var(--dim); }
footer a { color: var(--dim); }
footer .links { display: flex; gap: 20px; }
@media (max-width: 900px) { .grid-3 { grid-template-columns: 1fr 1fr; } }
@media (max-width: 620px) {
.grid-3, .grid-2 { grid-template-columns: 1fr; }
.hero { padding: 60px 0 52px; }
header .row { padding: 14px 0; flex-direction: column; align-items: flex-start; gap: 10px; }
nav ul { gap: 18px; flex-wrap: wrap; }
nav a { font-size: 14px; }
h2.rendered { font-size: 28px; }
.play-feature { grid-template-columns: 1fr; gap: 14px; }
.hiscore { text-align: left; }
footer .row { flex-direction: column; align-items: flex-start; }
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
* { transition: none !important; }
}
"""
FAVICON = ("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>"
"<rect width='64' height='64' rx='14' fill='%2314161a'/>"
"<text x='32' y='45' font-family='ui-monospace,Menlo,monospace' font-size='40' "
"font-weight='700' fill='%2363c583' text-anchor='middle'>%23</text></svg>")
def render(cfg, live, built_at):
s, hero = cfg["site"], cfg["hero"]
out = []
w = out.append
w(f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{e(s['title'])}</title>
<meta name="description" content="{e(s['description'])}">
<meta name="author" content="{e(s['author'])}">
<link rel="canonical" href="https://{s['domain']}/">
<meta property="og:type" content="website">
<meta property="og:url" content="https://{s['domain']}/">
<meta property="og:title" content="{e(s['title'])}">
<meta property="og:description" content="{e(s['og_blurb'])}">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{e(s['title'])}">
<meta name="twitter:description" content="{e(s['og_blurb'])}">
<script type="application/ld+json">{json.dumps({
"@context": "https://schema.org", "@type": "Person", "name": s["author"],
"url": f"https://{s['domain']}", "sameAs": ["https://nateober.com", s["github"], s["linkedin"]]})}</script>
<link rel="icon" type="image/svg+xml" href="{FAVICON}">
<style>{CSS}</style>
</head>
<body>
<header>
<div class="wrap row">
<a href="/" class="wordmark">obercode</a>
<nav><ul>
<li><a href="#apps">apps</a></li>
<li><a href="#tools">tools</a></li>
<li><a href="#play">play</a></li>
<li><a href="#colophon">colophon</a></li>
<li><a href="{s['github']}">github</a></li>
</ul></nav>
</div>
</header>
<main>
<section class="hero">
<div class="wrap">
<span class="src">{e(hero['kicker'])}</span>
<h1>{e(hero['headline'])}</h1>
<p class="tagline">{tidy(hero['tagline'])}</p>
<p class="meta">{e(hero['meta'])} · <a href="{s['github']}">{s['github'].split('//')[1]}</a> · <a href="mailto:{s['email']}">{s['email']}</a></p>
</div>
</section>
""")
# ---- apps
a = cfg["apps"]
w(f"""<section id="apps">
<div class="wrap">
<span class="src">{e(a['label'])}</span>
<h2 class="rendered">{e(a['title'])}</h2>
<p class="sec-note">{tidy(a['note'])}</p>
<div class="grid grid-3">""")
for item in a["item"]:
status = item["status"]
cls = "status live"
if item.get("status_live") == "appstore_version":
v = live.get(f"appstore_{item['appstore_id']}")
status = f"{status} · v{v}" if v else status
elif not item.get("repo") and "open source" not in status:
cls = "status"
shot_cls = "shot pad" if item.get("shot_pad") else "shot"
links = " ".join(f'<a href="{l["url"]}">{e(l["text"])}</a>' for l in item["links"])
w(f"""
<div class="card">
<div class="{shot_cls}"><img src="{item['shot']}" alt="{e(item['shot_alt'])}" loading="lazy"></div>
<div class="app-head"><div>
<h3>{e(item['name'])}</h3>
<span class="{cls}">{e(status)}</span>
</div></div>
<p>{tidy(item['blurb'])}</p>
<div class="links">{links}</div>
</div>""")
w(f"""
</div>
<p class="note-box">{tidy(a['footnote']['text'])}</p>
</div>
</section>
""")
# ---- tools
t = cfg["tools"]
w(f"""<section id="tools">
<div class="wrap">
<span class="src">{e(t['label'])}</span>
<h2 class="rendered">{e(t['title'])}</h2>
<p class="sec-note">{tidy(t['note'])}</p>
<div class="grid grid-2">""")
for item in t["item"]:
# Star counts are shown only once they mean something. A "1★" badge reads
# as weaker than no badge at all; the threshold lives in site.toml.
stars = live["stars"].get(item["repo"]) or 0
star_html = f'<span class="stars">{stars}★</span>' if stars >= cfg["tools"]["min_stars"] else ""
w(f"""
<div class="card">
<span class="repo">{e(item['repo'])}</span>
<h3>{e(item['name'])}</h3>
<p>{tidy(item['blurb'])}</p>
<div class="links"><a href="https://github.com/{item['repo']}">GitHub</a>{star_html}</div>
</div>""")
w(f"""
</div>
<p class="note-box">{tidy(t['footnote']['text'])}</p>
</div>
</section>
""")
# ---- play
p = cfg["play"]
w(f"""<section id="play">
<div class="wrap">
<span class="src">{e(p['label'])}</span>
<h2 class="rendered">{e(p['title'])}</h2>
<p class="sec-note">Not products. They work, and that's the whole claim.</p>""")
rest = []
for item in p["item"]:
if item.get("featured"):
hs = live.get("neon_highscore") if item.get("score_live") else None
score_html = ""
if hs:
score_html = (f'<div class="hiscore"><span class="n">{hs["score"]:,}</span>'
f'<span class="l">HIGH SCORE · {e(hs["name"])}</span></div>')
w(f"""
<a class="play-feature" href="{item['url']}">
<div>
<h3>{e(item['name'])} →</h3>
<p>{tidy(item['blurb'])}</p>
</div>
{score_html}
</a>""")
else:
rest.append(item)
if rest:
w('\n <div class="list">')
for item in rest:
w(f"""
<a href="{item['url']}">
<span class="name">{e(item['name'])}</span>
<span class="what">{tidy(item['blurb'])}</span>
</a>""")
w("\n </div>")
w("""
</div>
</section>
""")
# ---- colophon: the page explaining its own construction
stamp = built_at.strftime("%Y-%m-%d %H:%M UTC")
w(f"""<section id="colophon">
<div class="wrap">
<span class="src">Colophon</span>
<h2 class="rendered">This page builds itself</h2>
<p class="sec-note">
The site above argues that the written procedure is the durable artifact. It would be a
poor argument if this page were maintained by hand — so it isn't.
</p>
<div class="colophon">
<span class="t">$ python3 build.py</span><br>
Compiled from <a href="/source/">site.toml</a> by
<a href="/source/#build">build.py</a> at <b>{stamp}</b>, nightly on cron.<br><br>
Every word above lives in a {len(cfg['apps']['item']) + len(cfg['tools']['item']) + len(cfg['play']['item'])}-entry
data file. The numbers that rot — GitHub stars, the App Store version, the high score —
are never typed here; they are fetched at build time from the GitHub API, the iTunes
lookup API, and the leaderboard's SQLite file.<br><br>
The previous version of this page said <b>© 2024</b> for two years and described an
app as "coming soon" for one. That is what hand-maintenance buys you.
<a href="/source/">Read the source →</a>
</div>
</div>
</section>
</main>
<footer>
<div class="wrap row">
<div>© {s['year']} obercode · built in Minnesota by <a href="https://nateober.com">{e(s['author'])}</a></div>
<div class="links">
<a href="{s['github']}">GitHub</a>
<a href="{s['linkedin']}">LinkedIn</a>
<a href="mailto:{s['email']}">Email</a>
</div>
</div>
</footer>
<script defer src="/track.js"></script>
</body>
</html>
""")
return "".join(out)
# ------------------------------------------------------- the /source/ page
def render_source(toml_text, build_text, built_at):
stamp = built_at.strftime("%Y-%m-%d %H:%M UTC")
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Source — obercode.com</title>
<meta name="description" content="The data file and build script that compile obercode.com's homepage.">
<link rel="icon" type="image/svg+xml" href="{FAVICON}">
<style>{CSS}
pre {{
background: var(--panel); border: 1px solid var(--panel-edge); border-radius: 12px;
padding: 22px; overflow-x: auto; font-family: var(--mono); font-size: 13px;
line-height: 1.6; color: var(--text); margin-bottom: 40px;
}}
.lead {{ color: var(--dim); max-width: 660px; margin-bottom: 34px; }}
</style>
</head>
<body>
<header><div class="wrap row">
<a href="/" class="wordmark">obercode</a>
<nav><ul><li><a href="/">← home</a></li></ul></nav>
</div></header>
<main><div class="wrap" style="padding-top:56px">
<span class="src">Source</span>
<h2 class="rendered">The two files that make the homepage</h2>
<p class="lead">
<code>site.toml</code> is the content. <code>build.py</code> is the compiler: it reads the
data, fetches the three numbers that go stale, and writes <code>index.html</code>. It runs
nightly on cron. Last build: <b>{e(stamp)}</b>.
</p>
<h3 id="toml" style="margin-bottom:12px">site.toml</h3>
<pre>{html.escape(toml_text)}</pre>
<h3 id="build" style="margin-bottom:12px">build.py</h3>
<pre>{html.escape(build_text)}</pre>
</div></main>
<footer><div class="wrap row">
<div>© 2026 obercode</div>
<div class="links"><a href="/">Home</a></div>
</div></footer>
<script defer src="/track.js"></script>
</body>
</html>
"""
# ----------------------------------------------------------------- main
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", default="/var/www/obercode.com", help="webroot to write into")
ap.add_argument("--dry-run", action="store_true", help="print to stdout, write nothing")
args = ap.parse_args()
cfg = tomllib.loads((HERE / "site.toml").read_text())
warnings = []
built_at = datetime.now(timezone.utc)
live = fetch_live(cfg, warnings)
page = render(cfg, live, built_at)
source = render_source((HERE / "site.toml").read_text(),
Path(__file__).read_text(), built_at)
if args.dry_run:
sys.stdout.write(page)
else:
out = Path(args.out)
(out / "index.html").write_text(page)
(out / "source").mkdir(exist_ok=True)
(out / "source" / "index.html").write_text(source)
print(f"wrote {out/'index.html'} ({len(page):,} bytes)", file=sys.stderr)
print(f"wrote {out/'source'/'index.html'} ({len(source):,} bytes)", file=sys.stderr)
# All diagnostics go to stderr so that --dry-run emits clean HTML on stdout.
log = sys.stderr
stars = ", ".join(f"{k.split('/')[-1]}={v}" for k, v in sorted(live["stars"].items()))
print(f"live: {stars}", file=log)
for k, v in live.items():
if k.startswith("appstore_"):
print(f"live: {k}=v{v}", file=log)
if live.get("neon_highscore"):
print(f"live: neon={live['neon_highscore']['score']:,} by {live['neon_highscore']['name']}", file=log)
for warn in warnings:
print(f"WARN: {warn}", file=log)
return 1 if warnings else 0
if __name__ == "__main__":
sys.exit(main())