Skip to content

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>
PropPflichtBedeutung
titlejaSeitentitel ohne Suffix; site.titleTemplate ergänzt ihn
descriptionneinfällt auf site.description zurück, damit sie nie leer ist
breadcrumbsneinsichtbarer Pfad und BreadcrumbList aus einer Liste
ogImageneinSchlüssel aus dem Asset-Manifest; sonst seo.defaultOgImage
ogTypeneinwebsite (Vorgabe) oder article
translationsneinSprache → Pfad; erzeugt hreflang
jsonLdneinzusätzliche JSON-LD-Knoten aus den Buildern
localeneinnur für Seiten, die nicht in der Standardsprache vorliegen
noindexneinEinzelfall; der Regelweg ist die Routen-Policy
mainClassneinKlassen 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.

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.

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.

PropWerteStandard
widthpage, contentpage
tonedefault, muteddefault
assection, div, article, header, footersection

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.

PropWerteStandard
variantprimary, secondary, ghostprimary
sizemd, lgmd
typebutton, submit, resetbutton

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.

PropBedeutung
namePflicht; dient zugleich als Grundlage der ids
labelPflicht (außer bei Checkbox, dort kommt der Text aus dem Slot)
descriptionerklärender Text unter dem Label
errorgesetzt 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:

  1. Nachschlagewerk beim Bauen neuer Seiten.
  2. 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} />)}