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: falseUn 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