Communitygithub.com

qalimero/qalim-portfolio

Umfassende Anleitung zum Erstellen und Bereitstellen statischer Websites mit dem Astro-Framework.

Was ist qalim-portfolio?

Dieses Skill bietet eine vollständige Referenz für die Erstellung und Bereitstellung von Astro-Websites im Static-Site-Generation-Modus (SSG). Es deckt alles ab, von den Grundlagen der Astro-Installation und Projektstruktur über Content Collections (einschließlich der neuen Content Layer API in Astro 5) bis hin zu erweiterten Funktionen wie Syntax-Highlighting mit Shiki und Expressive Code, der Integration von Diagrammen (Mermaid, PlantUML), Client-seitiger Suche mit Pagefind oder Fuse.js, Versionierung von Dokumentation und Internationalisierung (i18n) mit Fallbacks. Das Skill beschreibt gängige Muster wie Paginierung und RSS-Feeds, Best Practices für Performance (Prefetching, Critical CSS) und detaillierte Bereitstellungsoptionen für Dienste wie Netlify, Vercel, GitHub Pages und Google Cloud Platform / Firebase. Es enthält außerdem eine Pre-Deploy-Checkliste, Teststrategien mit Vitest und Playwright sowie eine Fehlerbehebung, was es zu einer wertvollen Ressource für Entwickler macht, die statische, inhaltsorientierte Websites mit Astro erstellen möchten.

Funktioniert mit✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/qalimero/qalim-portfolio/tree/HEAD/.claude/skills/publishing-astro-websites

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Vorschau

Aus dem Skill-README

qalim-portfolio preview

Dokumentation

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

# 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

ModusWann Seiten rendernAnwendungsfall
SSG (Standard)Zur Build-ZeitBlogs, Dokumentationen, Marketingseiten
SSRBei jeder AnfrageDynamische Daten, Personalisierung
HybridMischung aus beidemStatische 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:

  1. CSS-Variablen – Browser Farben zur Laufzeit auflösen lassen:
// mermaid config
mermaid.initialize({
  theme: 'base',
  themeVariables: {
    primaryColor: 'var(--diagram-primary)',
    lineColor: 'var(--diagram-line)'
  }
});
  1. 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>
  1. 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>
AttributZweck
data-pagefind-bodyIndizierung auf dieses Element beschränken
data-pagefind-ignoreElement vom Index ausschließen
data-pagefind-meta="key"Metadatenfeld definieren
data-pagefind-weight="10"Relevanz erhöhen (Standard: 1)

Pagefind vs. Fuse.js

FunktionPagefindFuse.js
ArchitekturVorgefertigte BinärblöckeLaufzeit im Arbeitsspeicher
BandbreiteNiedrig (lädt nur benötigte Blöcke)Hoch (lädt vollständigen Index)
Skalierbarkeit10.000+ Seiten< 500 Seiten
MehrsprachigNative WortstammerkennungManuelle Konfiguration
AnwendungsfallGlobale WebsitesucheKleine 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 build erfolgreich ausgeführt (keine Fehler)
  • Build-Ordner (dist/) auf korrekte Inhalte überprüft
  • Alle Links intern/extern geprüft (broken-link-checker verwenden)
  • Meta-Tags zur sozialen Optimierung hinzugefügt (Open Graph, Twitter Cards)
  • Strukturierte Daten für SEO (JSON-LD) validiert mit Schema.org Validator
  • robots.txt und sitemap.xml vorhanden
  • 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

ProblemUrsacheLösung
Build schlägt fehlFehler im Markdown-FrontmatterSchema validieren (Typo, fehlendes Feld)
Fehler beim Mermaid-RenderingDOM nicht bereitdocument ready sicherstellen oder Delay hinzufügen
SSR wird verwendet (obwohl statisch gewünscht)Falscher output-Modusoutput: 'static' in astro.config.mjs setzen
Fehlende BilderFalscher Pfad in image()Gültigen relativen Pfad von src/ aus überprüfen
Keine Suche bei PagefindBuild-Schritt fehltnpx pagefind --site dist zum Build-Script hinzufügen
i18n-Routing ignoriert LocaleFalsche Ordnerstruktursrc/content/docs/[locale]/ Schema einhalten

Verwandte Skills