Contexto
Para el portfolio necesitaba paginas estaticas generadas desde archivos .mdx en disco -- sin base de datos, sin CMS, sin API calls en runtime.
Lo que aprendi
El patron combina tres piezas: gray-matter para parsear frontmatter YAML, fs.readdirSync para listar archivos, y generateStaticParams para decirle a Next.js que rutas generar en build time.
1. Content loader (lib/content.ts)
import fs from "fs";
import path from "path";
import matter from "gray-matter";
const contentDir = path.join(process.cwd(), "content");
export type TilEntry = {
title: string;
slug: string;
category: string;
stack: string[];
date: string;
body: string;
};
export function getAllTils(): TilEntry[] {
const tilDir = path.join(contentDir, "til");
if (!fs.existsSync(tilDir)) return [];
const files = fs
.readdirSync(tilDir)
.filter((f) => f.endsWith(".mdx") || f.endsWith(".md"));
return files.map((file) => {
const raw = fs.readFileSync(path.join(tilDir, file), "utf-8");
const { data, content } = matter(raw);
const slug = file.replace(/\.mdx?$/, "");
return {
title: data.title ?? slug,
slug: data.slug ?? slug,
category: data.category ?? "general",
stack: data.stack ?? [],
date: data.date ?? "",
body: content,
} satisfies TilEntry;
});
}
2. Static params (app/til/[slug]/page.tsx)
export async function generateStaticParams() {
const tils = getAllTils();
return tils.map((t) => ({ slug: t.slug }));
}
export default async function TilPage({ params }: Props) {
const { slug } = await params;
const til = getTilBySlug(slug);
if (!til) notFound();
return (
<article className="prose max-w-none">
<MdxContent source={til.body} />
</article>
);
}
Resultado
- Build time: Next.js lee todos los
.mdx, genera HTML estatico para cada slug - Runtime: Zero filesystem calls, zero DB calls -- es HTML puro servido desde CDN
- Agregar contenido: crear un
.mdxcon frontmatter, push a git, Vercel auto-deploya satisfies: TypeScript valida que el objeto cumple el tipo sin perder la inferencia