colophon.
The Obsidian vault is the source of truth; a small build script turns it into the pages you're reading.
folder layout01
./vault/ using a fine-grained PAT.┌ vault (private · obsidian) ├── daily │ └── YYYY-MM-DD.md # micro-posts · one ##HH:MM = one thought ├── notes # publish: journal | making → routed below │ ├── *.md # loose notes (publish: journal lives here) │ ├── making # convention bucket │ ├── dev # engineering posts │ └── ideas # never published (no publish: key) ├── attachments # images / audio / video └── .obsidian/ # ignored ┌ mattdoes-site (public) ├── build.js # the generator entrypoint ├── lib # intake (vault → model) · emit (model → dist) · listening · lastfm codec ├── site.config.js # identity + last.fm ├── templates # page templates · shared row renderers · helpers ├── static # css, js (tweaks · geo-bg · live), fonts, baked geojson, _headers ├── scripts # prebuild, optimize-media, sync-media, bake-geo ├── workers # listening · geo · lib (shared edge transport) ├── vault → cloned pre-build └── dist # deployed
frontmatter schema02
publish: is the only required field.| key | type | req | description |
|---|---|---|---|
| publish | enum | req | journal · making · thoughts · about · draft (listening is pulled from last.fm) |
| title | string | opt | Display title. Defaults to filename. |
| date | date | opt | Bare YYYY-MM-DD is anchored to CT midnight (so the post lands on the day you wrote, not UTC's). Full ISO timestamps are used as-is. Defaults to file mtime. |
| slug | string | opt | URL path segment. Defaults to kebab filename. |
| tags | [string] | opt | Render as #tag. Drives filter rows. |
| summary | string | opt | One-sentence lede. Shown in index + RSS. |
| updated | date | opt | Last meaningful edit. |
| aliases | [string] | opt | Extra wikilink targets. |
routing · publish → url03
publish: frontmatter value, not the folder. Folders are organizational only.wikilinks & embeds04
[[some-slug]]— resolves to published page; else renders as plain text with a dotted underline.[[some-slug|custom label]]— aliased label.![[image.png]]— image embed. Served from R2 in production.![[clip.mp3]]— renders as native <audio>.[[#some heading]]— intra-page anchor. Rare.
the build05
One pass, two modules. Intake turns notes into the content model — frontmatter validation, thought splitting, stable IDs, the slug index. Emit writes the model to dist/ — markdown, templates, hashed assets, feeds. Four surfaces in the nav (home · blog · listening · about); a new section (say, publish: recipes) is a new template file and one line in routeFor().
Two dynamic surfaces, both same-origin Workers; connect-src 'self' holds. /api/listening/* proxies Last.fm (now-playing for the topbar, recent tracks for the listening page) with a stale-while-revalidate KV cache out front. /api/geo/lookup reverse-geocodes a visitor's coords against Nominatim if they opt in via the tweaks panel — by default the animated background renders the home polygon baked into static/home.geojson, no prompt, no network call. Both Workers answer through one shared envelope (workers/lib/transport.js) — JSON + CORS, preflight, cached error responses — with caching policy kept per-Worker.
Media takes the long way around. scripts/optimize-media.js hashes every attachment and emits .webp siblings into .cache/media-build/; scripts/sync-media.js PUTs originals + variants to R2 over wrangler r2 object, and the build emits <picture> tags pointed at media.mattdoes.online. CSS and JS are content-hashed and served immutable from Pages; CSP is strict, no inline scripts, no third-party connect. Mail is Fastmail, contact is a plain mailto:, no form worker.