Colophon · Under the hood

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.

The stack

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.css with :root design 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/ as woff2 with 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.
Design tokens

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)
Internationalization

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.

  1. 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.

  2. Hreflang, automatically

    scripts/stamp-hreflang.mjs walks every page, detects its counterpart in the other locale, and injects the exact <link rel="alternate"> pair Google expects. No manual bookkeeping.

  3. Runtime UI strings

    JS-emitted strings (aria-labels, audio player, form validation) live in _includes/i18n.es.json and get stamped into every ES page's <script> so site.js can read them as window.__i18n without a network request.

  4. Parity enforced at build

    scripts/check-locale-parity.mjs --check exits 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."

Operator sheets

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.

  1. Single source of truth

    data/sheets.json declares every sheet — title, summary, walkaway, cadence, format, pack, pairsWith.{tools,glossary,blog}, status. data/sheets.es.json carries 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.

  2. Shell + fragment composition

    _includes/sheet-shell.html is the shared chrome: hero, breadcrumb, mm-card form container, results panel, mm-actions row, mm-save panel, pairs-with knit. scripts/sheets-fragments/<slug>.html is the per-sheet body — fieldsets, inputs, repeating tables, the per-sheet recalc script. scripts/build-sheet-pages.mjs wraps the shell around the fragment and writes 62 pages (31 sheets × EN + ES).

  3. Centralized benchmarks

    data/benchmarks.json declares every threshold band ("prime cost 55–65% healthy") with EN + ES messages and source citation. scripts/build-sheet-benchmarks.mjs generates assets/js/sheet-benchmarks.gen.js, exposing window.Bench.evaluate(metric, value). Update one threshold here, 31 sheets follow.

  4. Versioned save payloads

    Workshop saves carry { v: 1, slug, savedAt, inputs, outputs }. The /api/workbench/sheet-history endpoint reads back the user's prior saves of the same sheet to draw a sparkline above the save panel — extractSheetMetric handles the v-absent fallback so saves shipped before versioning still draw cleanly.

  5. 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's pairsWith field — no separate registry to drift.

  6. Verifiable privacy posture

    scripts/check-sheet-no-fetch.mjs greps 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.

URL history

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.

  1. Sprint 1 /audit/ /tools/

    Origin. Single-purpose audit tool lived at its own path.

  2. First restructure /tools/restaurant-audit/ /tools/audits/restaurant/

    Opened room for more audit types without renaming the tool twice.

  3. Today /tools/audits/restaurant/

    The /tools/ category exists so new tools — generators, converters, analyzers — can ship without another restructure.

  4. 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.

Audio narration

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.mjs chunks the article by heading, generates each chunk via text-to-speech, and stitches audio.mp3 into the post's own directory.
  • Per-paragraph data-audio-alt attributes give complex figures (charts, tables) a narrator-friendly alternative read.
  • AudioObject JSON-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.
Search

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 to dist/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.yml excludes 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.
Research

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.mjs walks 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 (ArticlecitationScholarlyArticle) 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>.showModal or fetch is unavailable — or the reader holds ⌘/Ctrl/shift to force a new tab — the anchor's original target="_blank" takes over. Zero regression, full progressive enhancement.
Accessibility

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 via aria-labelledby.

  • Visible focus

    A dedicated --ring-focus token renders a 3px teal ring on every focusable element. Never removed, never replaced with outline: none.

  • Reduced motion

    Scroll reveals, listening-callout pulse, and audio player transitions all respect prefers-reduced-motion and 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-expanded and aria-haspopup are accurate.

And if you want to talk about it

Want a site built
like this one?

If you're a small-business owner and this is the kind of care you want applied to your site, book a twenty-minute call. If you're a fellow builder and you want to compare notes — email works too.