Saltar al contenido principal
Next.jsTypeScriptTailwindMDX

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.

8 min de lectura

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-label en 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.