Cómo he construido esta web: Next.js, MDX y un blog bilingüe con export estático
La arquitectura detrás de blasruizraul.com: Next.js 14 con App Router, Tailwind CSS, blog en MDX, internacionalización ES/EN con dos root layouts y despliegue estático en Netlify.
Esta web es, además de mi portfolio, un proyecto más: decisiones de arquitectura, trade-offs y algún que otro callejón sin salida. En este artículo documento cómo está construida, por si te sirve de referencia para montar la tuya.
El stack
- Next.js 14 (App Router) con TypeScript como base.
- Tailwind CSS para los estilos, con un pequeño sistema de diseño propio (glass cards, acentos neón, animaciones respetuosas con
prefers-reduced-motion). - MDX para el blog, renderizado con
next-mdx-remote. - Vitest + Testing Library para los tests.
- Export estático (
output: 'export') desplegado en Netlify.
La decisión más importante es la última: la web no tiene servidor. Cada build genera HTML estático puro, lo que significa hosting gratuito, tiempos de respuesta mínimos y una superficie de ataque prácticamente nula. A cambio, renuncias a todo lo dinámico en runtime — y esa restricción condiciona el resto de la arquitectura.
El blog: MDX + frontmatter
Cada artículo es un fichero .mdx en content/blog/ con su frontmatter (título, fecha, extracto, tags, idioma). Una pequeña librería (lib/posts.ts) lee el directorio, valida los metadatos y expone los posts ordenados por fecha.
export interface PostMeta {
slug: string
title: string
date: string
excerpt: string
tags: string[]
readingTime: number
lang: 'es' | 'en'
}El renderizado usa next-mdx-remote/rsc con remark-gfm (tablas), rehype-slug (anclas en los headings) y rehype-pretty-code (resaltado de sintaxis). Sobre eso, componentes propios inyectados en el MDX: un Callout para notas y advertencias, y un CodeBlock con botón de copiar.
El tiempo de lectura se calcula automáticamente a partir del número de palabras si no se indica en el frontmatter — un detalle pequeño, pero es el tipo de cosa que un fichero de contenido no debería tener que mantener a mano.
Internacionalización sin servidor
La parte más interesante técnicamente. Quería la web completa en español e inglés con dos requisitos: que las URLs en español no cambiaran (la raíz sigue siendo /) y que cada blog mostrara solo su idioma.
El problema: con output: 'export' no existe el i18n nativo de Next.js. La solución, dos root layouts mediante route groups:
app/
(es)/
layout.tsx ← <html lang="es">
page.tsx ← /
blog/... ← /blog
(en)/
layout.tsx ← <html lang="en">
en/
page.tsx ← /en
blog/... ← /en/blog
Cada layout declara su <html lang>, y todo el chrome del sitio (nav, footer, skip-link) se comparte a través de un componente SiteShell parametrizado por idioma.
Los textos viven en diccionarios tipados: es.ts define la forma con export type Dictionary = typeof es, y en.ts se anota con ese tipo — si a la traducción le falta una clave, el build falla en compilación. No hay librería de i18n: son objetos TypeScript planos que los server components pasan a los client components como props.
Para emparejar los artículos entre idiomas hay un slug-map.ts que es la única fuente de verdad, protegido por un test que lee el frontmatter real de content/blog y falla si un post nuevo no tiene su par. El selector de idioma del nav usa ese mapa para llevarte al artículo equivalente, no a la home.
Detalles que marcan la diferencia
- Scroll-spy en el nav con
IntersectionObserver: la sección visible se resalta sin librerías. - Contadores animados en las estadísticas, con easing propio y desactivados si el usuario prefiere movimiento reducido.
- Tabla de contenidos generada del propio MDX, con la sección activa resaltada durante la lectura.
- hreflang y canonical en todas las páginas emparejadas, para que Google entienda la relación entre versiones.
- Accesibilidad: skip-link,
aria-labelen todo lo interactivo, foco visible, objetivo WCAG AA.
El proceso: desarrollado con IA, revisado por humanos
Coherente con lo que cuento en mi artículo sobre la IA: esta web está construida con Claude Code como copiloto — la arquitectura i18n, el refactor a diccionarios y los tests salieron de sesiones de trabajo conjunto. Mi papel: decidir, revisar y cortar cuando la propuesta no encajaba. El resultado es código que entiendo línea a línea, que es exactamente el criterio que defiendo.
Conclusión
Una web personal es el proyecto perfecto para tomar decisiones de arquitectura sin presión: aquí caben un export estático radical, un sistema i18n hecho a mano y un blog en MDX con tests. Todo el código es open source — y si algo de lo que has leído te encaja, el blog irá documentando la evolución.