Aufbau einer produktionsreifen zweisprachigen Website mit Astro: Architektur und Implementierung
Der Aufbau einer zweisprachigen statischen Website erfordert mehr als nur das Duplizieren von Inhalten—es erfordert eine robuste Architektur, die typsichere Schemas, intelligentes Routing, effizientes Caching und nahtloses Deployment verwaltet. Dieser Beitrag untersucht die technische Implementierung eines produktionsreifen zweisprachigen Portfolios unter Verwendung von Astros Content Collections, komplett mit Docker-Containerisierung und nginx-Optimierungsstrategien.
Am Ende dieses Leitfadens verstehst du, wie man eine typsichere, performante zweisprachige Website mit Astro architekturiert, die von der Entwicklung bis zur Produktion skaliert.
Technische Anforderungen
Eine produktionsreife zweisprachige Website muss mehrere architektonische Herausforderungen lösen:
- Typsicherheit: Content-Schemas zur Build-Zeit validieren, um Fehler frühzeitig zu erkennen
- i18n-Routing: Sprachspezifische URLs ohne manuelle Routing-Konfiguration verwalten
- Performance: Caching-Strategien für statische Assets vs. dynamische Inhalte optimieren
- Deployment: Containerisierte Builds mit minimaler Image-Größe und maximaler Sicherheit
- Developer Experience: Schnelle Builds, Hot Reload und klare Fehlermeldungen
Die Lösung kombiniert Astros Content Collections für typsicheres Content-Management, natives i18n für Routing, Docker Multi-Stage Builds für Deployment und sorgfältig abgestimmte nginx-Konfiguration für Produktionsperformance.
Technische Grundlagen
Astro Content Collections mit i18n
Die Architektur beginnt mit Astros Content Collections, die typsicheres Content-Management mit Zod-Schema-Validierung bieten:
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
const portfolio = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
image: z.object({
url: z.string(),
alt: z.string()
}),
technologies: z.array(z.string()),
projectUrl: z.string().url().optional(),
githubUrl: z.string().url().optional(),
featured: z.boolean().default(false),
}),
});
export const collections = { portfolio };
Dieses Schema validiert jedes Portfolio-Element vor der Build-Zeit und erkennt Fehler wie fehlende Pflichtfelder oder ungültige URLs frühzeitig im Entwicklungsprozess.
Zweisprachige Content-Struktur
Die Verzeichnisstruktur trennt Inhalte nach Sprache und bleibt dabei organisiert:
src/content/
├── portfolio/
│ ├── en/
│ │ ├── mitkai.md
│ │ └── plugin-installer.md
│ └── de/
│ ├── kai.md
│ └── plugin-installer.md
└── blog/
├── en/
└── de/
Astros i18n-Konfiguration behandelt Routing automatisch:
// astro.config.mjs
export default defineConfig({
i18n: {
locales: ["de", "en"],
defaultLocale: "en",
routing: {
prefixDefaultLocale: false
}
}
});
Dies erzeugt saubere URLs:
- Englisch (Standard):
/portfolio/mitkai - Deutsch:
/de/portfolio/kai
Content-Abfragen und Filterung
Astros getCollection API bietet einen leistungsstarken Filtermechanismus für sprachspezifische Inhalte:
// src/pages/index.astro
import { getCollection } from 'astro:content';
// Nur englische Portfolio-Elemente abrufen
const englishProjects = await getCollection('portfolio', ({ id, data }) => {
return id.startsWith('en/') && data.featured;
});
// Deutsche Blog-Posts abrufen, Entwürfe ausschließen
const germanPosts = await getCollection('blog', ({ id, data }) => {
return id.startsWith('de/') && data.draft !== true;
});
// Nach Datum sortieren
const sortedPosts = germanPosts.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
Das id-Feld folgt dem Muster {sprache}/{dateiname} (z.B. en/mitkai.md), wodurch die Sprachfilterung mit startsWith() unkompliziert wird.
Dynamische Route-Generierung
Astro generiert Routen dynamisch für jedes Content-Element mit getStaticPaths:
// src/pages/portfolio/[...slug].astro
import { getCollection, type CollectionEntry } from 'astro:content';
export async function getStaticPaths() {
const portfolio = await getCollection('portfolio', ({ id }) =>
id.startsWith('en/')
);
return portfolio.map(entry => ({
params: { slug: entry.slug },
props: { entry },
}));
}
type Props = {
entry: CollectionEntry<'portfolio'>;
};
const { entry } = Astro.props;
const { Content } = await entry.render();
Dieses Methode:
- Ruft alle englischen Portfolio-Elemente zur Build-Zeit ab
- Erstellt automatisch eine Route für jedes Element
- Bietet typsichere Props mit vollständiger TypeScript-Unterstützung
- Rendert Markdown-Inhalte mit
entry.render()
Für die deutsche Version (/de/portfolio/[...slug].astro) änderst du einfach den Filter zu id.startsWith('de/').
Erweitertes Schema
Schemas mit berechneten Feldern erweitern
Astro Content Collections unterstützen berechnete Felder durch Remark-Plugins. So fügst du Lesezeit zu Blog-Posts hinzu:
// remark-reading-time.mjs
import getReadingTime from 'reading-time';
import { toString } from 'mdast-util-to-string';
export function remarkReadingTime() {
return function (tree, { data }) {
const textOnPage = toString(tree);
const readingTime = getReadingTime(textOnPage);
data.astro.frontmatter.minutesRead = readingTime.text;
};
}
Konfiguration in astro.config.mjs:
import { remarkReadingTime } from './remark-reading-time.mjs';
export default defineConfig({
markdown: {
remarkPlugins: [remarkReadingTime],
},
});
Schema aktualisieren, um das berechnete Feld einzuschließen:
const blog = defineCollection({
schema: z.object({
// ... andere Felder
minutesRead: z.string().optional(),
}),
});
Bildoptimierung mit Astro Assets
Für optimale Bildbehandlung verwende Astros Bilddienst:
import { z, defineCollection } from 'astro:content';
const portfolio = defineCollection({
schema: ({ image }) => z.object({
title: z.string(),
description: z.string(),
image: z.object({
url: image(), // Validiert Bildexistenz und optimiert es
alt: z.string(),
}),
technologies: z.array(z.string()),
projectUrl: z.string().url().optional(),
githubUrl: z.string().url().optional(),
featured: z.boolean().default(false),
}),
});
Der image()-Helper:
- Validiert Bildpfade zur Build-Zeit
- Ermöglicht automatische Optimierung
- Bietet typsichere Bild-Props
- Unterstützt responsive Bilder mit
<Picture>-Komponente
Produktions-Deployment
Docker Multi-Stage Build
Das Dockerfile verwendet einen zweistufigen Build für optimale Image-Größe:
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production
FROM macbre/nginx-brotli:latest
RUN apk add --no-cache curl
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s CMD curl --fail http://localhost/ || exit 1
CMD ["nginx", "-g", "daemon off;"]
Ergebnis: 30MB Produktions-Image mit Brotli-Komprimierungsunterstützung.
Intelligente Caching-Strategie
Die nginx-Konfiguration implementiert gestuftes Caching basierend auf Content-Typ:
# Astro gehashte Assets (Dateinamen ändern sich bei Updates)
location ~ ^/_astro/.*\.(js|css)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Bilder und Schriften
location ~* \.(jpg|png|svg|webp|woff2)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
# HTML-Seiten (Updates schnell sichtbar)
location ~* \.html$ {
expires 5m;
add_header Cache-Control "public, max-age=300, must-revalidate";
}
Wichtige Erkenntnis: Astro hasht Asset-Dateinamen automatisch (main.abc123.js), sodass sie für immer gecacht werden können. HTML-Seiten cachen nur 5 Minuten, wodurch neue Inhalte schnell erscheinen und gleichzeitig exzellente Performance für wiederkehrende Besucher erhalten bleibt.
Security-Härtung
Die Produktionskonfiguration umfasst umfassende Sicherheit:
# Rate Limiting (10 Anfragen/Sekunde pro IP)
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
# Security Headers
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'...";
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Zugriff auf versteckte Dateien verweigern
location ~ /\. { deny all; }
Performance-Ergebnisse
Nach Optimierung:
- Update-Sichtbarkeit: Neue Inhalte erscheinen innerhalb von 5 Minuten (war 1 Stunde)
- Komprimierung: Brotli erreicht ~50% Reduktion (vs ~30% mit gzip)
- Cache-Trefferquote: ~95% für wiederkehrende Besucher
- Seitenladezeit: Unter einer Sekunde für gecachte Inhalte
- Image-Größe: ~30MB Docker-Image (inklusive nginx + Inhalt)
SEO und Sitemap-Generierung
Astro generiert automatisch Sitemaps mit i18n-Unterstützung wenn konfiguriert:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com',
i18n: {
locales: ["de", "en"],
defaultLocale: "en",
},
integrations: [
sitemap({
i18n: {
defaultLocale: 'en',
locales: {
en: 'en-US',
de: 'de-DE',
},
},
}),
],
});
Dies generiert:
/sitemap-index.xml- Haupt-Sitemap-Index/sitemap-0.xml- Seiten-Sitemap mit hreflang-Tags
Beispiel-Sitemap-Eintrag mit Sprachalternativen:
<url>
<loc>https://example.com/portfolio/mitkai</loc>
<xhtml:link rel="alternate" hreflang="en-US"
href="https://example.com/portfolio/mitkai"/>
<xhtml:link rel="alternate" hreflang="de-DE"
href="https://example.com/de/portfolio/kai"/>
</url>
Language-Switcher-Komponente
Implementiere einen Sprachwechsler, der zwischen Locales mappt:
// src/components/LanguageSwitcher.astro
---
import { getRelativeLocaleUrl } from 'astro:i18n';
interface Props {
currentLocale: string;
currentPath: string;
}
const { currentLocale, currentPath } = Astro.props;
// Pfade zwischen Sprachen mappen
const pathMap = {
'portfolio/mitkai': 'portfolio/kai',
'portfolio/plugin-installer': 'portfolio/plugin-installer',
};
const translatedPath = currentLocale === 'en'
? pathMap[currentPath] || currentPath
: Object.keys(pathMap).find(key => pathMap[key] === currentPath) || currentPath;
const alternateLocale = currentLocale === 'en' ? 'de' : 'en';
const alternateUrl = getRelativeLocaleUrl(alternateLocale, translatedPath);
---
<a href={alternateUrl} hreflang={alternateLocale}>
{alternateLocale === 'en' ? 'English' : 'Deutsch'}
</a>
Docker-Build-Optimierung
Layer-Caching-Strategie
Der Schlüssel zu schnellen Docker-Builds ist intelligentes Layer-Caching:
# Stage 1: Dependencies (gecacht solange package.json unverändert)
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Stage 2: Build (gecacht solange Source unverändert)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Production
FROM macbre/nginx-brotli:latest
RUN apk add --no-cache curl
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
CMD curl --fail http://localhost/ || exit 1
CMD ["nginx", "-g", "daemon off;"]
Kritischer Punkt: Content-Änderungen beeinflussen package.json nicht, daher könnte Docker den Build-Layer wiederverwenden. Verwende immer --no-cache beim Deployment neuer Inhalte:
docker-compose build --no-cache && docker-compose up -d
Monitoring und Observability
Health Checks
Der Docker Health Check überwacht nginx intern:
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl --fail http://localhost/ || exit 1
Status prüfen:
docker inspect --format='{{.State.Health.Status}}' container_name
Performance-Monitoring
Cache-Effektivität überwachen:
# nginx Access Logs mit Cache-Status anzeigen
docker exec container_name tail -f /var/log/nginx/access.log
# Nach Cache-Misses filtern
docker exec container_name grep "MISS" /var/log/nginx/access.log
Cache-Status-Header in nginx.conf für Debugging hinzufügen:
add_header X-Cache-Status $upstream_cache_status always;
Build-Zeit-Optimierung
Astro Build-Performance verfolgen:
# In package.json hinzufügen
"scripts": {
"build": "astro build",
"build:verbose": "DEBUG=* astro build"
}
Bundle-Größe analysieren:
npm run build
du -sh dist/*
Erwartete Größen:
- HTML-Seiten: ~10-50 KB pro Seite
- Gehashte JS/CSS: ~100-500 KB insgesamt
- Bilder: Variabel (mit Tools wie Sharp optimieren)
Technologie-Stack
Core Framework:
- Astro 5.x: Statische Site-Generierung mit Islands-Architektur
- TypeScript 5.x: Typsichere Entwicklung
- Zod 3.x: Runtime-Schema-Validierung
- Tailwind CSS v4: Utility-First-Styling
Build & Deployment:
- Vite: Schnelles HMR und optimierte Builds
- Docker 24+: Containerisierung
- nginx 1.25+ mit Brotli: Produktions-Webserver
- Node.js 20 LTS: Build-Umgebung
Performance:
- Brotli-Komprimierung: ~50% Größenreduktion
- 5-Minuten HTML-Cache: Schnelle Content-Updates
- 1-Jahr Asset-Cache: Optimale Wiederholungsbesuche
- ETag-Validierung: Effiziente Revalidierung
Fazit
Der Aufbau einer produktionsreifen zweisprachigen Astro-Website kombiniert mehrere technische Disziplinen: typsichere Content Collections mit Zod-Schemas, intelligentes i18n-Routing, optimierte Docker-Builds mit Multi-Stage-Patterns und sorgfältig abgestimmte nginx-Caching-Strategien. Das Ergebnis ist eine schnelle, sichere und wartbare statische Website, die zwei Sprachen nahtlos verarbeitet.
Wichtigste technische Erkenntnisse:
- Typsicherheit ist wichtig: Zod-Schemas fangen Fehler zur Build-Zeit, nicht zur Laufzeit
- i18n ist eingebaut: Astros natives i18n behandelt Routing automatisch
- Strategisch cachen: Gehashte Assets cachen für immer, HTML für 5 Minuten
- Effizient bauen: Multi-Stage Docker-Builds minimieren Image-Größe
- Korrekt deployen: Immer
--no-cachefür Content-Updates verwenden
Diese Architektur bedient tausende monatliche Besucher mit Sub-Sekunden-Ladezeiten, 95%+ Cache-Trefferquoten und umfassenden Security-Headern—alles aus einem 30MB Docker-Image.
Tech Stack: Astro 5 · TypeScript · Docker · nginx + Brotli · Tailwind CSS v4 · Zod