El sistema
detrás del estudio.
Muntin Digital es un estudio de una sola persona, lo que significa que todo lo que ves en este sitio — el código, la tipografía, la marca, las traducciones, la narración en audio — lo construyó, lo publicó y lo mantiene una sola persona. Esta página muestra el trabajo.
Deliberadamente
pequeño.
Cero frameworks, cero paso de build para el propio HTML, cero renderizado del lado del cliente. El stack le cabe a una persona en la cabeza, despliega en menos de un minuto y corre en una capa gratuita.
- Hosting Cloudflare Pages HTML estático servido desde el edge. ~30 ms de TTFB desde el DMV, ~80 ms desde Europa.
-
Rutas dinámicas
Cloudflare Workers
Los endpoints de las herramientas (diagnóstico de restaurante, APIs de calificadores) corren en el edge con caché en KV. Un solo
src/worker.js. -
HTML
Escrito a mano
Cada página es un
index.htmlplano. El nav y el footer se sincronizan desde_includes/con un script de Node de 240 líneas en cada deploy. -
CSS
Una hoja, con tokens
Un solo
site.csscon tokens de diseño en:rootpara color, tipografía, espaciado, radio y movimiento. Sin preprocesador. -
JS
Un solo archivo
Un solo
site.js, condefer, sin framework. Aloja el reproductor de audio, el menú para compartir, la detección de idioma y la validación de formularios. -
Tipografía
Auto-hospedada
Fraunces (display) e Inter (texto) servidas desde
/assets/fonts/comowoff2con fallbacks de métricas emparejadas. Cero viaje a Google Fonts. - Analítica Plausible Respeta la privacidad, sin cookies, limpia ante el GDPR. Sin banner de consentimiento; sin sopa de píxeles de rastreo.
- Build Scripts de Node Un puñado de scripts de Node para sincronizar los includes, estampar hreflang, revisar paridad de locales, renderizar imágenes OG, el flujo de audio y enriquecer el SEO.
Una paleta,
una escala tipográfica, una curva de movimiento.
Cada color, cada espacio, cada radio y cada transición del sitio resuelve a una propiedad personalizada de CSS definida una sola vez en :root. Cambiar el teal principal lo cambia en todos lados — header, footer, enlaces, anillos de foco, estados hover de botones — en una sola línea.
Paleta
--teal#1F4E5B--teal-dark#143640--teal-tint#E8F1F3--rust#B8541A--ink#14161A--ink-soft#2A2D33--stone#6B6B6B--cream#FAF7F2--cream-2#F3EEE3
Tipografía
- Fraunces
--font-displayDisplay editorial — H1, texto destacado - Inter en sans-serif para texto
--font-bodyCuerpo del artículo, chrome de UI, formularios
Radio · Movimiento
--r-sm8px--r-md14px--r-lg22px--t-fast180ms--t-med420ms--ease-outcubic-bezier(.16,1,.3,1)
Bilingüe,
sin el sobrecosto.
Cada página sale al aire con versión en inglés y en español desde el primer día — sin servicio de traducción de terceros, sin cambio de idioma en tiempo de ejecución, sin framework de JS. La versión en español vive bajo /es/ y Google la indexa con prioridad completa.
-
Un partial por idioma
El nav y el footer viven en
_includes/nav.html+_includes/footer.html(inglés) y_includes/es/(español). Un paso de build los estampa en cada página, así una sola edición se propaga a 85 archivos. -
Hreflang, automático
scripts/stamp-hreflang.mjsrecorre cada página, detecta su contraparte en el otro idioma, e inyecta el par exacto de<link rel="alternate">que Google espera. Sin papeleo manual. -
Strings de UI en tiempo de ejecución
Las cadenas emitidas por JS (aria-labels, reproductor de audio, validación de formularios) viven en
_includes/i18n.es.jsony se estampan dentro del<script>de cada página en español, para quesite.jslas lea comowindow.__i18nsin una petición de red. -
Paridad obligada en el build
scripts/check-locale-parity.mjs --checksale con código de error si alguna página en inglés no tiene su contraparte en español. Publicar una página sin traducir exige un override explícito — lo normal es "se mantienen en sincronía".
Treinta y un papeles
imprimibles.
Cada hoja en /sheets/<slug>/ es un formulario interactivo de una sola página: la llenas, la matemática corre en tu navegador, la imprimes o la guardas como CSV. Cinco paquetes (Operaciones y Márgenes, SEO local y Descubrimiento, Conversiones y Reservas, Marca y Diseño, Confianza y Reseñas) en paridad EN+ES. El pipeline está dirigido por datos desde una sola fuente de verdad, así que agregar una hoja cuesta una entrada de JSON y un fragmento HTML.
-
Una sola fuente de verdad
data/sheets.jsondeclara cada hoja — título, resumen, walkaway, cadencia, formato, paquete, pairsWith.{tools,glossary,blog}, status.data/sheets.es.jsonlleva la prosa larga en español (bullets de when_to_use + mistakes). Un campo cambia una hoja de queued a live, y el build la recoge en todos lados. -
Composición shell + fragment
_includes/sheet-shell.htmles la chrome compartida: hero, breadcrumb, mm-card, panel de resultados, fila de mm-actions, panel mm-save, knit de pairs-with.scripts/sheets-fragments/<slug>.htmles el cuerpo por hoja — fieldsets, inputs, tablas repetidas, el script de recalc específico.scripts/build-sheet-pages.mjsenvuelve la shell alrededor del fragment y escribe 62 páginas (31 hojas × EN + ES). -
Bandas centralizadas
data/benchmarks.jsondeclara cada banda de umbral ("costo primo 55–65% saludable") con mensajes EN + ES y cita de fuente.scripts/build-sheet-benchmarks.mjsgeneraassets/js/sheet-benchmarks.gen.js, que exponewindow.Bench.evaluate(metric, value). Actualiza un umbral aquí y 31 hojas siguen. -
Payloads de guardado versionados
Los guardados en el Taller llevan
{ v: 1, slug, savedAt, inputs, outputs }. El endpoint/api/workbench/sheet-historylee los guardados previos del usuario para la misma hoja y dibuja una sparkline encima del panel de guardar —extractSheetMetricmaneja el fallback v-absent para que los guardados anteriores al versionado sigan dibujándose limpios. -
Cross-links recíprocos
32 páginas de glosario estampan un sidecar "Usa esta hoja"; 10 páginas pillar de tema estampan un rail de hojas; 30 páginas de detalle de herramienta estampan un rail "Empareja con papeleo" hacia las hojas que las usan. Cuatro posts del blog enlazan vía post-end-CTA hacia una hoja en el momento de intención. Cada enlace se indexa al revés desde el campo
pairsWithdedata/sheets.json— sin registro separado que se desfase. -
Postura de privacidad verificable
scripts/check-sheet-no-fetch.mjsrastrea cada fragmento y cada bloque<main>renderizado en busca de patrones de red prohibidos (fetch, XHR, sendBeacon, localStorage en fragmentos, eval, src de script externo). CI falla si algún fragmento contacta cualquier URL. La promesa "se queda en tu navegador" en cada hoja enlaza a/es/receipts/donde el rastro de auditoría está visible.
Tres reestructuraciones,
cero enlaces rotos.
La estructura de URLs de este sitio se ha reorganizado tres veces desde el lanzamiento. Cada enlace entrante desde cada ruta histórica sigue cayendo en la página correcta, en el primer salto, con el equity de SEO intacto. Todo está en _redirects.
-
Sprint 1
/audit//tools/Origen. La herramienta de diagnóstico vivía en su propia ruta, dedicada solo a eso.
-
Primera reestructuración
/tools/restaurant-audit//tools/audits/restaurant/Abrió espacio para más tipos de diagnóstico sin tener que renombrar la herramienta dos veces.
-
Hoy
/tools/audits/restaurant/La categoría /tools/ existe para que nuevas herramientas — generadores, convertidores, analizadores — puedan lanzarse sin otra reestructuración.
-
Sprint K
/tools/audits/wellness//tools/audits/restaurant/El diagnóstico de wellness y su checklist se retiraron cuando el estudio se enfocó primero en restaurantes. En lugar de un 404, los enlaces entrantes caen en la herramienta actual.
Cada artículo,
leído en voz alta en seis idiomas.
Cada artículo del blog sale al aire con narración en inglés, español, francés, italiano, portugués y chino. El flujo es un script de Node; el reproductor es JS puro. Sin servicio de streaming, sin muro de pago.
Cómo se publica
scripts/render-post-audio.mjscorta el artículo por encabezados, genera cada fragmento con text-to-speech y cose elaudio.mp3dentro del propio directorio del artículo.- Los atributos
data-audio-altpor párrafo les dan a las figuras complejas (gráficas, tablas) una lectura alternativa pensada para el narrador. - El schema JSON-LD
AudioObjectle anuncia la narración a los buscadores para ser elegible como contenido speakable. - El reproductor en sí es un solo elemento acoplado en pantalla que solo se renderiza cuando el lector decide activarlo, así el peso por defecto de la página se mantiene honesto.
Índice estático,
resultados al instante.
La biblioteca se puede buscar completa sin servidor. Presiona K (o Ctrl K, o la tecla /) desde cualquier parte del sitio para probarla.
Cómo se publica
- Pagefind corre una sola vez al final de cada deploy, lee cada HTML construido en
dist/y emite un índice estático adist/pagefind/. Sin servidor, sin base de datos, sin API key. - El índice se divide automáticamente por
<html lang>, así un lector en español recibe resultados en español y uno en inglés recibe resultados en inglés — misma caja de búsqueda, idioma correcto. - Un archivo de configuración en
pagefind.ymlexcluye la navegación, el footer, las migas de pan y el chrome de overlay del índice, para que los resultados sean contenido de la página, no relleno. - La UI es una ventana modal a medida en
site.js(sin la UI por defecto de Pagefind), cargada de forma perezosa la primera vez que se abre, así los lectores que nunca buscan no pagan ni un byte por la función. - Primero el teclado: las flechas mueven entre resultados, intro abre, escape cierra. El mouse resalta al pasar. El foco regresa al disparador al cerrar.
Cada cita externa
tiene un espejo escrito por Muntin.
Cuando un artículo o herramienta de este sitio cita investigación externa, la cita apunta primero a un resumen escrito por Muntin en /es/learn/research/, y después al estudio original. El lector conserva su lugar en la página que estaba leyendo; el resumen se abre al lado en una pestaña nueva.
Cómo se publica
- Cada URL externa de investigación que cita la biblioteca está mapeada a una página interna de resumen en
/learn/research/<slug>/— en lenguaje claro, con marco de restaurante, con el hallazgo principal, los números clave y un índice inverso de "cómo lo usa Muntin" que muestra cada artículo que se apoya en el estudio. scripts/wire-research-citations.mjsrecorre cada artículo publicado del blog, encuentra anclas externas que coinciden con el mapa conocido de citas, e inyecta un enlace interno a "Leer el resumen de Don" (pestaña nueva) inmediatamente antes de cada una. Es idempotente — re-ejecutarlo no hace nada.- Los enlaces a notas de investigación se abren en una pestaña nueva (
target="_blank") para que un lector a medio diagnóstico o a medio artículo no pierda su lugar. Las migas de pan en la nota muestran claramente "Inicio › Aprende › Investigación › <nota>" así el lector sabe dónde está. - Cada nota de investigación incluye schema JSON-LD completo (
Article→citation→ScholarlyArticle) para que los buscadores entiendan la relación resumen-cita-fuente. - Hacer clic en cualquier enlace a una nota de investigación ahora abre un cajón inline sobre la página de origen — lateral en escritorio, desde abajo en móvil — así el lector puede ver el resumen, los hallazgos clave y ambos CTA sin salir de su diagnóstico o abandonar el artículo a media lectura. El cajón carga la nota de forma perezosa y extrae el preview con
DOMParser, con caché por slug para que la segunda apertura sea instantánea. Si<dialog>.showModalofetchno están disponibles — o el lector presiona ⌘/Ctrl/shift para forzar una pestaña nueva — eltarget="_blank"original del enlace toma el relevo. Cero regresión, mejora progresiva completa.
Los fundamentos
silenciosos.
La accesibilidad no es una sección del sitio — es un valor por defecto que respeta el sitio entero. Así se ve en la práctica.
-
Enlaces de salto
Cada página abre con
<a class="skip-link" href="#main">para que quien navega con teclado se salte el nav en cada carga. -
Landmarks semánticos
El nav principal, el menú móvil y el selector de idioma cargan cada uno un
aria-labeldistinto. Cada sección se refiere a su encabezado víaaria-labelledby. -
Foco visible
Un token dedicado
--ring-focuspinta un anillo teal de 3 px sobre cada elemento enfocable. Nunca se quita, nunca se reemplaza poroutline: none. -
Movimiento reducido
Los reveals al hacer scroll, el pulso del callout de audio y las transiciones del reproductor respetan
prefers-reduced-motiony se acortan a casi cero cuando está activo. -
Contraste de color
El texto sobre cream pasa WCAG AA con 16.4:1; el teal de los enlaces pasa AA con 4.8:1; el anillo de foco pasa AA Large sobre cada fondo de la paleta.
-
Todo con teclado
El menú móvil, el menú para compartir, el reproductor de audio, el selector de idioma y cada formulario son totalmente operables solo con teclado.
aria-expandedyaria-haspopupreflejan el estado real.
¿Quieres un sitio construido
como este?
Si tienes un negocio pequeño y quieres este mismo cuidado aplicado a tu sitio, agenda una llamada de veinte minutos. Si eres colega del oficio y quieres comparar notas — el correo también funciona.