mattdoes.online colophon say hi
docsยทupdated apr 20

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 โ€” not a submodule, since CF Pages' GitHub App auth doesn't reach submodule clones.
โ”Œ vault (private ยท obsidian)
โ”œโ”€โ”€ daily
โ”‚   โ””โ”€โ”€ YYYY-MM-DD.md   # micro-posts ยท one ##HH:MM = one thought
โ”œโ”€โ”€ notes
โ”‚   โ”œโ”€โ”€ dev              # โ†’ /journal/<slug>/
โ”‚   โ”œโ”€โ”€ making           # โ†’ /making/<slug>/
โ”‚   โ””โ”€โ”€ ideas            # never published
โ”œโ”€โ”€ attachments          # images / audio / video
โ””โ”€โ”€ .obsidian/           # ignored

โ”Œ mattdoes-site (public)
โ”œโ”€โ”€ build.js             # the generator
โ”œโ”€โ”€ site.config.js       # identity + last.fm
โ”œโ”€โ”€ templates
โ”œโ”€โ”€ vault                โ†’ cloned pre-build
โ””โ”€โ”€ dist                 # deployed
Every note that wants to appear on the site declares it. publish: is the only required field.
keytypereqdescription
publishenumreqjournal ยท thoughts ยท making ยท draft (listening is pulled from last.fm)
titlestringoptDisplay title. Defaults to filename.
datedateoptISO 8601. 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.
statusenumoptseed ยท growing ยท evergreen
updateddateoptLast meaningful edit.
coverpathoptImage in vault/attachments/. Rewritten at build.
aliases[string]optExtra wikilink targets.
notes/dev/<slug>.mdโ†’/journal/<slug> notes/making/<slug>.mdโ†’/making/<slug> daily/YYYY-MM-DD.md ##HH:MMโ†’/thoughts#t-NNN attachments/<file>โ†’/img/<file> last.fm recent tracksโ†’/listening/ notes/ideas/*.mdโœ•never 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')

Four templates, three content types, one loop. If a new section shows up (say, publish: recipes), it's a new template file and one line in routeFor(). That's the whole extension story.

tweaks