Auf dieser Seite
- Das große Bild
- Toolchain: Bun statt npm
- Astro: Standardmäßig statisch
- JavaScript-Auslieferung
- Ladeleistung und Core Web Vitals
- Visuelles Design
- Content Collections
- Routing und Seiten
- Internationalisierung
- Bildbehandlung
- Optimierung und responsive Varianten
- Automatische Dimensionsinjektion
- SEO und Auffindbarkeit
- HTML-Head
- Sitemap
- RSS-Feeds
- robots.txt
- llms.txt
- Deployment: GitHub + Cloudflare Pages
- CI: GitHub Actions
- HTTP-Header
- Weiterleitungen
- Kommentare: Giscus
- Das vollständige Bild
- Welche Rolle spielt die KI dabei?
- Deine Erfahrung
Wie diese Website funktioniert: Ein technischer Blick auf den Stack

In einem Begleitartikel habe ich darüber geschrieben, warum ich diese Website mit Astro neu gebaut habe und wie diese Entscheidung zu einem viel größeren Projekt wurde. Jener Artikel handelte von der Reise und den Überlegungen. Dieser hier handelt vom Ergebnis: Wie die Website tatsächlich funktioniert, von der Schicht, auf der ein Beitrag geschrieben wird, bis zu dem Moment, in dem er im Browser eines Lesers erscheint.
Das große Bild
Im Kern ist die Website eine statische Website. Jede Seite wird zur Build-Zeit generiert und als reines HTML, CSS und minimales JavaScript ausgeliefert. Es gibt keine Server, die Anfragen rendern, keine Datenbank und keine Laufzeitinfrastruktur, die gewartet werden muss. Was Cloudflare an Leser ausliefert, ist genau das, was Astro während des Builds produziert hat.
┌──────────────────────────────────────────────────────────────┐
│ Autor │
│ Markdown/MDX schreiben → git push → GitHub │
└───────────────────┬────────────────────────┬─────────────────┘
│ PR (parallel) │ merge to main
▼ ▼
┌────────────────────┐ ┌────────────────────────────────────┐
│ GitHub Actions │ │ Cloudflare Pages │
│ nur Build-Check │ │ build → deploy ins CDN │
│ (kein Deploy) │ │ PR → Vorschau bei *.workers.dev│
└────────────────────┘ └──────────────┬─────────────────────┘
│
▼
Browser des Lesers
(HTML + CSS + ~0 JS pro Seite)
Diese Einfachheit ist gewollt. Statische Websites sind schnell, günstig und leicht zu verstehen. Die Toolchain darum herum kann trotzdem interessant sein — und genau das erkundet dieser Artikel.
Toolchain: Bun statt npm
Das Projekt setzt Bun >=1.0.0 als JavaScript-Laufzeitumgebung und Paketmanager voraus. Bun hat npm während der Migration ersetzt, und der Unterschied im täglichen Einsatz ist spürbar. Die Installation ist schneller, die Lockfile (bun.lock) ist kompakter, und die Befehle fühlen sich bei einem Projekt dieser Größe flotter an.
Die package.json-Skripte sind unkompliziert:
{
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"deploy": "bun run build && wrangler pages deploy dist/ --project-name=website"
}
}
bun run dev startet einen lokalen Entwicklungsserver mit Hot Module Reloading. bun run build erzeugt die statische Ausgabe in dist/. Das deploy-Skript ist eine lokale Notlösung: In der Praxis erfolgen Produktions-Deploys automatisch über die Git-Integration von Cloudflare Pages, nicht manuell.
Astro: Standardmäßig statisch
Astro ist das Framework, das Inhalte und Komponenten in die Seiten umwandelt, die an Leser ausgeliefert werden. Die Konfiguration spiegelt einen klaren Satz von Prioritäten wider:
// astro.config.mjs
export default defineConfig({
site: "https://ammar-najjar.com",
trailingSlash: "always",
output: "static",
markdown: {
shikiConfig: { theme: "css-variables" },
rehypePlugins: [rehypeImgDimensions],
},
integrations: [
mdx(),
sitemap({
filter: page => !page.includes(".xml") && !page.includes("/404/"),
}),
],
});
output: 'static' bedeutet, dass der Build reine HTML-Dateien erzeugt. Die Website läuft vollständig als statische Assets auf Cloudflare Pages, ohne dass ein Server-Adapter benötigt wird.
JavaScript-Auslieferung
Eines der Kernprinzipien von Astro ist es, standardmäßig kein JavaScript auszuliefern. Jede .astro-Komponente rendert zur Build-Zeit zu statischem HTML und sendet nichts an den Browser, es sei denn, man entscheidet sich explizit mit einer client:*-Direktive dafür. Diese Website verwendet keine dieser Direktiven. Jede Komponente ist rein serverseitig gerendertes HTML.
Das einzige JavaScript, das tatsächlich an Leser ausgeliefert wird, ist:
- Ein kleines Inline-Skript im
<head>, daslocalStorageliest unddata-themevor dem ersten Paint setzt, um einen falschen Theme-Blitz zu verhindern - Ein Skript, das den Dunkel-Modus-Schalter und den Locale-Wechsler verdrahtet
- Das Giscus-Kommentar-Widget, das nur dann lazy geladen wird, wenn ein Leser ans Ende eines Beitrags scrollt
Das war’s. Kein Framework-Laufzeit, kein Hydration-Overhead, kein Komponenten-Baum, der im Browser bootstrappt. Die vorherige Website (Docusaurus, auf React aufgebaut) lieferte auf jeder Seite eine vollständige React-Laufzeit und ein Komponenten-Bundle aus, unabhängig davon, ob Interaktivität benötigt wurde. Der Unterschied in der Bundle-Größe ist erheblich: Die meisten Seiten dieser Website liefern unter 10 KB JavaScript aus, verglichen mit dem 100-200 KB React-Bundle, das zuvor bedingungslos geladen wurde.
Ladeleistung und Core Web Vitals
Core Web Vitals messen drei Dinge, die Leser tatsächlich spüren: wie schnell das größte sichtbare Element lädt (LCP), wie stark sich das Layout beim Laden verschiebt (CLS) und wie schnell die Seite auf Interaktionen reagiert (INP). Jedes davon wird direkt von Architekturentscheidungen beeinflusst, die an anderer Stelle in diesem Stack getroffen werden.

Dieses Ergebnis bestätigt die Auswirkung der Architekturentscheidungen: minimaler JavaScript-Transfer, keine render-blockierenden Ressourcen und optimierte Bilder sorgen dafür, dass jede Seite nahezu augenblicklich geladen wird.
- LCP — das Hero-Bild eines Beitrags.
BaseLayoutakzeptiert einepreloadImage-Prop, die<link rel="preload" fetchpriority="high">ausgibt und dem Browser mitteilt, es vor dem Parsen des Seitentexts mit höchster Priorität zu laden. Cloudflares CDN liefert es von einem Edge-Knoten nahe beim Leser aus. - CLS — durch drei Dinge verhindert:
width- undheight-Attribute, die zur Build-Zeit injiziert werden (der Browser reserviert Platz, bevor das Bild geladen wird), Systemschriften (kein Web-Font-Tausch) und kein clientseitiges Rendering, das Elemente nach dem initialen Paint einfügt. - INP — da kein JavaScript-Framework läuft, gibt es keine Event-Loop-Konkurrenz durch Komponenten-Reconciliation oder Hydration. Die einzigen vorhandenen Skripte sind klein und fokussiert.
Syntax-Highlighting verwendet Shiki mit einem css-variables-Theme. Anstatt ein festes Farbschema in die Ausgabe einzubetten, werden alle Token-Farben als CSS-Custom-Properties ausgedrückt. Dadurch ist das Wechseln zwischen hellem und dunklem Modus eine reine CSS-Angelegenheit: Die Variablenwerte ändern, und die Code-Blöcke folgen ohne JavaScript.
Visuelles Design
Die visuelle Schicht ist reines CSS, kein Framework. Alle Farben sind als CSS-Custom-Properties auf :root definiert, mit einem [data-theme='dark']-Override-Block, der alle Variablen tauscht, wenn der Benutzer den Dunkelmodus aktiviert:
:root {
--color-bg: #ffffff;
--color-text: #1a1a2e;
--color-primary: #003366;
--font-sans: system-ui, -apple-system, sans-serif;
--font-mono: "SF Mono", "Fira Code", monospace;
--max-width: 900px;
}
[data-theme="dark"] {
--color-bg: #0d1117;
--color-text: #c9d1d9;
--color-primary: #3399ff;
}
Der Schalter liest localStorage beim Seitenaufruf, fällt auf prefers-color-scheme zurück und schreibt das data-theme-Attribut auf <html>:
const saved = localStorage.getItem("theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (saved === "dark" || (!saved && prefersDark))
document.documentElement.setAttribute("data-theme", "dark");
Das ist das einzige JavaScript, das beim Theming involviert ist. Typografie verwendet durchgehend Systemschriften: keine heruntergeladenen Web-Fonts, keine render-blockierenden Anfragen, kein Layout-Shift. Das Layout ist auf --max-width: 900px begrenzt und passt sich über Media Queries kleineren Bildschirmen an. Bilder werden global auf max-width: 100%; height: auto gesetzt, sodass jedes Bild ohne individuelle Behandlung responsive ist.
Content Collections
Astros Content Collections sind das Rückgrat der Inhaltsschicht. Jeder Beitrag lebt in src/content/ als Markdown- oder MDX-Datei, und jede Collection wird durch ein Zod-Schema beschrieben, das Frontmatter zur Build-Zeit validiert:
// src/content.config.ts
const blogSchema = z.object({
slug: z.string(),
title: z.string(),
authors: z.string(),
date: z.coerce.date(),
tags: z.array(z.string()).default([]),
description: z.string().optional(),
image: z.string().optional(),
imageStyle: z.string().optional(),
draft: z.boolean().default(false),
});
Wenn das Frontmatter eines Beitrags ein Pflichtfeld fehlt oder den falschen Typ hat, schlägt der Build fehl. Das ist gewollt: Ein fehlgeschlagener Build ist besser als ein veröffentlichter Beitrag mit fehlerhaften Metadaten.
Die Inhaltsverzeichnisse entsprechen direkt den Locale-Varianten:
src/content/
blog/ ← Englische Beiträge
blog-de/ ← Deutsche Beiträge
blog-ar/ ← Arabische Beiträge
books/ ← Englische Lesenotizen
books-de/ ← Deutsche Lesenotizen
books-ar/ ← Arabische Lesenotizen
authors/ ← YAML-Datendateien der Autoren
Die Inhalte jeder Locale bilden ihre eigene Collection, die unter einem anderen Namen registriert ist, aber alle dasselbe Schema teilen. Der collectionName()-Helfer in src/i18n/collections.ts handhabt die Namenskonvention:
export function collectionName(type: ContentType, locale: Locale): string {
if (locale === "en") return type;
return `${type}-${locale}`;
}
So gibt collectionName('blog', 'ar') 'blog-ar' zurück, und die Seitenschicht fragt die richtige Collection basierend darauf ab, für welche Locale sie gerade rendert.
Die Felder tags und authors des Schemas treiben die Entdeckungsschicht an.
Tags haben zwei Zwecke. Für Leser sind sie Navigation: Ein Leser, der bei einem Beitrag über Cloudflare landet und mehr zu diesem Thema möchte, kann dem Tag zu einer dedizierten Listenseite unter /blog/tags/cloudflare/ folgen, die alles mit demselben Tag zeigt. Für Suchmaschinen sind Tag-Seiten themenspezifische URLs mit eigenen kanonischen Adressen, die Relevanzsignale um ein Thema bündeln, anstatt sie über unzusammenhängende Beiträge zu verteilen. Jeder in einem Beitrag verwendete Tag bekommt automatisch zur Build-Zeit über getStaticPaths() seine eigene Seite — keine manuelle Pflege nötig.
Das Feld authors verknüpft Beiträge mit Autorenprofilen unter /authors/:author/. Paginierung unter /blog/page/:n/ verhindert, dass der Blog-Index mit wachsendem Archiv unbegrenzt wächst. Zusammen bieten diese Leserinnen und Lesern mehrere Einstiegspunkte in den Inhalt jenseits eines einzigen chronologischen Feeds.
Routing und Seiten
Astros dateibasiertes Routing übersetzt die Verzeichnisstruktur direkt in URLs. Der src/pages/-Baum spiegelt die URL-Struktur der Website wider:
src/pages/
index.astro → /
about.astro → /about/
blog/[...slug].astro → /blog/:slug/
blog/tags/[tag].astro → /blog/tags/:tag/
blog/page/[page].astro → /blog/page/:n/
books/[...slug].astro → /books/:slug/
authors/[author].astro → /authors/:author/
rss.xml.js → /rss.xml
ar/ → /ar/* (Arabische Locale-Spiegelung)
de/ → /de/* (Deutsche Locale-Spiegelung)
Dynamische Routen wie [...slug].astro verwenden getStaticPaths(), um zur Build-Zeit jeden Beitrag aufzuzählen:
export async function getStaticPaths() {
const posts = await getPublishedCollection("blog");
return posts.map(post => ({
params: { slug: post.id },
props: { slug: post.id, id: post.id },
}));
}
Zur Build-Zeit ruft Astro getStaticPaths() für jede dynamische Seite auf, sammelt alle Parameterkombinationen und rendert jede zu einer statischen HTML-Datei. Das Ergebnis ist ein dist/-Verzeichnis, das genau der URL-Struktur der Live-Website entspricht.
Internationalisierung
Die Website unterstützt drei Locales: Englisch (Standard), Deutsch (/de/) und Arabisch (/ar/). Die Implementierung ist explizit statt magisch: keine automatische Locale-Erkennung, keine serverseitige Weiterleitung basierend auf Browser-Headern. Jede Locale ist ein paralleles Verzeichnis von Seiten, das seine eigene Collection abfragt.
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ /blog/ │ │ /de/blog/ │ │ /ar/blog/ │
│ (Englisch) │ │ (Deutsch) │ │ (Arabisch) │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
▼ ▼ ▼
blog collection blog-de collection blog-ar collection
Ein Sprachumschalter in der Navigation ermöglicht es Lesern, die Locale manuell zu wechseln. BaseLayout akzeptiert eine dir-Prop ('ltr' oder 'rtl'), sodass die arabische Locale korrekte Textrichtung und Layout-Spiegelung auf HTML-Ebene erhält — ohne clientseitiges JavaScript.
Locale-Helfer sorgen für konsistente URL-Konstruktion im gesamten Codebase:
export function localizedPrefix(locale: Locale): string {
return locale === "en" ? "" : `/${locale}`;
}
export function localizedUrl(path: string, locale: Locale): string {
const prefix = localizedPrefix(locale);
return prefix === "" && path.startsWith("/") ? path : `${prefix}${path}`;
}
Englische Pfade bleiben unverändert; deutsche und arabische Pfade erhalten ihr Locale-Präfix.
Bildbehandlung
Bilder befinden sich an zwei Orten. Inhaltsbilder (Beitrags-Header, Diagramme, Screenshots) sind zusammen mit ihrem Beitrag in src/content/ abgelegt, was bedeutet, dass jedes Beitragsverzeichnis in sich geschlossen ist:
src/content/blog/every-camera-tells-a-story/
index.md
header.webp
Statische Bilder (Profilfoto, Social-Icons, Favicons) befinden sich in public/img/ und werden unverändert in die Ausgabe kopiert.
Optimierung und responsive Varianten
Inhaltsbilder, die in Komponenten referenziert werden, verwenden Astros eingebaute <Image>-Komponente aus astro:assets. Zur Build-Zeit optimiert sie die Quelldatei automatisch, konvertiert sie in ein modernes Format (standardmäßig WebP) und generiert ein srcset mit mehreren Größenvarianten, sodass der Browser nur die Auflösung herunterlädt, die er tatsächlich benötigt:
import { Image } from 'astro:assets';
<Image
src={headerImage}
alt={title}
width={120}
loading="lazy"
/>
Die Komponente gibt auch width- und height-Attribute auf dem ausgegebenen <img> aus, die der Browser verwendet, um Platz zu reservieren, bevor das Bild geladen wird — was Layout-Shift eliminiert.
Automatische Dimensionsinjektion
Für Bilder, die direkt in Markdown geschrieben werden (statt über eine Komponente), übernimmt ein benutzerdefiniertes Rehype-Plugin zur Build-Zeit die Dimensionsinjektion. Es liest width und height aus der tatsächlichen Datei mit Sharp und injiziert sie auf jedes <img>-Element, das auf eine Datei in public/img/ verweist:
// src/plugins/rehype-img-dimensions.mjs
async function getDimensions(src) {
if (!src || !src.startsWith("/img/")) return null;
const filePath = join(projectRoot, "public", src);
const { width, height } = await sharp(filePath).metadata();
return width && height ? { width, height } : null;
}
Das Plugin verarbeitet sowohl die Standard-Markdown-Bildsyntax () als auch inline geschriebene <img>-HTML-Tags. Das Ergebnis ist dasselbe wie bei der <Image>-Komponente: Der Browser kennt die Dimensionen, bevor das Bild ankommt, und Layout-Shift wird ohne manuelles Attributmanagement eliminiert.
SEO und Auffindbarkeit
Mehrere Systeme arbeiten zusammen, um Inhalte auffindbar zu machen.
HTML-Head
BaseLayout.astro ist die einzige Layout-Komponente, die jede Seite umschließt. Sie verwaltet den <head> mit einem konsistenten Satz von Tags:
<title>{fullTitle}</title>
<meta name="description" content="{description}" />
<meta property="og:title" content="{fullTitle}" />
<meta property="og:image" content="{image}" />
<link rel="canonical" href="https://ammar-najjar.com{pathname}" />
<link rel="alternate" hreflang="{locale}" href="..." />
<link rel="alternate" type="application/rss+xml" href="/rss.xml" />
Eine einzige Layout-Komponente, die all das handhabt, gewährleistet Konsistenz: Kein Beitrag wird ohne Titel ausgeliefert, keine Seite fehlt ihre kanonische URL.
Titel und Beschreibung erscheinen direkt in Suchergebnissen und sozialen Link-Vorschauen. Open-Graph-Tags steuern, wie ein geteilter Link auf sozialen Plattformen aussieht; ohne sie raten Plattformen — oft schlecht. Kanonische URL ist ein autoritatives Signal an Suchmaschinen über die maßgebliche Adresse jeder Seite, was Mehrdeutigkeit durch abschließende Schrägstriche oder Weiterleitungen verhindert. hreflang teilt Suchmaschinen mit, welche Locale für welches Publikum angezeigt werden soll; ohne es könnte ein deutschsprachiger Leser auf der englischen Version landen, auch wenn die deutsche Version existiert. RSS alternate ist auf jeder Seite enthalten, damit Feed-Reader den Abonnement-Link entdecken können, ohne dass Leser danach suchen müssen.
Sitemap
Eine Sitemap übergibt Crawlern eine vollständige Liste aller URLs der Website, anstatt sich darauf zu verlassen, dass sie Links folgen. Für eine mehrsprachige Website ist das wichtiger als für einen einfachen Blog: Die englischen, deutschen und arabischen Versionen jedes Beitrags befinden sich unter separaten URLs, und ein Crawler, der nur internen Links folgt, erreicht möglicherweise nicht alle.
Die Integration @astrojs/sitemap generiert zur Build-Zeit sitemap-index.xml aus jeder statischen Seite und filtert 404-Seiten und XML-URLs heraus. Sie wird in robots.txt und im Link-Response-Header auf jeder Seite referenziert, sodass Crawler sie unabhängig von ihrer Ankunftsweise finden.
Eine Weiterleitung handhabt den konventionellen Pfad:
/sitemap.xml → /sitemap-index.xml (301)
RSS-Feeds
Die Website veröffentlicht zwei RSS-Feeds:
/rss.xml: Blogbeiträge, nach Datum absteigend sortiert/books/rss.xml: Lesenotizen, nach Datum absteigend sortiert
Jeder wird von einem Astro-Endpunkt (rss.xml.js) generiert, der die veröffentlichte Collection abfragt und mit @astrojs/rss einen korrekt formatierten Feed zurückgibt.
robots.txt
User-agent: *
Allow: /
Content-Signal: ai-train=no, search=yes, ai-input=yes
Sitemap: https://ammar-najjar.com/sitemap-index.xml
robots.txt ist eine der ältesten Konventionen im Web und das Erste, was jeder Crawler prüft. Die Datei öffnet die gesamte Website für alle Crawler, verweist auf die Sitemap und fügt eine Content-Signal-Direktive hinzu. Dieser letzte Teil ist eine vorgeschlagene Erweiterung zur Darstellung von KI-Lizenzabsichten: ai-train=no bittet darum, dass die Inhalte nicht zum Training von Modellen verwendet werden; ai-input=yes erlaubt die Verwendung in Retrieval-Augmented Generation, wo ein Modell eine spezifische Frage beantwortet, anstatt Inhalte in seine Gewichte aufzunehmen. Rechtlich nicht durchsetzbar, aber es gibt die Absicht klar an einem Ort an, den Crawler ohnehin besuchen.
llms.txt
robots.txt teilt Crawlern mit, auf was sie zugreifen dürfen. llms.txt teilt Sprachmodellen mit, was die Website tatsächlich enthält.
Wenn ein KI-Assistent nach einer Person oder einer Website gefragt wird, kann er entweder etwas aus den Trainingsdaten abrufen (eingefroren in der Zeit, möglicherweise ungenau) oder die Website crawlen (langsam, teuer). llms.txt bietet eine dritte Option: eine strukturierte Klartextzusammenfassung unter einem bekannten Pfad, die die Identität des Autors, Inhaltskategorien und verfügbare Sprachen abdeckt. llms-full.txt ist die erweiterte Variante, nach Thema geordnet mit Beschreibungen einzelner Beiträge. Ein Modell, das zuerst eine der beiden Dateien liest, kann genau antworten, ohne jede Seite abzurufen. Die Konvention entwickelt sich noch, aber das Problem, das sie löst, ist real.
Deployment: GitHub + Cloudflare Pages
Der Veröffentlichungsworkflow verbindet drei Komponenten: einen lokalen Texteditor, GitHub als Versionskontrolle und Cloudflare Pages als Build- und Auslieferungsplattform. Die Geschichte darüber, wie und warum das Hosting gewechselt ist, einschließlich des Domain-Registrar-Vergleichs und der Stolpersteine bei der Migration, wird in einem separaten Artikel behandelt.
┌───────────────┐ git push ┌──────────────────────────────┐
│ Lokal │ ────────────────► │ GitHub │
│ (schreiben + │ │ main-Branch → Produktion │
│ committen) │ │ PR-Branch → Vorschau-Build │
└───────────────┘ └───────┬──────────────┬───────┘
│ PR │ merge to main
▼ ▼
┌────────────────────────────────────────────────────┐
│ Cloudflare Pages │
│ bun install + bun run build │
│ │
│ PR → Vorschau bei *.workers.dev │
│ (Actions führt Build-Check parallel aus) │
│ main → dist/ ins Produktions-CDN deployen │
└────────────────────────────────────────────────────┘
Cloudflare Pages überwacht das GitHub-Repository. Ein Push auf main löst einen Produktions-Build und -Deployment aus. Das Öffnen eines Pull Requests löst einen Vorschau-Build aus: ein separates Deployment unter einer eindeutigen URL, sodass Änderungen visuell überprüft werden können, bevor sie zusammengeführt werden.
CI: GitHub Actions
Es gibt einen einzigen GitHub-Actions-Workflow, test-deploy.yml, der bei jedem Pull Request gegen main ausgeführt wird:
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- run: bun install --frozen-lockfile
- run: bun run build
Seine einzige Aufgabe ist es, zu überprüfen, ob astro build erfolgreich ist. Es wird nichts deployed. Wenn der Build bei einem PR fehlschlägt, wird der PR blockiert, bevor ein fehlerhaftes Deployment Cloudflare Pages erreichen kann. Wenn der Build erfolgreich ist, übernimmt Cloudflare Pages das eigentliche Deployment als separaten Schritt, der durch seine eigene Git-Integration ausgelöst wird. Jeder Branch erhält auch automatisch ein Live-Vorschau-Deployment auf einer *.workers.dev-Subdomain, sodass Änderungen in einem echten Browser unter einer teilbaren URL überprüft werden können, bevor sie zusammengeführt werden.
HTTP-Header
Cloudflare liest public/_headers, um Response-Header auf jede deployed URL anzuwenden. Die Strategie spiegelt die standardmäßige Cache-Praxis für statische Websites wider:
/_astro/*
Cache-Control: public, max-age=31536000, immutable
/img/*
Cache-Control: public, max-age=31536000, immutable
/*
Cache-Control: public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
Astro gibt gehashte Dateinamen für alle gebündelten Assets aus (z. B. /_astro/main.Bx9qKt2a.css). Da sich der Dateiname ändert, wenn sich der Inhalt ändert, können diese Dateien unbegrenzt gecacht werden (max-age=31536000, immutable) ohne das Risiko, dass Leser veraltetes CSS oder JavaScript sehen. HTML-Seiten, die keinen Inhalts-Hash haben, verwenden ein kürzeres max-age von einer Stunde mit einem stale-while-revalidate-Fenster, sodass Cloudflares CDN leicht veraltete Seiten ausliefern kann, während es im Hintergrund revalidiert.
Sicherheits-Header werden global angewendet: X-Content-Type-Options verhindert MIME-Sniffing, X-Frame-Options: DENY blockiert die Einbettung der Website in iFrames, und Permissions-Policy deaktiviert explizit den Zugriff auf Geolocation, Mikrofon und Kamera.
Weiterleitungen
public/_redirects handhabt permanente Weiterleitungen für URLs, die sich während der Migration geändert haben. Die alte Website hatte separate /experience/- und /skills/-Seiten; diese wurden entfernt und ihr Inhalt in /about/ zusammengeführt. Die Weiterleitungen erhalten alle eingehenden Links und übergeben Suchmaschinen-Signale an die neuen URLs:
/sitemap.xml /sitemap-index.xml 301
/blog/archive/ /blog/ 301
/experience/ /about/ 301
/ar/experience/ /ar/about/ 301
/de/experience/ /de/about/ 301
/skills/ /about/ 301
/ar/skills/ /ar/about/ 301
/de/skills/ /de/about/ 301
Diese stellen sicher, dass Links von anderen Websites, die auf alte URLs zeigen, nach der Umstrukturierung weiterhin korrekt aufgelöst werden, und dass alle Suchmaschinen-Index-Einträge für alte Pfade ihre Signale an die neuen weitergeben.
Kommentare: Giscus
Kommentare werden von Giscus betrieben, das Diskussionen im Discussions-Feature eines GitHub-Repositories speichert. Es ist keine separate Kommentardatenbank oder Drittanbieter-Plattform involviert. Leser benötigen ein GitHub-Konto zum Kommentieren, was zum technischen Publikum passt, das diese Website tendenziell anzieht.
Die GiscusComments-Komponente lädt das Giscus-Skript lazy mit einem IntersectionObserver:
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
loadGiscus();
observer.disconnect();
}
},
{ rootMargin: "200px" },
);
Das Kommentar-Widget wird nur initialisiert, wenn der Leser innerhalb von 200 px des Seitenanfangs scrollt. Dadurch wird das Laden eines externen Skripts bei jedem Seitenaufruf vermieden. Nur Leser, die tatsächlich das Ende eines Artikels erreichen, lösen die Netzwerkanfrage aus.
Das vollständige Bild
┌─────────────────────────────────────────────────────────────────┐
│ Schreiben │
│ src/content/blog/<slug>/index.md (Markdown + Frontmatter) │
│ src/content/blog-de/<slug>/index.md (Deutsch) │
│ src/content/blog-ar/<slug>/index.md (Arabisch) │
└──────────────────────────┬──────────────────────────────────────┘
│ durch Zod-Schema zur Build-Zeit validiert
▼
┌─────────────────────────────────────────────────────────────────┐
│ Build (bun run build) │
│ ├─ Astro sammelt alle Content-Collection-Einträge │
│ ├─ getStaticPaths() generiert einen Pfad pro Beitrag × Locale │
│ ├─ <Image> optimiert + generiert srcset für Inhaltsbilder │
│ ├─ Rehype-Plugin injiziert img-Breite/Höhe via Sharp │
│ ├─ Shiki hebt Code hervor (CSS-Variablen, kein JS) │
│ ├─ @astrojs/sitemap schreibt sitemap-index.xml │
│ └─ dist/ enthält flache HTML-Dateien passend zur URL-Struktur │
└──────────────────────────┬──────────────────────────────────────┘
│ git push to main
▼
┌─────────────────────────────────────────────────────────────────┐
│ Cloudflare Pages │
│ ├─ Führt denselben Build aus (bun install + bun run build) │
│ ├─ Deployt dist/ ins globale CDN-Edge │
│ ├─ Wendet _headers an (Cache + Sicherheit) │
│ └─ Wendet _redirects an (Erhalt älterer URLs) │
└──────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Browser des Lesers │
│ ├─ HTML vom CDN-Edge (unter 100 ms global) │
│ ├─ CSS mit Shiki-CSS-Variablen für theme-bewusste Code-Blöcke │
│ ├─ Gehashte Assets ein Jahr lang gecacht │
│ └─ Giscus beim Scrollen geladen (Kommentare, kein eageres JS) │
└─────────────────────────────────────────────────────────────────┘
Jedes Teil dieser Architektur spiegelt eine Version desselben Prinzips wider: So viel wie möglich zur Build-Zeit erledigen, so wenig wie möglich zur Laufzeit ausliefern und die Infrastruktur einfach genug halten, um sie vollständig zu verstehen. Astro übernimmt den Build, Cloudflare die Auslieferung, und der Abstand zwischen dem Schreiben eines Beitrags und seiner Online-Verfügbarkeit ist genau ein git push.
Welche Rolle spielt die KI dabei?
Vielleicht fragen Sie sich: Wo kommt bei all dem im Jahr 2026 die KI ins Spiel? Kurz gesagt: Die KI ist nicht Teil des Runtime-Stacks der Website; es werden weder KI-APIs zur Build-Zeit aufgerufen, noch sind Machine-Learning-Modelle in die Seite eingebettet oder Chatbot-Widgets vorhanden. Dennoch spielte die KI eine entscheidende Rolle bei der Erstellung der Website.
Ich habe einen spezifikationsbasierten Entwicklungs-Workflow genutzt, der auf einer lokalen KI-Umgebung basiert: LM Studio als Modell-Runtime, OpenCode als autonomer („agentic“) Programmierassistent und Qwen 3.5 als lokales Modell, das auf einem MacBook Pro läuft. Das Prinzip ist einfach: Bevor man Code schreibt, beschreibt man das gewünschte Ergebnis in natürlicher Sprache. Der KI-Agent durchsucht daraufhin die Codebasis, plant das Vorgehen und setzt es um, und das alles, ohne das Terminal zu verlassen oder auch nur eine einzige Zeile Code an eine Cloud-API zu senden.
Dieser Ansatz hat maßgeblich die Entscheidungen für die oben dargestellte Architektur beeinflusst. Content-Collections, i18n-Hilfsfunktionen, das Rehype-Plugin und die Deployment-Pipeline, all diese Elemente wurden im Rahmen spezifikationsbasierter Dialoge mit einem lokalen Modell entwickelt. Die KI übernahm die Erkundung des Repositorys, generierte Boilerplate-Code, schlug Architekturmuster vor und half beim Verfassen sowie bei der inhaltlichen Überprüfung dieses Artikels. Einen tieferen Einblick, welche Modelle und Tools auf Hardware mit begrenzten Ressourcen tatsächlich funktionieren und warum die Fähigkeiten eines KI-Agenten wichtiger sind als Ergebnisse in Coding-Benchmarks, bietet mein begleitender Artikel zur Einrichtung einer lokalen KI-Entwicklungsumgebung.
Deine Erfahrung
Betreibst du ebenfalls eine persönliche Website? Dann würde mich interessieren, welche Entscheidungen du getroffen hast. Nutzt du einen statischen Ansatz oder Server Rendering? Selbst gehostet oder ein Managed Hosting? Lag dein Fokus auf Einfachheit, auf Funktionen oder auf etwas ganz anderem? Und was würdest du heute anders machen, wenn du noch einmal von vorne anfangen würdest?
Hinterlasse gerne einen Kommentar. Ich lese jeden einzelnen.