Todos los artículos

Publicar un portfolio con Next.js 16 y MDX

2 min de lectura

  • Next.js
  • TypeScript
  • Herramientas

Este sitio fue una sola página durante un tiempo. Añadir un blog obligó a responder primero una pregunta: ¿dónde vive el contenido? Un CMS quedaba descartado: no hay login, ni base de datos, y quería los artículos en el mismo flujo de revisión que el código. Así que los artículos son ficheros MDX en content/blog/, y toda la sección es una única ruta dinámica que se compila en tiempo de build.

Una capa de contenido, no una convención de rutas

El atajo tentador es meter cada fichero .mdx en app/blog/<nombre>/page.mdx y dejar que el enrutado por ficheros haga el resto. Funciona, y aquí es la forma equivocada. Los nombres de fichero se convertirían en URLs, así que renombrar uno rompería un enlace en silencio, y las traducciones, los borradores y el anterior/siguiente acabarían repartidos por el árbol.

En su lugar, cada artículo declara su propia identidad en el frontmatter:

slug: publicar-un-portfolio-con-nextjs
lang: es
translationKey: nextjs-portfolio
title: Publicar un portfolio con Next.js 16 y MDX
publishedAt: 2025-11-18
tags:
  - nextjs
draft: false

Un módulo lib/blog/posts.ts lee esos ficheros con gray-matter, los valida, los ordena de más nuevo a más antiguo y devuelve objetos planos. La ruta en sí es aburrida, que es justo lo que se busca.

Note

El slug se declara a mano en el frontmatter. Renombrar el fichero no cambia la URL, así que los enlaces publicados siguen funcionando.

El tiempo de lectura es parte de los datos

La tarjeta del listado y la página del artículo muestran el mismo número, así que el número se calcula una sola vez, en la capa de contenido, y se guarda en el objeto. Cualquier otra opción se desincroniza en cuanto editas uno de los dos lados.

const WORDS_PER_MINUTE = 200
 
function readingTime(body: string): number {
  const words = body.split(/\s+/).filter(Boolean)
  return Math.max(1, Math.round(words.length / WORDS_PER_MINUTE))
}

El código cuenta. Contar solo la prosa hace quedar bien un artículo que es mitad fragmentos de código.

Borradores que solo existen en local

Un artículo con draft: true es invisible en producción: sin página, sin tarjeta y sin hueco en el anterior/siguiente. En next dev se genera como cualquier otro, que es todo lo que hace falta para previsualizarlo.

Dos builds, dos públicos

El filtro de borradores tiene que vivir en la capa de contenido, no en la página. Filtrar más tarde sigue emitiendo la ruta, y entonces un enlace viejo da un 404 en producción en lugar de simplemente no existir.

Resaltado de sintaxis sin runtime

Shiki se ejecuta durante el build. Los dos temas se emiten a la vez como variables CSS, así que cambiar entre claro y oscuro es un cambio de CSS y el cliente no envía ningún resaltador.

Qué haría distinto

Dos cosas. Primero, habría escrito el validador del frontmatter antes del primer artículo, no después del tercero. Segundo, habría decidido antes el esquema de nombres de la clave de traducción.

El código de este sitioComponentes, capa de contenido y pipeline MDX, todo en el mismo repositorio.github.com