How this was made
Colophon
How this was made. This site is a small workshop, built in the open. Every line of type is self-hosted, every color is deliberate, and every animation knows how to sit still when you ask it to. It's an Astro site — content-first, almost no JavaScript — with the writing and projects living in plain Markdown so they outlast any framework I might regret later.
The unusual part: I can publish to it from an MCP server. There's a small Model Context Protocol server (both a local stdio transport and an authenticated HTTP one, bearer-token gated) that writes new posts and projects straight into the content folder and triggers a rebuild. The trust model is simple and strict: the author — me, holding the token — is trusted and can ship real bespoke HTML, CSS, and JavaScript inside a post. You, the visitor, are not granted that trust and never need to be: every post's custom code is isolated so it can't reach the site's navigation or any other post. One page can be weird without making the rest of the site fragile.
The stack
- Astro — static, pre-rendered, island hydration only where needed.
- Markdown content collections — typed with Zod; 5 projects, 3 field notes.
- Vanilla TS islands — no UI framework; content pages ship <~100 KB JS.
- satori + resvg — per-post OG images rendered at build, in this site's style.
- sharp — image optimization (also used by the MCP upload tool).
Type & color
- Space Grotesk — display + UI chrome.
- Source Serif 4 — long-form reading.
- Space Mono — labels, badges, code.
- All self-hosted, subset, preloaded — zero third-party calls.
- Two color "worlds" (day / night) toggled live; every body pairing meets WCAG AA.
The blog has its own MCP server
An AI agent connects over the Model Context Protocol and can do the whole publishing loop —
draft, edit, illustrate, set bespoke code, publish, rebuild. Locally it speaks
stdio; remotely it speaks authenticated HTTP (a bearer token from the
environment). Both write the same Markdown files this site reads, so the content folder
stays the single source of truth. Setup lives in
mcp-server/README.md.
| Tool | Input | Does |
|---|---|---|
list_posts | status?, tag?, limit?, offset? | Browse posts. |
get_post | slug | Full post incl. body + customCode. |
create_post | title, body, excerpt, tags[], status, customCode? | Write a new post. |
update_post | slug, partialFields | Edit any field. |
set_post_status | slug, draft|published | Publish or unpublish. |
set_post_custom_code | slug, {html?,css?,js?} | Set a post's bespoke code. |
delete_post | slug | Remove a post. |
upload_image | postSlug?, filename, dataBase64|url, alt | Optimize + store an image → {url,width,height}. |
get_site_info | — | Base URL, counts, build status. |
trigger_rebuild | — | Rebuild the static site. |
get_build_status | — | Poll the last build. |
The per-post custom-code trust model
Trusted: the author
Authenticated via the MCP server (local process, or a bearer token over HTTP). Can write arbitrary HTML, CSS, and JavaScript into a post. That's the whole point — a field note can carry a live diagram, a custom visualization, or a layout unlike any other.
Not trusted: visitors
You read the site; you have no input path into any post's code. Nothing you do reaches the author surface, so there's no stored-XSS vector to exploit.
How a weird post stays contained
- Shadow DOM (default) scopes a post's CSS/HTML; its JS runs from a blob-module import (no
eval) and only sees its own shadow root. - Sandboxed iframe (
sandbox="allow-scripts", no same-origin) for heavier pieces — an opaque origin that can't touch the parent DOM, cookies, or storage. - A source-locking CSP pins every resource to this origin. (The strong isolation comes from the sandbox + Shadow DOM, not the CSP — so inline author code inside a sandboxed frame is fine, because there's no untrusted input to weaponize it.)
- Result: one post can set
nav {display:none}or delete the whole document and the global nav and every other post are untouched. See it running →