Publizieren von Astro-Websites
Erstellen Sie schnelle, inhaltsorientierte statische Websites mit dem Laufzeit-Null-SSG-Ansatz von Astro, teilweiser Hydrierung und umfangreicher Markdown-Unterstützung.
Inhalt
- Schnellstart
- Wann nicht zu verwenden
- Projektstruktur
- SSG vs. SSR vs. Hybrid
- Content Collections – Content Layer API, benutzerdefinierte Loader
- Syntaxhervorhebung – Shiki, Transformer, Expressive Code
- Diagrammintegration – Mermaid, PlantUML, Dark-Mode-Design
- Clientseitige Suche – Pagefind (Steuerung, Gewichtung), Fuse.js
- Versionierte Dokumentation – Mehrere Versionen
- Internationalisierung (i18n) – Routing, Fallbacks
- Häufige Muster – Paginierung, Tags, RSS, Formulare
- Best Practices für die Leistung – Prefetching, Critical CSS
- Bereitstellung – Netlify, Vercel, GitHub Pages, GCP/Firebase
- Checkliste vor der Bereitstellung
- Testen und Qualität – Vitest, Playwright, Linkprüfung
- Fehlerbehebung
Schnellstart
# Neues Projekt erstellen (Blog-Vorlage für Markdown-Seiten verwenden)
npm create astro@latest
# Entwicklung
npm run dev # Lokaler Server unter http://localhost:4321
npm run build # Statische Dateien in dist/ generieren
npm run preview # Produktionsbuild in der Vorschau anzeigen
Wann nicht zu verwenden
Dieses Skill konzentriert sich auf Static Site Generation (SSG). Ziehen Sie andere Ansätze in Betracht für:
- Echtzeit-Datenanwendungen – Verwenden Sie den SSR-Modus mit Datenbankverbindungen
- Benutzerauthentifizierungsabläufe – Erfordert serverseitige Sitzungsverwaltung
- E-Commerce mit dynamischem Bestand – Verwenden Sie den Hybrid-Modus oder vollständiges SSR
- Single-Page-Anwendungen (SPAs) – Ziehen Sie React/Vue-Frameworks direkt in Betracht
Für hybride SSG+SSR-Muster lesen Sie die Dokumentation zu Astros Adaptern.
Projektstruktur
src/
components/ # Astro-, React-, Vue-, Svelte-Komponenten
content/ # Content Collections (Markdown/MDX)
config.ts # Schema für Collections
docs/ # Beispiel-Collection
layouts/ # Seiten-Wrapper mit Slots
pages/ # Dateibasiertes Routing
public/ # Statische Assets (Bilder, Schriften, Favicons)
astro.config.mjs # Framework-Konfiguration
SSG vs. SSR vs. Hybrid
| Modus | Wann Seiten rendern | Anwendungsfall |
|---|---|---|
| SSG (Standard) | Zur Build-Zeit | Blogs, Dokumentationen, Marketingseiten |
| SSR | Bei jeder Anfrage | Dynamische Daten, Personalisierung |
| Hybrid | Mischung aus beidem | Statische Seiten + dynamische Endpunkte |
Verwenden Sie für rein statische Seiten das Standard-output: 'static' – kein Adapter erforderlich.
Content Collections
Legacy-Muster (Astro 4.x)
Definieren Sie Schemas in src/content/config.ts:
import { defineCollection, z } from "astro:content";
export const collections = {
docs: defineCollection({
schema: z.object({
title: z.string(),
description: z.string().optional(),
tags: z.array(z.string()).optional(),
order: z.number().optional(),
draft: z.boolean().default(false)
})
})
};
Content Layer API (Astro 5.0+)
Neues Muster mit dem glob()-Loader – bis zu 75 % schnellere Builds für große Websites:
// src/content.config.ts (Hinweis: anderer Dateiname)
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
schema: ({ image }) => z.object({
title: z.string(),
pubDate: z.coerce.date(),
draft: z.boolean().default(false),
cover: image(), // Überprüft, ob das Bild existiert
author: reference('authors'), // Referenz über Collections hinweg
})
});
export const collections = { blog };
Erweiterte Schema-Muster
schema: ({ image }) => z.object({
cover: image(), // Überprüft Bild in src/
category: z.enum(['tech', 'news']),
author: reference('authors'), // Referenz über Collections hinweg
relatedPosts: z.array(reference('blog')).optional(),
})
Benutzerdefinierte Loader (Remote-Inhalte)
Inhalte von externen APIs abrufen (GitHub-Releases, CMS usw.):
// src/loaders/github-releases.ts
import type { Loader } from 'astro/loaders';
export function githubReleasesLoader(repo: string): Loader {
return {
name: 'github-releases',
load: async ({ store, logger }) => {
logger.info(`Hole Releases für ${repo}`);
const response = await fetch(`https://api.github.com/repos/${repo}/releases`);
const releases = await response.json();
for (const release of releases) {
store.set({
id: release.tag_name,
data: {
version: release.tag_name,
published_at: release.published_at,
body: release.body // Markdown-Release-Notizen
}
});
}
}
};
}
Registrieren in content.config.ts:
import { githubReleasesLoader } from './loaders/github-releases';
const releases = defineCollection({
loader: githubReleasesLoader('owner/repo'),
schema: z.object({
version: z.string(),
published_at: z.string(),
body: z.string(),
})
});
Collections abfragen und rendern:
---
import { getCollection } from "astro:content";
export async function getStaticPaths() {
const docs = await getCollection("docs");
return docs.map(doc => ({
params: { slug: doc.slug },
props: { doc }
}));
}
const { doc } = Astro.props;
const { Content } = await doc.render();
---
<article>
<h1>{doc.data.title}</h1>
<Content />
</article>
Syntaxhervorhebung
Grundlegende Shiki-Konfiguration
import { defineConfig } from "astro/config";
export default defineConfig({
markdown: {
shikiConfig: {
theme: "github-dark",
wrap: true
}
}
});
Duales Hell-/Dunkel-Theme
shikiConfig: {
themes: {
light: 'github-light',
dark: 'github-dark',
},
}
CSS hinzufügen, um die Themes zu wechseln:
@media (prefers-color-scheme: dark) {
.astro-code, .astro-code span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}
}
Zeilenhervorhebung und Transformer
```typescript {2,4}
const a = 1;
const b = 2; // hervorgehoben
const c = 3;
console.log(a + b + c); // hervorgehoben
```
Shiki-Transformer (Astro 4.14+):
import { transformerNotationFocus, transformerNotationDiff } from '@shikijs/transformers';
shikiConfig: {
transformers: [transformerNotationFocus(), transformerNotationDiff()],
}
Notationskommentare im Code verwenden:
// [!code focus]– Diese Zeile fokussieren// [!code ++]– Als Hinzufügung markieren (grün)// [!code --]– Als Löschung markieren (rot)
Expressive Code (Empfohlen für Dokumentationen)
Umfangreiche Codeblöcke mit Kopierbuttons, Dateinamen und Diff-Hervorhebung:
npm install astro-expressive-code
import expressiveCode from 'astro-expressive-code';
export default defineConfig({
integrations: [expressiveCode()],
});
Funktionen: Kopierbutton, Datei-Tabs, Zeilenmarkierungen, Terminal-Frames, Textmarkierungen.
Diagrammintegration
Mermaid (Empfohlen)
Die Astro-Integration installieren:
npm install astro-mermaid mermaid
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mermaid from 'astro-mermaid';
export default defineConfig({
integrations: [mermaid({ theme: 'default' })]
});
In Markdown verwenden:
```mermaid
graph TD;
A-->B;
B-->C;
```
Funktionen: Clientseitiges Rendern, automatischer Theme-Wechsel, offline-fähig, kein Playwright erforderlich.
Alternative (Build-Zeit statisches SVG): Verwenden Sie rehype-mermaid mit Playwright für vorgerenderte Diagramme (npx playwright install --with-deps erforderlich).
Dark-Mode-Design-Strategien:
- CSS-Variablen – Browser Farben zur Laufzeit auflösen lassen:
// mermaid config
mermaid.initialize({
theme: 'base',
themeVariables: {
primaryColor: 'var(--diagram-primary)',
lineColor: 'var(--diagram-line)'
}
});
- Picture-Element – Beide Themes generieren, mit Media Query wechseln:
<picture>
<source srcset="/diagrams/flow-dark.svg" media="(prefers-color-scheme: dark)">
<img src="/diagrams/flow-light.svg" alt="Flussdiagramm">
</picture>
- Inline-SVG – SVG-Klassen mit CSS ansprechen (Risiko: Stilkonflikte) – Barrierefreiheitskriterien hinzufügen mit der Möglichkeit, role='image' oder role="presentation" zu haben:
.dark .mermaid-svg .node rect {
fill: var(--bg-dark);
}
PlantUML
npx astro add plantuml
In Markdown verwenden:
```plantuml
@startuml
Alice -> Bob: Hallo
Bob --> Alice: Hi!
@enduml
```
Clientseitige Suche
Pagefind (Empfohlen für große Websites)
Null-Konfiguration statische Suche, die zur Build-Zeit indiziert:
npm install pagefind
Zu package.json hinzufügen:
{
"scripts": {
"build": "astro build && npx pagefind --site dist",
"postbuild": "pagefind --site dist"
}
}
In Komponenten verwenden:
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script src="/pagefind/pagefind-ui.js" type="text/javascript"></script>
<div id="search"></div>
<script>
window.addEventListener('DOMContentLoaded', () => {
new PagefindUI({ element: '#search', showSubResults: true });
});
</script>
Funktionen: Kein externer Dienst, funktioniert offline, automatische Indizierung, kleines Bundle (~10 KB).
Granulare Indizierungssteuerung:
<!-- Nur Hauptinhalt indizieren, nicht Kopf-/Seitenleisten -->
<main data-pagefind-body>
<h1 data-pagefind-meta="title">Seitentitel</h1>
<p data-pagefind-weight="10">Wichtiger Einleitungstext</p>
<!-- Von Suchergebnissen ausschließen -->
<nav data-pagefind-ignore>
<a href="/related">Verwandte Beiträge</a>
</nav>
</main>
| Attribut | Zweck |
|---|---|
data-pagefind-body | Indizierung auf dieses Element beschränken |
data-pagefind-ignore | Element vom Index ausschließen |
data-pagefind-meta="key" | Metadatenfeld definieren |
data-pagefind-weight="10" | Relevanz erhöhen (Standard: 1) |
Pagefind vs. Fuse.js
| Funktion | Pagefind | Fuse.js |
|---|---|---|
| Architektur | Vorgefertigte Binärblöcke | Laufzeit im Arbeitsspeicher |
| Bandbreite | Niedrig (lädt nur benötigte Blöcke) | Hoch (lädt vollständigen Index) |
| Skalierbarkeit | 10.000+ Seiten | < 500 Seiten |
| Mehrsprachig | Native Wortstammerkennung | Manuelle Konfiguration |
| Anwendungsfall | Globale Websitesuche | Kleine Listenfilterung |
Fuse.js (Leichte Alternative)
Für kleinere Websites mit benutzerdefinierten UI-Anforderungen:
---
import { getCollection } from "astro:content";
const posts = await getCollection('blog');
const searchIndex = JSON.stringify(posts.map(post => ({
title: post.data.title,
slug: post.slug,
body: post.body.slice(0, 500)
})));
---
<input type="search" id="search" placeholder="Suchen..." />
<ul id="results"></ul>
<script define:vars={{ searchIndex }}>
import Fuse from 'fuse.js';
const fuse = new Fuse(JSON.parse(searchIndex), {
keys: ['title', 'body'],
threshold: 0.3
});
document.getElementById('search').addEventListener('input', (e) => {
const results = fuse.search(e.target.value);
const resultsEl = document.getElementById('results');
// Vorherige Ergebnisse sicher entfernen
resultsEl.replaceChildren();
// Ergebnisse mit sicheren DOM-Methoden erstellen
results.forEach(r => {
const li = document.createElement('li');
const link = document.createElement('a');
link.href = `/blog/${r.item.slug}`;
link.textContent = r.item.title;
li.appendChild(link);
resultsEl.appendChild(li);
});
});
</script>
Für Unternehmensanforderungen erwägen Sie Algolia (gehostete Such-API).
Versionierte Dokumentation
Muster für mehrere Versionen
Verwenden Sie eine ordnerbasierte Struktur:
src/content/docs/
v1/
getting-started.md
api-reference.md
v2/
getting-started.md
api-reference.md
new-feature.md
Internationalisierung (i18n)
Konfiguration in astro.config.mjs:
export default defineConfig({
i18n: {
defaultLocale: "en",
locales: ["en", "fr", "es"],
routing: {
prefixDefaultLocale: false
}
}
});
Inhalte nach Sprache strukturieren:
src/content/docs/
en/
getting-started.md
fr/
getting-started.md
Sprache in Komponenten erkennen:
---
const locale = Astro.currentLocale || 'en';
---
Fallback für fehlende Übersetzungen
Englischen Inhalt mit einem Banner anzeigen, wenn Übersetzungen nicht vorhanden sind:
---
// src/pages/[lang]/[...slug].astro
import { getCollection, getEntry } from "astro:content";
const languages = ['en', 'es', 'fr'];
const defaultLang = 'en';
export async function getStaticPaths() {
const englishDocs = await getCollection('docs', ({ id }) => id.startsWith('en/'));
const paths = [];
for (const doc of englishDocs) {
const slug = doc.id.replace(/^en\//, '');
for (const lang of languages) {
const localizedId = `${lang}/${slug}`;
const localizedDoc = await getEntry('docs', localizedId);
paths.push({
params: { lang, slug },
props: {
entry: localizedDoc || doc, // Fallback auf Englisch
isFallback: !localizedDoc
}
});
}
}
return paths;
}
const { entry, isFallback } = Astro.props;
const { Content } = await entry.render();
---
{isFallback && (
<div class="übersetzungshinweis">
Diese Seite ist noch nicht in Ihrer Sprache verfügbar.
</div>
)}
<Content />
Häufige Muster
Paginierte Auflistungen
Für Websites mit 50+ Beiträgen auf Seiten aufteilen:
---
// src/pages/blog/page/[page].astro
import { getCollection } from "astro:content";
const BEITRÄGE_PRO_SEITE = 10;
export async function getStaticPaths() {
const alleBeiträge = await getCollection("blog");
const gesamtSeiten = Math.ceil(alleBeiträge.length / BEITRÄGE_PRO_SEITE);
// ... (Rest des Codes bleibt unverändert, nur Kommentare übersetzt)
}
const { seite } = Astro.params;
const start = (parseInt(seite) - 1) * BEITRÄGE_PRO_SEITE;
const beiträge = alleBeiträge.slice(start, start + BEITRÄGE_PRO_SEITE);
---
<h1>Blog-Seite {seite}</h1>
{beiträge.map(beitrag => (
<article>
<h2><a href={`/blog/${beitrag.slug}`}>{beitrag.data.title}</a></h2>
<p>{beitrag.data.description}</p>
</article>
))}
Tags und Kategorien
export async function getStaticPaths() {
const alleBeiträge = await getCollection("blog");
const tags = [...new Set(alleBeiträge.flatMap(b => b.data.tags))];
return tags.map(tag => ({
params: { tag },
props: {
beiträge: alleBeiträge.filter(b => b.data.tags?.includes(tag))
}
}));
}
RSS-Feed
npm install @astrojs/rss
import rss from '@astrojs/rss';
export async function GET(context) {
const beiträge = await getCollection("blog");
return rss({
title: 'Mein Blog',
description: 'Ein Blog über coole Sachen',
site: context.site,
items: beiträge.map(beitrag => ({
title: beitrag.data.title,
pubDate: beitrag.data.pubDate,
description: beitrag.data.description,
link: `/blog/${beitrag.slug}/`,
})),
});
}
Formularverarbeitung (Statisch)
Verwenden Sie Dienste wie Formspree, Netlify Forms oder Vercel Forms:
<form action="https://formspree.io/f/meinFormular" method="POST">
<label for="email">E-Mail:</label>
<input type="email" id="email" name="email" required>
<button type="submit">Absenden</button>
</form>
Best Practices für die Leistung
Prefetching
import { defineConfig } from 'astro/config';
export default defineConfig({
prefetch: {
prefetchAll: true,
defaultStrategy: 'hover'
}
});
Critical CSS
Inline-CSS über benutzerdefinierte Integration extrahieren und inline einfügen.
Bildoptimierung
---
import { Image } from '@astrojs/image';
import heroImage from '../assets/hero.jpg';
---
<Image src={heroImage} alt="Held" widths={[300, 600, 900]} sizes="(max-width: 900px) 100vw, 900px" />
Bereitstellung
Netlify
npm install @astrojs/netlify
import netlify from '@astrojs/netlify';
export default defineConfig({
output: 'static', // kein Adapter für statische Sites erforderlich
adapter: netlify(),
});
Vercel
npm install @astrojs/vercel
import vercel from '@astrojs/vercel';
export default defineConfig({
adapter: vercel({ isr: true })
});
GitHub Pages
name: Deployment
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm ci
- run: npm run build
- name: Auf GitHub Pages bereitstellen
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
GCP / Firebase
Firebase Hosting
npm install -g firebase-tools
firebase login
firebase.json:
{
"hosting": {
"public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [{
"source": "**",
"destination": "/index.html"
}]
}
}
Google Cloud Storage + CDN
# Build generieren
npm run build
# In einen Cloud Storage Bucket hochladen
gsutil rsync -R dist/ gs://meine-bucket-name
# Öffentlichen Zugriff gewähren
gsutil iam ch allUsers:objectViewer gs://meine-bucket-name
# Load Balancer und CDN für HTTPS und globale Verteilung einrichten
Checkliste vor der Bereitstellung
-
npm run builderfolgreich ausgeführt (keine Fehler) - Build-Ordner (
dist/) auf korrekte Inhalte überprüft - Alle Links intern/extern geprüft (
broken-link-checkerverwenden) - Meta-Tags zur sozialen Optimierung hinzugefügt (Open Graph, Twitter Cards)
- Strukturierte Daten für SEO (JSON-LD) validiert mit Schema.org Validator
-
robots.txtundsitemap.xmlvorhanden - Fehlerseiten (404.html) angepasst
- Leistungsmetriken mit Lighthouse überprüft (Benchmark: >90 auf allen Metriken)
- Reaktionsfähiges Design auf mehreren Geräten und Browsern getestet
- Barrierefreiheit überprüft mit aXe oder WAVE
- Korrekte HTTP-Header für Caching und Sicherheit festgelegt (CSP, HSTS, etc.)
- Umgebungsvariablen mit
import.meta.envüberprüft - Analytics-Tracking (z.B. Plausible, Fathom) getestet
- Nach der Bereitstellung: Auf Konsolenfehler in den DevTools überprüfen
- CDN-Cache leeren, falls aktualisierte statische Assets vorhanden sind
Testen & Qualität
Vitest (Unit-Tests)
npm install -D vitest
// sum.test.ts
import { describe, it, expect } from 'vitest';
function sum(a: number, b: number): number {
return a + b;
}
describe('sum', () => {
it('sollte 2 + 2 = 4 sein', () => {
expect(sum(2, 2)).toBe(4);
});
});
Playwright (End-to-End-Tests)
npm install -D @playwright/test
npx playwright install
// tests/home.spec.ts
import { test, expect } from '@playwright/test';
test('Homepage sollte funktionieren', async ({ page }) => {
await page.goto('http://localhost:4321');
await expect(page).toHaveTitle(/Willkommen/);
});
Linkprüfung
npm install -D broken-link-checker
npx broken-link-checker --filter-level 3 http://localhost:4321
Fehlerbehebung
| Problem | Ursache | Lösung |
|---|---|---|
| Build schlägt fehl | Fehler im Markdown-Frontmatter | Schema validieren (Typo, fehlendes Feld) |
| Fehler beim Mermaid-Rendering | DOM nicht bereit | document ready sicherstellen oder Delay hinzufügen |
| SSR wird verwendet (obwohl statisch gewünscht) | Falscher output-Modus | output: 'static' in astro.config.mjs setzen |
| Fehlende Bilder | Falscher Pfad in image() | Gültigen relativen Pfad von src/ aus überprüfen |
| Keine Suche bei Pagefind | Build-Schritt fehlt | npx pagefind --site dist zum Build-Script hinzufügen |
| i18n-Routing ignoriert Locale | Falsche Ordnerstruktur | src/content/docs/[locale]/ Schema einhalten |