Der erste Beitrag handelt vom Blog selbst. Kurzfassung: Es gibt
keine Datenbank, kein CMS und keinen Server, der zur Laufzeit etwas rendert. Ein Beitrag ist eine
Datei im Repository, und npm run build macht fertiges HTML daraus.
Warum Astro
Das Layout stammt aus einem Next.js-Theme. Für einen Blog, der aus Text und ein paar Bildern besteht, ist React zur Laufzeit aber viel Gepäck für wenig Nutzen. Astro rendert alles beim Bauen und liefert von sich aus kein JavaScript aus. Nur dort, wo ein Skript wirklich gebraucht wird (sticky Header, mobiles Menü, back-to-top), kommt ein kleines Inline-Skript dazu. Auf der Startseite sind das zusammen rund 2,4 KB.
Ein Beitrag ist eine Datei
Alle Beiträge liegen in einem Verzeichnis. Der Dateiname wird zur URL:
wie-dieser-blog-gebaut-ist.mdx ergibt /beitraege/wie-dieser-blog-gebaut-ist/. Oben steht das
Frontmatter, darunter Markdown. Und weil es MDX ist, bei Bedarf eigene Komponenten wie die
Hinweisbox weiter oben.
---
title: "Wie dieser Blog gebaut ist"
description: "Astro, MDX und ein Ordner voller Dateien."
pubDate: 2026-09-14
category: "Softwareentwicklung"
tags: ["Astro", "MDX"]
cover: "../../assets/blog/astro-setup.png"
---
import { Callout } from "../../components/content";
Normaler Markdown-Text.
<Callout type="tip">Und dazwischen eine Hinweisbox.</Callout>
Das Frontmatter ist keine lose Vereinbarung, sondern ein Schema. Astro nennt das Content Collections1 und prüft die Felder beim Build mit Zod. Ein Tippfehler im Feldnamen oder ein fehlendes Titelbild bricht den Build ab, statt still eine kaputte Seite zu erzeugen.
// src/content.config.ts
const blog = defineCollection({
loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
schema: ({ image }) =>
z.object({
title: z.string().max(120),
description: z.string().max(320),
pubDate: z.coerce.date(),
category: z.string(),
tags: z.array(z.string()).default([]),
cover: image(),
draft: z.boolean().default(false),
}),
});
Aus dem Frontmatter entsteht mehr als der Beitrag selbst. Jede Kategorie und jedes Schlagwort bekommt automatisch eine eigene Übersichtsseite, ohne dass irgendwo etwas registriert werden muss.
Was beim Bauen passiert
npm run build erzeugt den Ordner dist/, und der ist alles, was auf dem Server liegt.
| Was | Wie |
|---|---|
| Seiten | Eine index.html pro URL fertig gerendert |
| Bilder | Aus src/assets/ |
| Schrift | Wird selbst gehostet |
| Icons | Font-Awesome-CSS auf die genutzten Icons reduziert: 532 KB → 19 KB |
| Suchindex | Pagefind läuft nach dem Build über dist/ und legt den Index daneben |
| RSS, Sitemap | Aus denselben Daten wie die Seiten |
Zur Laufzeit passiert dann nichts mehr. Keine Cookies, kein localStorage, kein Analytics. Die Datenschutzerklärung ist daher entsprechend kurz.
Suche ohne Server
Die Volltextsuche läuft komplett im Browser. Pagefind2 zerlegt beim Build alle Beiträge in einen Index aus kleinen Fragmenten, von denen der Browser nur die lädt, die zum Suchbegriff passen. Indiziert wird nur, was im Beitragslayout entsprechend markiert ist.
Veröffentlichen
Ein Push auf den main-Branch reicht. GitHub Actions installiert die Abhängigkeiten, prüft Typen und
Frontmatter, baut die Seite und deployed das Artefakt als Azure Static Web App.
Weil die Seite statisch ist, entscheidet sich alles beim Bauen. Ein Beitrag mit pubDate
in der Zukunft wird nicht versteckt, sondern gar nicht erst gebaut und ist daher auch nirgens verfügbar.
// src/lib/posts.ts
export async function getPublishedPosts() {
const now = Date.now();
const posts = await getCollection(
"blog",
({ data }) => import.meta.env.DEV || (!data.draft && data.pubDate.valueOf() <= now),
);
return posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
}
Fazit
Ein Blog braucht keinen Server. Ein Ordner mit Markdown-Dateien, eine Build-Pipeline und ein
statisches Hosting reichen. Das Ergebnis ist schnell, günstig und wartungsarm. Wer einen Beitrag
schreiben will, braucht nur einen Editor und git push.
