mattdoes.online colophon say hi
docs·updated

colophon.

The Obsidian vault is the source of truth; a small build script turns it into the pages you're reading.

Two repos. Vault stays private; site repo is thin and public. A pre-build script clones the vault into ./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
Every note that wants to appear on the site declares it. publish: is the only required field.
Frontmatter fields accepted in vault notes
keytypereqdescription
publishenumreqjournal · making · thoughts · about · draft (listening is pulled from last.fm)
titlestringoptDisplay title. Defaults to filename.
datedateoptBare 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.
slugstringoptURL path segment. Defaults to kebab filename.
tags[string]optRender as #tag. Drives filter rows.
summarystringoptOne-sentence lede. Shown in index + RSS.
updateddateoptLast meaningful edit.
aliases[string]optExtra wikilink targets.
Routes are driven by the publish: frontmatter value, not the folder. Folders are organizational only.
publish: journal/journal/<slug>/ · /blog/?kind=journal publish: making/making/<slug>/ · /blog/?kind=making publish: thoughts (daily/YYYY-MM-DD.md, split on ##HH:MM)/blog/#t-<timestamp> publish: about/about/ attachments/<file>/img/<file> (R2 in prod via media.mattdoes.online) last.fm recent tracks/listening/ (static at build · refreshed live) listening worker/api/listening/{now,recent} geo worker/api/geo/lookup publish: <other>/<slug>/ (catch-all) publish: draft · missingnever built
  • [[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.
// simplified for (const note of walk('vault/')) { const { data, body } = matter(note) if (!data.publish || data.publish === 'draft') continue const html = renderMarkdown(body, { wikilinks: resolveAgainst(publishedNotes), embeds: rewriteToR2() }) const route = routeFor(note.path, data) write(`dist${route}.html`, template(data.publish, { data, html })) } writeIndex('dist/index.html') writeFeeds('dist/feed.xml')

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.

tweaks
dark mode
accent
local map
map style

picking mine uses your location once to look up your city outline. the outline is cached on this device for 7 days; your coordinates aren't saved and never leave the lookup. switch back to home to clear it.