post.md
23 oct 2026 · 4 min de lectura
Cómo está construido este sitio
Un recorrido breve por la arquitectura de cmkrw.com, desde el diseño de terminal hasta Markdown, gráficos e informes de errores.
- nextjs
- design
Contenido
Este sitio es pequeño a propósito: una página, un blog y el mínimo de JavaScript posible. Así encajan las piezas, por si quieres construir algo parecido.
La terminal es el diseño
El sitio está organizado como una aplicación de terminal. Una barra de pestañas recorre la parte superior, una línea de estado ocupa la inferior y todo lo que hay en medio es un panel: una caja con borde y su título integrado en el borde superior. Un pequeño componente, Pane, dibuja todos.
El blog como vista maestro-detalle: la lista permanece fija mientras cambia la entrada.
La navegación sigue la misma idea. En el blog, la lista de entradas permanece a la izquierda mientras la entrada se abre a la derecha, así que moverse entre entradas nunca necesita un enlace de volver. Los atajos funcionan como en una terminal, y puedes ordenar esta tabla haciendo clic en el encabezado de una columna:
| 1 | Abrir inicio | global |
| 2 | Abrir blog | global |
| j | Bajar | global |
| k | Subir | global |
| g g | Ir al inicio | global |
| G | Ir al final | global |
| n | Entrada más antigua | blog |
| p | Entrada más reciente | blog |
| d | Cambiar tema | global |
| ? | Mostrar atajos | global |
El movimiento es sobre todo CSS. El comando del encabezado se escribe con una animación steps(), los paneles aparecen al desplazarse a la vista, y tanto la línea bajo la barra de pestañas como el porcentaje de la barra de estado siguen la posición de desplazamiento. Todo se desactiva con prefers-reduced-motion.
Escribir entradas
Las entradas son archivos Markdown en content/blog/<slug>/<locale>.md, renderizados con TanStack Markdown. Si falta una traducción, se usa el archivo en inglés, y draft: true deja una entrada fuera de las compilaciones de producción.
Tres piezas de TanStack hacen el resto:
- Markdown analiza y renderiza la entrada como elementos de React, sin una cadena HTML de por medio.
- Highlight colorea los bloques de código. Solo se incluyen los lenguajes que registro, y los colores salen de las mismas variables CSS que el resto del tema.
- Charts y Table se incrustan con un comentario de una línea que nombra un archivo JSON junto a la entrada.
Los bloques de código pueden llevar un título de archivo:
export function createEmbeds(slug: string, locale: string) {
return {
chart: ({ data = "", kind = "bar", x = "x", y = "y", title = "Chart" }) => (
<TuiChart
rows={getPostDataset(slug, data, locale)}
kind={kind}
x={x}
y={y}
label={title}
/>
),
}
}Las tablas de Markdown normales también funcionan y adoptan el mismo estilo:
| Capa | Carpeta | Responsabilidad |
|---|---|---|
| Rutas | app/ | Páginas, metadatos, iconos e imágenes |
| Interfaz | components/tui/ | Paneles, barra superior, barra de estado, atajos |
| Funciones | features/ | Secciones de inicio y el blog |
| Utilidades | lib/, hooks/ | Metadatos, observabilidad, entrada de teclas |
Este es el número de archivos fuente de cada carpeta en el momento de escribir:
Ver datos
| folder | files |
|---|---|
| app | 17 |
| components | 16 |
| lib | 16 |
| features | 15 |
| i18n | 4 |
| hooks | 3 |
| types | 1 |
Iconos y tarjetas sociales generados
Next.js 16 genera imágenes de metadatos a partir de archivos del directorio app/:
icon.tsxyapple-icon.tsxgeneran el favicon y el icono de iOS a partir de un único SVG,assets/logo-icon.svg.opengraph-image.tsxgenera la tarjeta social de la página de inicio, y otra genera una tarjeta para cada entrada.
No existe favicon.tsx. La documentación es explícita: un favicon no se puede generar, así que icon.tsx hace ese trabajo. Todo esto comparte un pequeño conjunto de primitivas, de modo que cambiar la marca cambia el favicon y todas las tarjetas sociales a la vez.
Instrumentación
Dos archivos se encargan de la observabilidad. instrumentation.ts se ejecuta en el servidor e informa de los errores de las peticiones. instrumentation-client.ts se ejecuta antes de la hidratación y registra errores del cliente, métricas web y cambios de ruta. Ambos pasan los eventos a un reportero que los reparte entre varios destinos:
export const serverReporter = createReporter([
consoleSink,
...(endpoint ? [endpointSink(endpoint)] : []),
])Los eventos del cliente se envían a /api/observability, que los registra. Apuntar el reportero a un colector real más adelante significa añadir un destino, sin tocar ninguno de los dos archivos de entrada.
Traducciones
El sitio está disponible en diez idiomas, incluidos el árabe y el hebreo, que se leen de derecha a izquierda. Cada texto vive en un archivo de mensajes por idioma; si alguna vez falta una clave, se mezclan por debajo los mensajes del idioma predeterminado, así que una página nunca se queda en blanco. Las entradas funcionan igual: cada idioma tiene su propio archivo y, si falta, se usa el inglés.