mattdoes.online
Open site menu

colophon

updated

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   # notebook · one ##HH:MM = one short note
├── 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
├── design-system        # @mattdoes/ds — React page components · renders every page · syncs to Claude Design · ships the timeline island
├── site.config.js       # identity + last.fm
├── templates            # shared row renderers · helpers · asset registry
├── static               # css, js (tweaks · geo-bg · live), fonts, baked geojson, _headers
├── scripts              # prebuild, optimize-media, sync-media, bake-geo
├── workers              # listening · geo · csp-report · 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)/thoughts/#t-<timestamp> (grouped by notebook day) tags across kinds/topics/<topic>/ (durable cross-kind paths) publish: about/about/ attachments/<file>/img/<file> (R2 in prod via media.mattdoes.online) last.fm recent tracks/listening/ (static at build · refreshed live) search index/search/ · /search-index.json (client-filtered) og card per post/og/<kind>/<slug>.png (1200×630 · sharp) listening worker/api/listening/{now,recent} geo worker/api/geo/lookup csp-report worker/api/csp-report (report-only sink) 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`, renderPage(data.publish, { data, html })) } writeIndex('dist/index.html') writeFeeds('dist/feed.xml')

One pass, deep projections. Intake turns notes into the content model — frontmatter validation, Thought splitting with source-day provenance, stable IDs, and the slug index. Emit renders markdown, then derives Notebook days, cross-kind Topic paths, and the editorial home projection before writing React-rendered static pages, hashed assets, search, and feeds. The only client-side React is the filter/density island on exhaustive archive timelines; the homepage, Notebook, and Topic paths ship as plain HTML with no hydration. Each long-form post also gets a build-time Open Graph card, and /search-index.json backs the client-filtered search page.

Two dynamic surfaces, both same-origin Workers; connect-src 'self' holds. On /api/listening/* a single-writer Durable Object — the ListeningPoller — is the only thing that calls Last.fm: a self-rescheduling ~25s alarm makes one user.getrecenttracks call, derives both the now-playing pill and the recent-tracks list from it, and writes them to KV; a cron watchdog re-arms the alarm if it ever stops. The request path just reads KV and serves — it never calls Last.fm and never blocks, so upstream volume is constant whatever the traffic. /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. Every Worker answers 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; the CSP is strict — no third-party connect and no 'unsafe-inline' anywhere. The few inline <head> scripts the document shell emits — the importmap and the speculation rules that prerender same-origin links — are admitted by a per-build sha256 in script-src, recomputed each build so the hashes track the content-hashed module URLs. style-src stays 'self' too (Shiki's per-token colors are de-duplicated into build-time CSS classes rather than inline styles). A Report-Only policy mirrors the enforced one to a same-origin /api/csp-report sink, so the next tightening is measured against real traffic first. Mail is Fastmail, contact is a plain mailto:, no form worker.

build snapshot

notes read
11
pages written
8
build time
2.2s
dist size
1134 KB
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.