Personal blog. Static HTML generated from Markdown, served via GitHub Pages.
content/
about.md # section: about
assets/
tal-episodes.json # generated by scripts/scrape_tal_episodes.py
blog/
YYYYMMDD-title.md # section: blog (special — see below)
generator/
generate.py
input.css
requirements.txt
templates/
base.html
blog_index.html
post.html
section.html
tal_player.html # TAL shuffle player (no .md source — see TAL Player)
scripts/
scrape_tal_episodes.py # scrapes thisamericanlife.org, writes tal-episodes.json
docs/ # generated output — GitHub Pages source
pip install -r generator/requirements.txt
python generator/generate.py
Each .md file in content/ must have a section: field in its YAML front matter. The value determines where the output is written:
- Regular sections (
section: about,section: projects, etc.) — generatedocs/{section}/index.html, reachable at/{section}/. Add a new section by dropping a new.mdfile incontent/with the desiredsection:value. The nav bar is auto-generated:blogalways appears first, then sections in filename-alphabetical order, thental-playerlast. section: blog— special case. Must live incontent/blog/. Does not generate a section directory; see Blog below.
Files in content/ missing a section: field are silently skipped.
Blog posts require title: and date: (ISO format: YYYY-MM-DD) in addition to section: blog.
URL slug is derived deterministically as {date}-{slugified-title}, e.g. a post titled Hello World dated 2026-04-25 becomes /blog/2026-04-25-hello-world/.
Collision detection — if two posts would produce the same slug, the generator exits with an error listing both source files before writing anything.
Blog index is auto-generated, sorted newest-first, and served at both / and /blog/. There is no content/ source file for it.
Posts missing title: or date: are skipped with a warning.
A shuffle player for This American Life archival episodes, served at /tal-player/.
Episode data is stored in content/assets/tal-episodes.json and loaded client-side at runtime. The page itself is generated directly from generator/templates/tal_player.html with no .md source file.
Queue persistence — the playback queue (up to 100 episodes) and current position are saved to localStorage['tal-queue'] after every navigation. On page load the queue is restored and the current episode is shown paused at the beginning. This survives mobile browser tab discards (e.g. iOS backgrounding the tab for long enough that the OS kills the process).
To update the episode list (normal case):
.venv/bin/python scripts/update_tal_episodes.py
.venv/bin/python generator/generate.py
update_tal_episodes.py does an incremental refresh: it HEAD-validates all existing audio URLs in parallel (10 workers), then discovers new episodes by fetching the /archive newest-first landing and the / homepage. Only episodes with a number higher than the current max are fetched. Typical wall time ~30s. It will surface broken existing URLs in its summary but does not auto-repair them.
Full rescrape (rebuild from scratch):
.venv/bin/python scripts/scrape_tal_episodes.py
.venv/bin/python generator/generate.py
This walks /archive?year=YYYY for every year from 1995 onward and re-fetches every episode page. Takes ~30 minutes due to per-page rate-limit delays. Use it if the JSON gets out of sync, an episode's URL changes, or you want to refresh metadata. The /archive?year=YYYY endpoint can lag for the most recent few episodes, so a full rescrape immediately after a release may still need a follow-up update_tal_episodes.py run to pick them up.
The .scrape-cache/ directory is a within-run deduplication cache used by scrape_tal_episodes.py only; it is wiped at the start of every full scrape and is not committed.
Point GitHub Pages to the docs/ folder on the main branch. Nav links use root-relative paths so they work correctly at the root domain.