Erscheinungsbild
project.config.ts
Die zentrale Projektkonfiguration. Alles hier ist öffentlich und wird versioniert — Geheimnisse gehören nicht in diese Datei.
ts
import { defineProject } from './src/lib/config';
export default defineProject({
site: {/* … */},
company: {/* … */},
localization: {/* … */},
navigation: {/* … */},
features: {/* … */},
seo: {/* … */},
tracking: {/* … */},
consent: {/* … */},
forms: {/* … */},
deployment: {/* … */},
});site
| Feld | Pflicht | Regel |
|---|---|---|
name | ja | nicht leer |
url | ja | vollständige URL, https://, ohne abschließenden Schrägstrich |
locale | ja | Sprachkennzeichen wie de-DE |
titleTemplate | ja | muss den Platzhalter %s enthalten |
description | ja | nicht leer |
company
Quelle für die Schemata Organization und LocalBusiness.
| Feld | Pflicht | Regel |
|---|---|---|
legalName | ja | nicht leer |
address | bedingt | erforderlich, wenn seo.schemas.localBusiness aktiv ist |
email | nein | gültige E-Mail-Adresse |
phone | nein | — |
mobile | nein | zweite Nummer neben dem Festnetz |
sameAs | nein | Liste von URLs, Vorgabe [] |
localization
| Feld | Pflicht | Regel |
|---|---|---|
defaultLocale | ja | muss in locales enthalten sein |
locales | ja | mindestens ein Eintrag |
Bei mehr als einer Sprache muss site.locale dem defaultLocale entsprechen.
Mehrere Sprachen erzeugen für sich genommen kein hreflang: Alternates entstehen erst, wenn eine Seite ihre Übersetzungen selbst nennt — siehe Metadaten.
navigation
Quelle für Kopf- und Fußnavigation. Ein Eintrag besteht aus label und href.
| Feld | Pflicht | Regel |
|---|---|---|
main | nein | Hauptnavigation im Kopf; Vorgabe [], keine doppelten Ziele |
legal.imprint | ja | Impressumslink |
legal.privacy | ja | Datenschutzlink |
legal.additional | nein | weitere Rechtslinks wie AGB; Vorgabe [] |
href muss ein absoluter Pfad (/kontakt) oder eine absolute https-URL sein. Relative Pfade sind ausgeschlossen: Sie hingen davon ab, auf welcher Seite die Navigation gerade gerendert wird, und ergeben in einem global eingebundenen Kopf keinen verlässlichen Sinn.
Impressum und Datenschutz sind Pflichtfelder
Beide sind für geschäftsmäßige Websites in Deutschland vorgeschrieben und müssen von jeder Seite aus erreichbar sein. Sie stehen deshalb als Pflichtfelder im Schema und nicht als zwei Einträge in einer Liste — fehlt eines, ist das ein Typfehler im Editor und nicht erst ein Build-Abbruch.
Externe Ziele erkennt die Navigation an der https-URL und ergänzt für Screenreader den Hinweis „(externe Website)". Ein target="_blank" setzt sie bewusst nicht: Ein erzwungener Tabwechsel nimmt der bedienenden Person eine Entscheidung ab, die ihr zusteht.
features
Alle Werte sind boolesch.
| Flag | Vorgabe | Wirkung |
|---|---|---|
sitemap | true | erzeugt sitemap.xml und den Verweis in robots.txt |
structuredData | true | Hauptschalter der JSON-LD-Ausgabe |
cookieConsent | true | Schalter der Consent-Schicht; die Kategorien füllen ihre Verbraucher |
vue | false | Vue als Astro Island; verlangt installiertes @astrojs/vue |
webMcp | false | experimentell; verlangt zusätzlich das Objekt webMcp |
seo
| Feld | Pflicht | Regel |
|---|---|---|
defaultOgImage | nein | Schlüssel im Asset-Manifest, kein freier Pfad |
logo | nein | Schlüssel im Asset-Manifest; Kopf und JSON-LD |
twitterSite | nein | muss mit @ beginnen |
schemas.organization | ja | boolesch |
schemas.localBusiness | ja | boolesch; verlangt company.address |
schemas.website | ja | boolesch |
defaultOgImage und logo verweisen absichtlich auf Manifest-Schlüssel statt auf Pfade. Im Referenzprojekt zeigten OG-Bild-Angaben auf Dateien, die nie existierten; ein unbekannter Schlüssel bricht dagegen den Build ab.
schemas.localBusiness ersetzt den Typ des Organisationsknotens, es entsteht kein zweiter Knoten. Details unter Structured Data.
tracking
| Feld | Vorgabe | Regel |
|---|---|---|
enabled | false | verlangt features.cookieConsent: true |
provider | 'gtm' | derzeit nur GTM |
containerId | — | Format GTM-XXXXXXX; Pflicht, wenn enabled |
gtmServerUrl | — | optionaler First-Party-Endpunkt für serverseitiges GTM |
consentMode | 'basic' | basic lädt GTM erst nach der Entscheidung |
ga4 | false | verlangt Consent-Kategorie analytics |
googleAds | false | verlangt Consent-Kategorie marketing |
ga4 und googleAds bauen keinen Code ein
Beide Flags dokumentieren, was im GTM-Container konfiguriert sein soll, und steuern Consent-Kategorien sowie Tests. Ein direkter Einbau von GA4 oder Google Ads neben GTM findet nicht statt und ist ausdrücklich unerwünscht.
consentMode: 'advanced' lädt den Container bereits vor der Entscheidung und überträgt dabei anonyme Messsignale. Das ist eine bewusste Entscheidung mit rechtlichen Folgen — Einzelheiten unter Tag Manager.
Unabhängig von diesen Werten lädt der Container ausschließlich bei PUBLIC_ENVIRONMENT=production.
consent
| Feld | Regel |
|---|---|
categories | mindestens ein Eintrag, muss necessary enthalten |
revision | ganze Zahl ab 1; Erhöhen fragt bestehende Zustimmungen erneut ab |
externalMedia | Liste eingebetteter Dienste (key, name) für external-media, Vorgabe [] |
categories ist eine Erklärung, keine Anzeigeliste: Angeboten wird eine Kategorie nur, wenn sie zusätzlich einen Verbraucher hat. Welche Signale sie freischaltet und wann die Fassungsnummer steigen muss, steht unter Consent.
forms
| Feld | Vorgabe | Regel |
|---|---|---|
provider | 'none' | none, standalone-php oder joomla |
endpoint | — | Pflicht, sobald ein Provider gewählt ist |
captcha | 'none' | none oder turnstile |
Der Endpunkt muss zum Provider passen: standalone-php verlangt einen same-origin Pfad beginnend mit /, joomla eine absolute https-URL des Backends.
Zwei Folgen, die man leicht übersieht:
- Bei
provider: 'none'rendert<Form />nichts. Ein Formular ohne Ziel gibt es nicht. - Sobald ein Provider gesetzt ist, wird die Kampagnenzuordnung zum Verbraucher der Kategorie
marketingund erscheint im Einwilligungsdialog — auch ohne Tracking. Stehtmarketingnicht inconsent.categories, wird nichts über Seitenwechsel hinweg gespeichert.
Welche Felder ein Formular annimmt, steht nicht hier, sondern in src/content/forms.ts. Siehe Formulare.
deployment
| Feld | Regel |
|---|---|
target | mittwald-static oder github-pages |
apache | optional; bei github-pages verboten |
apache.canonicalHost | optional; jede andere Schreibweise wird dorthin weitergeleitet |
apache.forceHttps | Vorgabe true |
apache.redirects | Liste aus from (absoluter Pfad), to, status (301 oder 302) |
apache.csp.mode | report-only (Vorgabe) oder enforce |
apache.csp.reportUri | optional; ohne ihn nur Konsolenmeldungen |
apache.csp.additionalSources | Zusätze je Direktive; 'unsafe-inline' bei script-src verboten |
apache.hsts | optional, standardmäßig aus |
Die apache-Sektion beschreibt die beim Bauen erzeugte .htaccess. Fehlt sie, gelten die Vorgaben; bei github-pages wird sie abgelehnt, weil dort niemand die Datei liest. Die Einzelheiten stehen unter Apache und .htaccess, die Richtlinie unter Content Security Policy.
Drei Regeln, die im Zusammenspiel greifen:
hsts.preloadverlangtincludeSubDomainsund mindestens ein JahrmaxAge— ein kürzerer Eintrag wird nicht angenommen und täuscht Schutz vor.site.urldarf keinen Unterpfad enthalten. Betrifft GitHub Project Pages: Die internen Links sind absolute Pfade ab der Wurzel und werden nirgends um ein Basisverzeichnis ergänzt.standalone-phpist mitgithub-pagesausgeschlossen — dort läuft kein PHP.
webMcp und meta
webMcp ist optional und erzwingt mit acknowledgeExperimental: true eine bewusste Aktivierung. Der Initialisierungsprozess fragt nicht danach — das Flag löst heute nichts aus, und eine Frage danach würde eine Funktion behaupten, die es nicht gibt.
meta.starterVersion setzt pnpm starter:init auf die Version des Starters, aus dem das Projekt entstanden ist. Sie ist der Ankerpunkt für ein späteres Upgrade — und zugleich das Kennzeichen, an dem der Init merkt, dass ein Projekt bereits initialisiert ist (ADR 0016).
Wie die Datei entsteht
Im Starter steht sie von Hand geschrieben da. In einem Kundenprojekt erzeugt sie pnpm starter:init aus den Antworten des Dialogs — und zwar aus demselben Objekt, das zuvor gegen dieses Schema geprüft wurde. Ab dem ersten Lauf ist sie eine gewöhnliche Quelldatei: Alles Weitere wird von Hand geändert und beim nächsten Build geprüft.