Erscheinungsbild
Layout und Komponenten
BaseLayout.astro
Jede Seite läuft durch dieses Layout. Es liefert das Dokumentgerüst, die Sprache aus der Projektkonfiguration, den Titel nach Vorlage, die Schriften und den Sprunglink.
astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Leistungen" description="Was wir anbieten.">
<p>Inhalt</p>
</BaseLayout>| Prop | Pflicht | Bedeutung |
|---|---|---|
title | ja | Seitentitel ohne Suffix; site.titleTemplate ergänzt ihn |
description | nein | fällt auf site.description zurück, damit sie nie leer ist |
breadcrumbs | nein | sichtbarer Pfad und BreadcrumbList aus einer Liste |
ogImage | nein | Schlüssel aus dem Asset-Manifest; sonst seo.defaultOgImage |
ogType | nein | website (Vorgabe) oder article |
translations | nein | Sprache → Pfad; erzeugt hreflang |
jsonLd | nein | zusätzliche JSON-LD-Knoten aus den Buildern |
locale | nein | nur für Seiten, die nicht in der Standardsprache vorliegen |
noindex | nein | Einzelfall; der Regelweg ist die Routen-Policy |
mainClass | nein | Klassen für das <main>, wenn der Standardabstand nicht passt |
Zusätzlich gibt es die benannten Slots head, header und footer.
Metadaten kommen aus einer Komponente
Titel, Canonical, Open Graph und JSON-LD gibt <Seo /> aus, eingebunden von diesem Layout. Eine Seite, die selbst etwas in den Kopf schreibt, hätte es doppelt. Details unter SEO.
Sprunglink
Der Sprunglink ist das erste per Tab erreichbare Element und wird erst bei Fokus sichtbar. Er ist ein reguläres Element und nicht hidden, damit Screenreader ihn in der Reihenfolge vorfinden. Der Rumpf trägt dazu <main id="inhalt">. Ein Smoke-Test prüft beides: Fokusreihenfolge und Sprungziel.
Kopf und Fuß
Header.astro und Footer.astro sind Standardinhalt der Slots header und footer — keine Seite muss sie einsetzen. Sonst fehlt der Impressumslink irgendwann auf genau der einen Seite, die jemand ohne Vorlage angelegt hat. Wer sie ersetzen will, etwa auf einer Landingpage ohne Navigation, befüllt den Slot:
astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Kampagne">
<div slot="header"></div>
<p>Inhalt ohne Navigation</p>
</BaseLayout>Aufklappnavigation
Auf schmalen Viewports klappt die Hauptnavigation über eine Schaltfläche auf. Bewusst kein Checkbox-Trick: Der käme ohne JavaScript aus, meldet Screenreadern aber ein Formularelement statt einer Schaltfläche und kennt keinen Auf-/Zu-Zustand. Die paar Zeilen Skript sind der ehrlichere Weg.
aria-expanded ist dabei die eigentliche Zustandsangabe, die CSS-Klasse folgt ihr nur. Wer beides getrennt pflegt, hat irgendwann eine sichtbare Navigation, die sich Screenreadern gegenüber als geschlossen ausgibt.
Abgedeckt sind: Öffnen und Schließen, Escape mit Fokusrückgabe, Klick daneben, Navigation im geöffneten Zustand — und eine axe-Prüfung im geöffneten Zustand, der in einer Prüfung des ausgelieferten HTML gar nicht vorkäme.
Ist navigation.main leer, entfallen Schaltfläche und <nav> vollständig. Ein leeres <nav> mit leerer Liste wäre für Screenreader ein Element, das nichts enthält.
Rechtliche Pflichtlinks
Impressum und Datenschutzerklärung stammen aus Pflichtfeldern der Projektkonfiguration (navigation.legal.imprint und .privacy) — nicht aus einer Liste, die leer bleiben könnte. Beide Seiten existieren ab dem Starter als Platzhalter, weil ein Footer-Link auf eine fehlende Seite in jedem Kundenprojekt ein toter Pflichtlink wäre.
Die Platzhalter enthalten keinen vorformulierten Rechtstext. Eine Datenschutzerklärung muss beschreiben, was die Website tatsächlich tut; ein generischer Text wäre im besten Fall unvollständig. Stattdessen leitet datenschutz.astro aus der Projektkonfiguration ab, welche Themen dieses Projekt betreffen — Tracking, Formulare, Turnstile — und listet sie als Arbeitsauftrag.
Brotkrumen
Breadcrumbs.astro rendert den Pfad, den BaseLayout als breadcrumbs bekommt. Dieselbe Liste erzeugt die BreadcrumbList im JSON-LD — sichtbarer Pfad und Auszeichnung können dadurch nicht auseinanderlaufen (ADR 0007).
Der letzte Eintrag ist die aktuelle Seite. Er trägt kein href, sondern aria-current="page": Ein Verweis auf die Seite, auf der man schon ist, führt nirgendwohin und wird trotzdem als Ziel angesagt.
Primitives
Unter src/components/ui/ liegen die Bausteine, die überall wiederkehren. Sie sind bewusst schmal: Sie liefern Fläche und Abstand, nicht Struktur.
Section.astro
Vertikaler Abschnitt mit Breitenbegrenzung und Zentrierung.
| Prop | Werte | Standard |
|---|---|---|
width | page, content | page |
tone | default, muted | default |
as | section, div, article, header, footer | section |
width="content" setzt die Lesebreite für Fließtext. as existiert, weil eine <section> ohne eigene Überschrift semantisch falsch wäre — dann ist div richtig.
Button.astro
Mit href entsteht ein <a>, ohne href ein <button>. Das ist keine Stiloption: Ein Link, der wie eine Schaltfläche aussieht, aber keine ist, bricht Tastaturbedienung und Screenreader-Ausgabe.
| Prop | Werte | Standard |
|---|---|---|
variant | primary, secondary, ghost | primary |
size | md, lg | md |
type | button, submit, reset | button |
type steht auf button, weil eine Schaltfläche ohne type innerhalb eines Formulars ungewollt absendet.
Card.astro
Abgesetzter Inhaltsblock. elevation="raised" hebt die Karte beim Überfahren an — gedacht für Karten, die als Ganzes verlinkt sind.
Die Komponente hat keinen Titel-Slot: Welche Überschriftenebene richtig ist, hängt vom Kontext der Seite ab. Die Struktur bleibt beim Aufrufer, sonst entstehen Dokumente mit springender Überschriftenhierarchie.
Prose.astro
Fließtext aus Markdown oder redaktionellem HTML. Nutzt @tailwindcss/typography und bindet dessen --tw-prose-*-Variablen an die Design Tokens, damit redaktioneller Text nicht anders aussieht als der Rest der Seite.
Alert.astro
Hinweisbox in den Tonalitäten info, warning und success. Die Tonalität wird nie allein über die Farbe transportiert — jede Box trägt einen Titel.
live ist für Meldungen gedacht, die erst nach einer Aktion erscheinen (ab M6 das Ergebnis eines Formularversands). Dann meldet die Box sich bei Screenreadern von selbst. Ruhende Hinweise bekommen das nicht: Sie würden das Vorlesen ohne Anlass unterbrechen.
Accordion.astro
Klappabschnitt auf Basis von <details> — kein JavaScript, keine ARIA-Attribute von Hand. Browser und Screenreader kennen das Element, melden den Zustand selbst und finden den Inhalt auch eingeklappt.
Mehrere Abschnitte mit demselben name schließen einander aus; auch das ist Teil des HTML-Standards und braucht kein Skript.
Faq.astro
Liegt unter src/components/seo/, weil sie zwei Dinge zugleich erzeugt: die sichtbaren Klappabschnitte (auf Basis von Accordion) und die FAQPage-Auszeichnung — beides aus derselben Liste. Genau das ist ihr Zweck: Google ahndet FAQ-Auszeichnung ohne sichtbare Fragen mit einer manuellen Maßnahme, und einen Weg, das eine ohne das andere zu bekommen, gibt es hier nicht.
Badge.astro
Kleine Auszeichnung für Status oder Kategorie in den Tonalitäten neutral, brand und success. Rein visuell — ein Badge darf nie die einzige Trägerin einer Information sein, weil seine Bedeutung ausschließlich aus Farbe und Kürze entsteht.
Formularfelder
Input.astro, Textarea.astro, Select.astro und Checkbox.astro. Alle vier nehmen Label, Beschreibung und Fehlertext als Props entgegen, statt sie dem Aufrufer zu überlassen — ein Feld ohne Label kann so gar nicht erst entstehen.
| Prop | Bedeutung |
|---|---|
name | Pflicht; dient zugleich als Grundlage der ids |
label | Pflicht (außer bei Checkbox, dort kommt der Text aus dem Slot) |
description | erklärender Text unter dem Label |
error | gesetzt heißt: fehlerhaft — setzt aria-invalid |
Die Verknüpfung von Feld, Beschreibung und Fehler übernimmt fieldIds() aus src/lib/forms/field.ts — an einer Stelle und nicht in jeder Komponente neu. Ein Fehlertext, der optisch unter dem Feld steht, aber nicht mit ihm verknüpft ist, existiert für Screenreader nicht; das ist die verbreitetste Art, ein Formular unbedienbar zu machen. Die Beschreibung wird dabei vor dem Fehler referenziert: erst wozu das Feld dient, dann was daran nicht stimmt.
Checkbox.astro nimmt den Text aus dem Slot statt aus einem Prop, weil Einwilligungstexte regelmäßig Links enthalten — etwa auf die Datenschutzerklärung.
Noch kein Formular
Die Felder sind Bausteine ohne Verdrahtung. Absenden, Validierung und Fehlerbehandlung kommen mit dem Formularvertrag in M6.
Styleguide
/styleguide zeigt jedes Primitive in jeder Variante auf einer Seite. Die Seite hat zwei Aufgaben:
- Nachschlagewerk beim Bauen neuer Seiten.
- Prüffläche — die axe-Prüfung deckt mit einem Aufruf die gesamte Komponentensammlung ab, statt jede Variante irgendwo im Projekt suchen zu müssen.
Sie ist noindex und steht nicht in der Sitemap — beides über dieselbe Routen-Policy.
Dynamische Klassennamen
Tailwind 4 liest den Quelltext als Text. Ein zusammengesetzter Klassenname wird deshalb nie gefunden:
Falsch — die Klasse entsteht nie:
astro
---
const stufe = 600;
---
<div class={`bg-brand-${stufe}`}></div>Richtig — vollständige Namen stehen im Quelltext:
astro
---
const stufen = [{ token: 'brand-600', klasse: 'bg-brand-600' }];
---
{stufen.map((stufe) => <div class={stufe.klasse} />)}