Technical documentation
Colophon
How the site works, what it's made from, and why those choices were made.
The stack
Astro 6, statically generated, hosted on Netlify. Pages are HTML; interactivity is vanilla TypeScript. Netlify edge functions handle the genuinely server-side work: Last.fm album data, the AI endpoint behind /ask.
Fathom instead of Google Analytics — no cookies, no consent banner, no third-party reporting. The DLM RSS feed fetches at each deploy and is injected automatically into / and /ask.
The data
The pages that feel most like live databases are all static HTML assembled at build time from offline source material.
Music starts in Roon — a library export matched against iTunes for cover art, then enriched with Last.fm play counts and genre data. The book library was bootstrapped from photographs of physical shelves: OCR'd into JSON, imported into Hardcover, and syncing normally since. Photo essays assemble from EXIF coordinates and timestamps, enriched with bank transaction exports and unpublished images for narrative context. DLM posts fetch from RSS on every deploy.
The type system
Three typefaces: Oswald 700 for labels, headings, and any number that needs presence — its condensed proportions compress well and read confidently at small sizes. Barlow 300 for body text — light enough to feel editorial, not so light it disappears on a coloured background. Special Elite for the guestbook.
The MRFRISBY hero is sized by a canvas API at render
time. A function called fitHero() measures the
viewport width, calculates the exact font-size that fills it wall to wall and updates on resize. It fires
again on Astro's astro:page-load event so it
survives view transitions back to the home page. Both font
files are self-hosted WOFF2 with font-display: swap and latin-subset preloading.
OSWALD · 700 · CONDENSED SANS
MRFRISBY
- Site titleclamp(2rem, 6vw, 3.5rem) / −0.02em / uppercase
- Page headings1rem / 0.08em / uppercase
- Section labels, eyebrows0.55rem / 0.22em / uppercase
- Navigation, data labels0.65rem / 0.12em / uppercase
BARLOW · 300 · HUMANIST SANS
- Body text, profile copy0.875rem / normal / normal
- Long-form reading0.9rem / normal / normal
- Update text, captions0.75–0.85rem / normal / normal
SPECIAL ELITE · 400 · TYPEWRITER SERIF
Why have you done this?
- Guestbook onlyvariable / normal / normal
- Typewriter texture reinforces the found-object metaphor of the EX-WORD format
The colour system
The palette is defined in OKLCH — a perceptually uniform colour space where equal steps in lightness actually look equal, unlike HSL or hex. The dark mode anchor is a deep forest green; light mode is a cool mint. The hue rotates slightly across the lightness axis: warmer at darker values, cooler at lighter ones, which gives the two modes a coherent relationship rather than feeling like unrelated palettes.
Colour-critical properties work via progressive
enhancement: base hex values first, then oklch() inside @supports (color: oklch(0 0 0)).
P3 gamut values are noticeably more saturated on
displays that support it. The full palette, with
contrast ratios, is on /colours.
| VAR | DARK L · C · H | LIGHT L · C · H | ROLE |
|---|---|---|---|
| --bg | 0.30 · 0.090 · 143 | 0.87 · 0.075 · 158 | Page background |
| --bg-surface | 0.38 · 0.105 · 146 | 0.82 · 0.095 · 155 | Panel backgrounds |
| --bg-elevated | 0.47 · 0.118 · 148 | 0.77 · 0.110 · 152 | Hover states, raised UI |
| --border-subtle | 0.55 · 0.120 · 150 | 0.72 · 0.100 · 150 | Hairlines, dividers |
| --border | 0.66 · 0.130 · 152 | 0.63 · 0.115 · 147 | Component borders |
| --text-mid | 0.82 · 0.045 · 155 | 0.38 · 0.075 · 144 | Labels, timestamps, secondary |
| --text | 0.98 · 0.005 · 147 | 0.12 · 0.010 · 147 | Body text, headings |
| --accent | 0.82 · 0.275 · 145 | 0.42 · 0.210 · 145 | Interactive, highlights |
| --opposite | 0.63 · 0.220 · 027 | 0.88 · 0.070 · 027 | Error states, complement |
The interface concepts
Every page has a reason to look the way it does. The visual metaphor speaks to the function of the page, and a nostalgic reflection of a more optimistic time for the web. The metaphor informs the design, and the design deviates as much as the metaphor requires.
The semantic layer
Every page carries a JSON-LD @graph with typed
Person and WebSite nodes. Dates
use <time datetime="…">. Mobile nav
uses a <details> element to avoid needing
state management. Webmentions are received via webmention.io;
the <link rel="webmention"> header lives
in the document <head>. Social
profiles carry rel="me". Interactive
elements that lack visible text labels get
aria-label. Photo essays use appropriate
landmark roles.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "Person",
"name": "Stuart Frisby",
"sameAs": ["…bsky.app/profile/mrfrisby.com"] },
{ "@type": "WebSite",
"url": "https://mrfrisby.com" }
]
}
</script>
<link rel="webmention"
href="https://webmention.io/mrfrisby.com/webmention"
/> <details>
<summary>Menu</summary>
<nav aria-label="Site navigation">
<a href="https://bsky.app/…"
rel="me">Bluesky</a>
</nav>
</details>
<time datetime="2026-06-18">
18 June 2026
</time>
<button aria-label="Close panel">×</button>
<!-- photo essays -->
<article role="article">
<main aria-label="Photo essay">
…
</main>
</article>