Crear un blog MDX con Next.js desde cero: así construí este blog
El proceso de crear con Next.js 16 App Router un blog que funciona solo con archivos Markdown, sin CMS. Estructura de carpetas, configuración de MDX y SEO, explicado con el flujo de código real.
Lo esencial
Si guardas los artículos como archivos .mdx bajo src/content/posts y los renderizas con @next/mdx, el blog funciona sin CMS. El archivo es la URL, y en vez de frontmatter se usa un meta export para generar el listado y los metadatos.
En esta página
Este blog que estás leyendo no tiene base de datos ni panel de administración. Cada artículo es un solo archivo Markdown. Aquí te cuento exactamente cómo lo construí.
Por qué basado en archivos
Hay muchas herramientas para blogs, pero para un desarrollador independiente la mayoría son excesivas.
- CMS: trae consigo panel de administración, integraciones y facturas
- Integración con Notion: cómoda, pero te topas con límites en renderizado, velocidad y personalización
- Archivos Markdown: los artículos se versionan con el código, es gratis, y
git pushequivale a publicar
Yo elegí la tercera opción. Es la que menos fricción tiene.
Estructura de carpetas
Si nos quedamos con lo esencial, se ve así.
src/
├── app/
│ ├── page.tsx # 홈 - 글 목록
│ ├── posts/[slug]/page.tsx # 글 상세
│ └── sitemap.ts # /sitemap.xml
├── content/posts/*.mdx # ← 글은 여기에만 추가
└── lib/posts.ts # 목록·메타 로딩
El nombre del archivo se convierte en la URL. hello.mdx se abre en /posts/hello.
Configuración de MDX
Para usar MDX con App Router se añade @next/mdx. Como Next 16 usa Turbopack por defecto, los plugins de remark/rehype hay que pasarlos como nombres en string (no se pueden pasar referencias a funciones).
const withMDX = createMDX({
options: {
remarkPlugins: ["remark-gfm"], // 표·취소선 지원
rehypePlugins: ["rehype-slug"], // 제목 앵커
},
})
meta export en vez de frontmatter
Normalmente el título y la fecha van en el bloque --- (frontmatter) al inicio del Markdown, pero como MDX puede contener JavaScript, hay una forma más limpia: simplemente exportar un objeto.
export const meta = {
title: "글 제목",
date: "2026-07-06",
category: "dev",
}
## 본문 시작
En la página de listado se hace un import dinámico de cada archivo y se lee solo ese meta.
const mod = await import(`@/content/posts/${slug}.mdx`)
return mod.meta // 본문 렌더 없이 메타만
No hacen falta librerías de parseo ni índices aparte.
SEO desde el primer día
Si el objetivo del blog es atraer visitas, el SEO no va al final sino al principio. Yo dejé listos desde el inicio los metadatos por artículo, Open Graph, datos estructurados JSON-LD, y también sitemap.xml, robots.txt y rss.xml. De esto hablaré con más detalle en el próximo artículo.
En resumen
- Cada artículo es un archivo
.mdx, y el nombre del archivo es la URL @next/mdx+ plugins como strings (Turbopack)export const metaen vez de frontmatter- SEO desde el primer commit
Con esta misma estructura puedes levantar tu blog en 30 minutos. Yo la uso para seguir acumulando historias de productos como La Ola del Día.
Preguntas frecuentes
¿Por qué usar archivos MDX en vez de un CMS?
Los artículos se versionan junto con el código, es gratis y desplegar equivale a publicar, así que es la opción con menos fricción para un desarrollador independiente. Además puedes insertar imágenes, código y componentes con total libertad.
¿Cómo se genera el listado sin frontmatter?
Exportas un objeto meta al inicio de cada .mdx y, en la página de listado, haces un import dinámico del archivo para leer solo ese meta. No necesitas ninguna librería de parseo adicional.
Artículos relacionados
- 💻 Desarrollo
Cuando un bucket tiene dos dueños, gana el último que hace apply
Añadí una sola regla de lifecycle en S3 y terraform apply falló. La causa: una estructura en la que dos recursos poseían cada uno por su cuenta la configuración de lifecycle del mismo bucket. El lifecycle de S3 no opera por reglas sino por documento entero, así que el último ganador borra en silencio las reglas del otro.
- 💻 Desarrollo
Los tests estaban en verde, pero la notificación nunca llegó a enviarse
El código que encolaba el job fallaba desde el primer día, siempre. El error se tragaba en un catch y los tests seguían en verde gracias a los mocks. La historia de cómo un solo carácter, dos puntos, mató en silencio tres funcionalidades.
- 💻 Desarrollo
Llegó una alerta diciendo que la base de datos estaba caída. La base de datos nunca se cayó
En un solo día se acumularon cuatro alertas CRITICAL. Un 504, un P2028 de Prisma, un 500 en el admin y 'servicio DATABASE caído'. Abrí primero las métricas de RDS y durante 21 horas seguidas la CPU no pasó del 19.7%, tan tranquila. Lo lento no era la base de datos, sino un viaje de ida y vuelta que cruzaba el Atlántico veinticinco veces, y la alerta de 'caída' se la había fabricado el propio health check.