Aufbau einer produktionsreifen zweisprachigen Website mit Astro: Architektur und Implementierung

• von Tobias Schäfer • 7 min read

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:

  1. Ruft alle englischen Portfolio-Elemente zur Build-Zeit ab
  2. Erstellt automatisch eine Route für jedes Element
  3. Bietet typsichere Props mit vollständiger TypeScript-Unterstützung
  4. 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-cache fü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