Skip to content

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 ​

FeldPflichtRegel
namejanicht leer
urljavollständige URL, https://, ohne abschließenden Schrägstrich
localejaSprachkennzeichen wie de-DE
titleTemplatejamuss den Platzhalter %s enthalten
descriptionjanicht leer

company ​

Quelle für die Schemata Organization und LocalBusiness.

FeldPflichtRegel
legalNamejanicht leer
addressbedingterforderlich, wenn seo.schemas.localBusiness aktiv ist
emailneingültige E-Mail-Adresse
phonenein—
mobileneinzweite Nummer neben dem Festnetz
sameAsneinListe von URLs, Vorgabe []

localization ​

FeldPflichtRegel
defaultLocalejamuss in locales enthalten sein
localesjamindestens 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.

Quelle für Kopf- und Fußnavigation. Ein Eintrag besteht aus label und href.

FeldPflichtRegel
mainneinHauptnavigation im Kopf; Vorgabe [], keine doppelten Ziele
legal.imprintjaImpressumslink
legal.privacyjaDatenschutzlink
legal.additionalneinweitere 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.

FlagVorgabeWirkung
sitemaptrueerzeugt sitemap.xml und den Verweis in robots.txt
structuredDatatrueHauptschalter der JSON-LD-Ausgabe
cookieConsenttrueSchalter der Consent-Schicht; die Kategorien füllen ihre Verbraucher
vuefalseVue als Astro Island; verlangt installiertes @astrojs/vue
webMcpfalseexperimentell; verlangt zusätzlich das Objekt webMcp

seo ​

FeldPflichtRegel
defaultOgImageneinSchlüssel im Asset-Manifest, kein freier Pfad
logoneinSchlüssel im Asset-Manifest; Kopf und JSON-LD
twitterSiteneinmuss mit @ beginnen
schemas.organizationjaboolesch
schemas.localBusinessjaboolesch; verlangt company.address
schemas.websitejaboolesch

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 ​

FeldVorgabeRegel
enabledfalseverlangt 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
ga4falseverlangt Consent-Kategorie analytics
googleAdsfalseverlangt 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.

FeldRegel
categoriesmindestens ein Eintrag, muss necessary enthalten
revisionganze Zahl ab 1; Erhöhen fragt bestehende Zustimmungen erneut ab
externalMediaListe 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 ​

FeldVorgabeRegel
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 marketing und erscheint im Einwilligungsdialog — auch ohne Tracking. Steht marketing nicht in consent.categories, wird nichts über Seitenwechsel hinweg gespeichert.

Welche Felder ein Formular annimmt, steht nicht hier, sondern in src/content/forms.ts. Siehe Formulare.

deployment ​

FeldRegel
targetmittwald-static oder github-pages
apacheoptional; bei github-pages verboten
apache.canonicalHostoptional; jede andere Schreibweise wird dorthin weitergeleitet
apache.forceHttpsVorgabe true
apache.redirectsListe aus from (absoluter Pfad), to, status (301 oder 302)
apache.csp.modereport-only (Vorgabe) oder enforce
apache.csp.reportUrioptional; ohne ihn nur Konsolenmeldungen
apache.csp.additionalSourcesZusätze je Direktive; 'unsafe-inline' bei script-src verboten
apache.hstsoptional, 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.preload verlangt includeSubDomains und mindestens ein Jahr maxAge — ein kürzerer Eintrag wird nicht angenommen und täuscht Schutz vor.
  • site.url darf 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-php ist mit github-pages ausgeschlossen — 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.