The system
behind the studio.
Muntin Digital is a one-person studio, which means everything you see on this site — the code, the type, the brand, the translations, the audio narration — was built, shipped, and maintained by one human. This page shows the work.
Deliberately
small.
Zero frameworks, zero build step for the HTML itself, zero client-side rendering. The stack fits in one person's head, deploys in under a minute, and runs on a free tier.
- Hosting Cloudflare Pages Static HTML served from the edge. ~30ms TTFB from the DMV, ~80ms from Europe.
-
Dynamic routes
Cloudflare Workers
Tool endpoints (the restaurant audit, grader APIs) run at the edge with KV caching. Single
src/worker.js. -
HTML
Handwritten
Every page is a plain
index.html. Nav + footer are synced from_includes/by a 240-line Node script at deploy. -
CSS
One stylesheet, tokenized
A single
site.csswith:rootdesign tokens for color, type, spacing, radius, and motion. No preprocessor. -
JS
One stylesheet's worth
A single
site.js, deferred, no framework. Hosts the audio player, share menu, language detection, and form validation. -
Type
Self-hosted
Fraunces (display) and Inter (body) served from
/assets/fonts/aswoff2with metric-matched fallbacks. Zero Google Fonts round-trip. - Analytics Plausible Privacy-respecting, cookie-free, GDPR-clean. No consent banner required; no tracking pixel soup.
- Build Node scripts A handful of small Node scripts for include sync, hreflang stamping, locale parity checks, OG image rendering, audio pipeline, and SEO enrichment.
One palette,
one type scale, one motion curve.
Every color, space, radius, and transition on the site resolves to a CSS custom property defined once in :root. Changing the primary teal swaps it everywhere — header, footer, links, focus rings, button hover states — in a single line.
Palette
--teal#1F4E5B--teal-dark#143640--teal-tint#E8F1F3--rust#B8541A--ink#14161A--ink-soft#2A2D33--stone#6B6B6B--cream#FAF7F2--cream-2#F3EEE3
Type
- Fraunces
--font-displayEditorial display — H1s, feature text - Inter sans-serif body copy
--font-bodyArticle body, UI chrome, forms
Radius · Motion
--r-sm8px--r-md14px--r-lg22px--t-fast180ms--t-med420ms--ease-outcubic-bezier(.16,1,.3,1)
Bilingual,
without the overhead.
Every page ships with an English and Spanish version from day one — no third-party translation service, no runtime language swap, no JS framework. The ES mirror lives under /es/ and is indexed by Google at full priority.
-
One partial per locale
Nav and footer live in
_includes/nav.html+_includes/footer.html(English) and_includes/es/(Spanish). A build step stamps them into every page so a single edit propagates to 85 files. -
Hreflang, automatically
scripts/stamp-hreflang.mjswalks every page, detects its counterpart in the other locale, and injects the exact<link rel="alternate">pair Google expects. No manual bookkeeping. -
Runtime UI strings
JS-emitted strings (aria-labels, audio player, form validation) live in
_includes/i18n.es.jsonand get stamped into every ES page's<script>sosite.jscan read them aswindow.__i18nwithout a network request. -
Parity enforced at build
scripts/check-locale-parity.mjs --checkexits non-zero if any EN page lacks its ES counterpart. Shipping an untranslated page requires an explicit override — the default is "they stay in sync."
Thirty-one printable
paperwork artifacts.
Every sheet at /sheets/<slug>/ is a single-page interactive form: you fill it in, the math runs in your browser, you print or save it as CSV. Five packs (Operations & Margin, Local SEO & Discovery, Conversions & Reservations, Brand & Design, Trust & Reviews) ship with full Spanish parity. The pipeline is data-driven from a single source of truth so adding a sheet costs one JSON entry plus one HTML fragment.
-
Single source of truth
data/sheets.jsondeclares every sheet — title, summary, walkaway, cadence, format, pack, pairsWith.{tools,glossary,blog}, status.data/sheets.es.jsoncarries the per-sheet ES prose (when_to_use + mistakes bullets). One field flips a queued sheet to live, and the build picks it up everywhere. -
Shell + fragment composition
_includes/sheet-shell.htmlis the shared chrome: hero, breadcrumb, mm-card form container, results panel, mm-actions row, mm-save panel, pairs-with knit.scripts/sheets-fragments/<slug>.htmlis the per-sheet body — fieldsets, inputs, repeating tables, the per-sheet recalc script.scripts/build-sheet-pages.mjswraps the shell around the fragment and writes 62 pages (31 sheets × EN + ES). -
Centralized benchmarks
data/benchmarks.jsondeclares every threshold band ("prime cost 55–65% healthy") with EN + ES messages and source citation.scripts/build-sheet-benchmarks.mjsgeneratesassets/js/sheet-benchmarks.gen.js, exposingwindow.Bench.evaluate(metric, value). Update one threshold here, 31 sheets follow. -
Versioned save payloads
Workshop saves carry
{ v: 1, slug, savedAt, inputs, outputs }. The/api/workbench/sheet-historyendpoint reads back the user's prior saves of the same sheet to draw a sparkline above the save panel —extractSheetMetrichandles the v-absent fallback so saves shipped before versioning still draw cleanly. -
Reciprocal cross-links
32 glossary term pages stamp a "Use this sheet" sidecar; 10 topic-pillar pages stamp a sheet rail; 30 tool detail pages stamp a "Pair with paperwork" rail back to the sheets that use them. Four blog posts post-end-CTA into a sheet at the moment of intent. Every link is reverse-indexed from
data/sheets.json'spairsWithfield — no separate registry to drift. -
Verifiable privacy posture
scripts/check-sheet-no-fetch.mjsgreps every sheet fragment + every rendered<main>block for forbidden network patterns (fetch, XHR, sendBeacon, localStorage in fragments, eval, external script src). CI fails if any fragment contacts any URL. The "Stays in your browser" claim on every sheet links to/receipts/where the audit trail is shown.
Three restructures,
zero broken backlinks.
This site's URL structure has been reorganized three times since launch. Every inbound link from every historical path still lands on the right page, on the first hop, with SEO equity intact. It's in _redirects.
-
Sprint 1
/audit//tools/Origin. Single-purpose audit tool lived at its own path.
-
First restructure
/tools/restaurant-audit//tools/audits/restaurant/Opened room for more audit types without renaming the tool twice.
-
Today
/tools/audits/restaurant/The /tools/ category exists so new tools — generators, converters, analyzers — can ship without another restructure.
-
Sprint K
/tools/audits/wellness//tools/audits/restaurant/The wellness audit and checklist were retired when the studio focused restaurant-first. Rather than 404, inbound links funnel into the current tool.
Every article,
in six languages, read aloud.
Every blog post ships with audio narration in English, Spanish, French, Italian, Portuguese, and Chinese. The pipeline is a Node script; the player is plain JS. No streaming service, no paywall.
How it ships
scripts/render-post-audio.mjschunks the article by heading, generates each chunk via text-to-speech, and stitchesaudio.mp3into the post's own directory.- Per-paragraph
data-audio-altattributes give complex figures (charts, tables) a narrator-friendly alternative read. AudioObjectJSON-LD schema announces the narration to search engines for speakable-content eligibility.- The player itself is a single docked UI element rendered only when the reader opts in, so the default page weight stays honest.
Static index,
instant results.
The library is fully searchable without a server. Press K (or Ctrl K, or the / key) from anywhere on the site to try it.
How it ships
- Pagefind runs once at the end of each deploy, reads every built HTML file in
dist/, and emits a static index todist/pagefind/. No server, no database, no API key. - The index splits automatically by
<html lang>, so a Spanish reader gets Spanish results and an English reader gets English results — same search box, right language. - A configuration file at
pagefind.ymlexcludes navigation, footer, breadcrumb, and overlay chrome from the index so results are page content, not boilerplate. - The UI is a custom modal in
site.js(no Pagefind default UI), lazy-loaded on first open so readers who never search pay zero bytes for the feature. - Keyboard-first: arrow keys move through results, return opens, escape closes. Mouse hover highlights. Focus returns to the trigger on close.
Every external citation
has a Muntin-authored mirror.
When an article or tool on this site cites outside research, the citation points first to a Muntin-authored summary at /learn/research/, and second to the original study. The reader keeps their place on the page they were reading; the summary opens alongside in a new tab.
How it ships
- Every external research URL the library cites is mapped to an internal
/learn/research/<slug>/summary page — plain-English, restaurant-framed, with the headline finding, key numbers, and a "how Muntin uses this" reverse-index showing every article that leans on the study. scripts/wire-research-citations.mjswalks every published blog post, finds external anchors that match the known citation map, and injects a "Read Don's summary" internal link (new tab) immediately before each. Idempotent — re-runs are no-ops.- Research-note links open in a new tab (
target="_blank") so a reader in the middle of an audit or an article doesn't lose their place. The breadcrumb on the research note clearly shows "Home › Learn › Research › <note>" so the reader knows where they are. - Each research note includes full JSON-LD schema (
Article→citation→ScholarlyArticle) so search engines understand the summary-cites-source relationship. - Clicking any research-note link now opens an inline drawer on top of the origin page — a side-sheet on desktop, a bottom sheet on mobile — so the reader can see the summary, key findings, and both CTAs without leaving their audit session or article mid-scroll. The drawer lazy-fetches the note's HTML and extracts the preview via
DOMParser, caching per-slug so the second open is instant. If<dialog>.showModalorfetchis unavailable — or the reader holds ⌘/Ctrl/shift to force a new tab — the anchor's originaltarget="_blank"takes over. Zero regression, full progressive enhancement.
The quiet
fundamentals.
Accessibility isn't a section of the site — it's a default the whole site respects. Here's what that looks like in practice.
-
Skip links
Every page opens with
<a class="skip-link" href="#main">so keyboard users bypass the nav on every load. -
Semantic landmarks
Primary nav, mobile menu, and language switcher each carry a distinct
aria-label. Every section references its heading viaaria-labelledby. -
Visible focus
A dedicated
--ring-focustoken renders a 3px teal ring on every focusable element. Never removed, never replaced withoutline: none. -
Reduced motion
Scroll reveals, listening-callout pulse, and audio player transitions all respect
prefers-reduced-motionand shorten to near-zero when set. -
Color contrast
Body text on cream clears WCAG AA at 16.4:1; link teal clears AA at 4.8:1; focus ring clears AA Large on every background in the palette.
-
Keyboard-complete
Mobile menu, share menu, audio player, language switcher, and every form are fully operable with keyboard alone.
aria-expandedandaria-haspopupare accurate.