How This Website Works

This site is a static Astro build of one folder in my Obsidian vault, plus a thin layer of live data on top. Publishing a page means writing a note and waiting about five minutes. This note explains both halves.

The previous iteration ran on Quartz. Quartz was fine until I wanted real control over layout and the dynamic stuff below, so I rebuilt it in Astro with the same URL scheme. Old links still resolve.

The static half

The pipeline, end to end:

  1. I write markdown in Obsidian. The Digital_Garden/ folder of the vault is the site content, everything else stays private.
  2. Obsidian Sync replicates the vault to my app server (cyrion), where a copy lives next to the site repo.
  3. A systemd timer runs a change-detection script every five minutes. It hashes the repo HEAD, any uncommitted edits, and the vault file mtimes into one signature. If the signature matches the last build, it exits. If not, it rebuilds.
  4. The build runs in a Node container, output gets baked into an nginx image, and Docker Compose swaps the container. There is also a manual hook for when I don’t want to wait out the timer.

Astro’s content collection loads the vault folder directly, with a compatibility shim that keeps Quartz’s slug rules, resolves [[wikilinks]], and copies image attachments. Every page also gets a generated OG card, and the footer build stamp links to the exact commit the site was built from.

Nothing about the static half requires the server to do work per visitor. It is HTML on nginx behind a reverse proxy.

The dynamic half

A static site can still feel alive if the pages fetch small pieces of data client side. The homepage does this in a few places:

  • the random reading highlight under the header, pulled from my Kobo Highlight Pipeline
  • the listening line (weekly minutes plus what I’ve been on lately, from a self-hosted Your Spotify instance)
  • the Susquehanna line: current river stage at Sunbury and my flood model’s 48 hour forecast with its uncertainty interval
  • the Knoebels line and the park heat map, which get their own writeup in the Knoebels model note
  • the bandwidth odometer in the footer, ticking forward at the lifetime average rate so it looks live without exposing actual current throughput

All of these hit api.c0smere.net, a small FastAPI service I run for exactly this purpose. The rules it lives by:

  • Read only, no auth, public. Nothing sensitive is reachable through it, by construction rather than by policy.
  • One database user per data source, each granted SELECT on purpose-built views and nothing else. The API runs canned queries only, no dynamic SQL.
  • Caching in the app (in-process TTL plus Cache-Control headers). If a backing database is down, endpoints serve the last cached value with an X-Stale header instead of erroring. A widget should never show visitors an error wall.

On the page side every widget follows the same pattern: the markup ships hidden, a small inline script fetches the endpoint, and any failure at all leaves the element hidden. If the API is unreachable you get a plain static page and would never know the widgets exist. Fail closed, degrade to boring.

The heat map is the fanciest of these. It draws Esri satellite tiles of the park, projects every ride onto them with the standard web map tile math, and lays a colored glow per ride on top, sized and colored by the current wait. When the park is closed it falls back to historical averages for a typical weekend afternoon, labeled as such. All of it is one SVG built by vanilla JS in the page, no chart library.

Why bother

The honest answer is that the pipeline is the hobby. But it also means the site never rots: the data pipelines that feed the widgets are the same ones I actually use, so if they break I notice for my own reasons, and the site heals when they do.

Wesley Ray · blog · git · resume · linkedin

built 2026-09-13 14:35 ET · b6c6233 · 117 notes