Skip to content
kobiakov.dev
Zurück zum Blog

Ein wartungsfreier Blog mit MDX und Next.js 15

Wie man einen git-basierten Blog mit MDX, gray-matter und dem Next.js App Router baut — ganz ohne Datenbank.

Vladyslav Kobiakov2 Min. Lesezeit
Ein wartungsfreier Blog mit MDX und Next.js 15

Moderne Portfolio-Seiten überkonstruieren ihren Blog oft. Man richtet ein CMS ein, konfiguriert Webhooks, stellt eine Datenbank bereit — und verbringt mehr Zeit mit Infrastruktur als mit dem Schreiben. MDX mit dem Next.js App Router macht all das überflüssig: Ihre Inhalte liegen im Repository, versioniert neben Ihrem Code, mit null Laufzeit-Abhängigkeiten.

Warum MDX statt Headless-CMS

Ein Headless-CMS wie Sanity oder Contentful ist die richtige Wahl, wenn ein nicht-technisches Team Inhalte veröffentlichen muss. Für den persönlichen Blog eines Entwicklers bringt es unnötige Kopplung mit sich:

  • Cold-Start-Latenz — jeder Seitenaufbau ruft eine externe API auf
  • Verfügbarkeits-Abhängigkeit — fällt Ihr CMS aus, fällt Ihr Blog mit
  • Kontextwechsel — Schreiben in einem Browser-Editor statt in Ihrer IDE

MDX dreht das um. Ein Inhalt ist eine .mdx-Datei. Das Deployment ist ein git push. Es gibt kein CMS zu warten.

Projektstruktur

content/
  blog/
    nextjs-15-architecture.mdx
    trading-bot-design.mdx
lib/
  mdx.ts          ← Frontmatter + Inhalt parsen
app/[locale]/blog/
  page.tsx         ← Übersicht
  [slug]/page.tsx  ← Detail

MDX-Dateien mit gray-matter parsen

Das Herzstück des Systems ist eine einzige Utility, die .mdx-Dateien von der Festplatte liest und auf Ihr Datenmodell abbildet:

import fs from 'fs'; import path from 'path'; import matter from 'gray-matter'; const BLOG_DIR = path.join(process.cwd(), 'content/blog'); export async function getMdxPosts() { const files = fs.readdirSync(BLOG_DIR).filter(f => f.endsWith('.mdx')); return files .map(file => { const raw = fs.readFileSync(path.join(BLOG_DIR, file), 'utf8'); const { data, content } = matter(raw); const slug = file.replace(/\.mdx$/, ''); return { slug: data.slug ?? slug, title: data.title ?? 'Untitled', excerpt: data.excerpt ?? '', content, coverImage: data.coverImage ?? null, date: new Date(data.date ?? Date.now()), author: data.author ?? 'Admin', tags: data.tags ?? [], published: data.published ?? true, }; }) .filter(p => p.published) .sort((a, b) => b.date.getTime() - a.date.getTime()); }
ts

gray-matter trennt das YAML-Frontmatter vom Textkörper. Der Rest ist reines TypeScript — in dieser Phase wird keine spezielle MDX-Laufzeit benötigt.

MDX im App Router rendern

Für die Detailseite empfiehlt Next.js @next/mdx oder das leichtere next-mdx-remote. Da wir Dateien zur Laufzeit laden (nicht statisch importieren), passt next-mdx-remote besser:

// app/[locale]/blog/[slug]/page.tsx import { MDXRemote } from 'next-mdx-remote/rsc'; import { getMdxPostBySlug } from '@/lib/mdx'; import { notFound } from 'next/navigation'; export default async function BlogPostPage({ params, }: { params: Promise<{ slug: string; locale: string }>; }) { const { slug } = await params; const post = await getMdxPostBySlug(slug); if (!post) notFound(); return ( <article className="prose prose-invert max-w-3xl mx-auto py-16 px-6"> <h1>{post.title}</h1> <MDXRemote source={post.content} /> </article> ); }
ts

MDXRemote aus dem /rsc-Export ist eine React Server Component — es kompiliert und rendert MDX auf dem Server, mit null Client-JavaScript.

Frontmatter-Schema

Eine konsistente Frontmatter-Struktur verhindert Sonderfälle auf der Übersichtsseite:

--- title: "Post-Titel" description: "Ein-Satz-Zusammenfassung für die SEO-Meta-Description." date: "2026-05-01" author: "Vladyslav Kobiakov" tags: ["nextjs", "performance"] coverImage: "/projects/blog/mdx-blog.webp" published: true ---
yaml

Lassen Sie bei Entwürfen published: false — der getMdxPosts-Filter schließt sie aus, ohne das Routing zu berühren.

Statische Generierung mit generateStaticParams

Für maximale Performance rendern Sie alle Beiträge zur Build-Zeit vor:

export async function generateStaticParams() { const posts = await getMdxPosts(); return posts.map(post => ({ slug: post.slug })); } export const revalidate = false; // vollständig statisch
ts

Zusammen mit next/image für die Titelbilder und einem CDN vor Vercel sinkt die Time to First Byte auf einstellige Millisekunden.

Eigene MDX-Komponenten hinzufügen

Einer der größten Vorteile von MDX ist das Einbetten von React-Komponenten in den Fließtext. Bilden Sie Komponentennamen an einer Stelle auf Implementierungen ab:

const components = { CodeBlock: ({ code, lang }: { code: string; lang: string }) => ( <SyntaxHighlighter language={lang}>{code}</SyntaxHighlighter> ), Callout: ({ children }: { children: React.ReactNode }) => ( <div className="border-l-4 border-primary pl-4 my-6 text-muted-foreground"> {children} </div> ), }; // An MDXRemote übergeben <MDXRemote source={post.content} components={components} />
ts

Jetzt kann jede .mdx-Datei <Callout> oder <CodeBlock> inline verwenden.

Abwägungen

Was Sie gewinnen:

  • Keine Datenbank, keine CMS-API, keine Webhooks
  • Inhalte in git versioniert — Entwürfe sind Branches, Reviews sind PRs
  • Volles IDE-Tooling: Autovervollständigung, Rechtschreibprüfung, Refactoring

Worauf Sie verzichten:

  • Kein visueller Editor für nicht-technische Mitarbeitende
  • Keine integrierte Medienverwaltung (nutzen Sie Cloudinary oder public/)
  • Erneutes Deployment zum Veröffentlichen nötig (für Solo-Portfolios akzeptabel)

Fazit

Für ein Entwickler-Portfolio ist ein MDX-basierter Blog genau das richtige Maß an Komplexität. Das Setup passt in zwei Dateien, skaliert ohne Datenbank auf Hunderte Beiträge und hält das Schreiberlebnis in den Werkzeugen, die Sie ohnehin nutzen. Der einzige Wartungsaufwand ist ein gelegentliches npm update für gray-matter und next-mdx-remote.

Share: