Site Services
Zurück zu Site Services

ENTWICKLER-ANLEITUNG · KONTAKTFORMULARE

Ein Astro-Kontaktformular verbinden

API-Referenz· OpenAPI JSON

Die öffentliche OpenAPI-Quelle ist für Coding-Agents und API-Werkzeuge direkt ohne Anmeldung lesbar. Sie beschreibt die öffentlichen Formularschema- und Anfrage-Endpunkte sowie authentifizierte Admin-Operationen. Ihr servers-Eintrag nennt Produktion; konfiguriere dein Werkzeug für andere Umgebungen mit der eingerichteten Backend-Origin.

Behalte dein Astro-Frontend. Verbinde ein eingerichtetes Formular über den Same-Origin-Proxy der Kundenwebsite, lies sein Schema und sende JSON.

01 · Mit persönlicher Einrichtung starten

Zugang erfolgt auf Einladung. Frage frühen Zugang für ein Projekt an; damit wird weder ein Konto erstellt noch ein Dienst aktiviert. Bevor dieses Beispiel an ein echtes Backend senden kann, müssen Workspace, Umgebung und eine aktive Formular-Umgebungszuordnung eingerichtet sein.

  • Vereinbare Feldschlüssel, Typen, Grenzen, Sprachen, Spam-Schutz und Aufbewahrung. Das Schema unten setzt die Pflichtfelder email und message voraus; es ist keine bereits vorhandene Kontovorlage.
  • Erlaube die exakte Website-Origin inklusive Protokoll und abweichendem Port: https://www.example.com ist nicht https://example.com. Richte Vorschau und Produktion getrennt ein.
  • Du benötigst die UUID der Formular-Umgebungszuordnung und den HTTPS-Hostnamen des Backends. Die UUID ist eine öffentliche Routing-Kennung, gelegentlich Formular-Token genannt; sie ist weder geheim noch ein API-Key. Diese öffentlichen Endpunkte verwenden weder Authorization-Header noch Benutzersitzung. Zugangsdaten für Betreiber gehören ausschliesslich in die Serverkonfiguration.

Validierung und Speicherung sind implementiert. Zum Launch umfasst der vollständige Kontaktformular-Ablauf API, Speicherung und E-Mail-Benachrichtigungen. Die operative E-Mail-Einrichtung steht aus; eine Posteingangsoberfläche ist separat geplant. Vereinbare und teste vor dem Kunden-Go-live, wie die zuständige Person legitime Anfragen erhält und bearbeitet.

Frühzeitigen Zugang anfragen →

02 · Proxy der Kundenwebsite einrichten

Diese nginx-Konfiguration gehört zur Kundenwebsite, die Astro ausliefert. Ersetze beide Platzhalter überall in der Serverkonfiguration durch die eingerichteten Werte. Die exakten Locations begrenzen den Zugriff auf eine UUID und zwei Routen. Der eigene Marketing-Ingress stellt nur seinen eingerichteten Submission-POST bereit; er ist kein Schema-Proxy für Kundenprojekte.

# Inside the CLIENT WEBSITE's HTTPS server block.
# Replace FORM_ENVIRONMENT_UUID and api.example.invalid during server setup.
location = /api/site-services/forms/FORM_ENVIRONMENT_UUID/schema {
    limit_except GET { deny all; }
    proxy_pass https://api.example.invalid/api/v1/forms/FORM_ENVIRONMENT_UUID/schema;
    include /etc/nginx/snippets/site-services-upstream.conf;
}
location = /api/site-services/forms/FORM_ENVIRONMENT_UUID/submissions {
    limit_except POST { deny all; }
    proxy_pass https://api.example.invalid/api/v1/forms/FORM_ENVIRONMENT_UUID/submissions;
    include /etc/nginx/snippets/site-services-upstream.conf;
}
# Reject every other Site Services route; do not expose admin or auth.
location /api/site-services/ { return 404; }

# /etc/nginx/snippets/site-services-upstream.conf
proxy_set_header Host api.example.invalid;
proxy_set_header Origin $http_origin;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Cookie "";
proxy_set_header Authorization "";
proxy_ssl_server_name on;
proxy_ssl_name api.example.invalid;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
proxy_intercept_errors off;
proxy_cache off;

Der Browser ruft /api/site-services/… auf seiner eigenen Origin auf; nginx leitet über geprüftes TLS an /api/v1/… weiter. Host und TLS-SNI müssen das Backend benennen, nicht die Kundenwebsite. Die eingehende Origin bleibt erhalten; ersetze sie nie durch eine erlaubte Konstante. Antwortstatus, JSON und Retry-After werden ohne Abfangen weitergereicht.

Dieses Beispiel setzt voraus, dass nginx öffentliches HTTPS selbst terminiert und das System-CA-Bundle am gezeigten Linux-Pfad verwendet. Leite öffentliches HTTP auf HTTPS um. Bei TLS-Terminierung am Loadbalancer musst du vertrauenswürdige Proxys und Real-IP konfigurieren und das externe Protokoll ausschliesslich von dort ableiten; ungeprüfte Client-Header weiterzureichen ist unsicher. Setze das nginx-Body-Limit mindestens auf das vereinbarte Formularlimit; zusätzlich gilt ein globales Backend-Limit. Rein statisches Hosting benötigt einen entsprechenden Server-/Edge-Proxy. Astro dev und preview installieren diese nginx-Routen nicht.

03 · Vertrag lesen, dann Nutzdaten aufbauen

GET /api/site-services/forms/FORM_ENVIRONMENT_UUID/schema

Ein erfolgreicher GET liefert ein unverpacktes JSON Schema nach Draft 2020-12, keine data/meta-Hülle. Es ist öffentlich und 60 Sekunden cachebar. Diese illustrative Antwort entspricht den vereinbarten Beispielfeldern; lies immer dein tatsächlich eingerichtetes Schema.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Contact",
  "type": "object",
  "properties": {
    "email": {
      "type": "string",
      "format": "email",
      "maxLength": 254,
      "x-labels": {
        "en": "Email",
        "de": "E-Mail"
      },
      "x-placeholders": {}
    },
    "message": {
      "type": "string",
      "maxLength": 2000,
      "x-labels": {
        "en": "Message",
        "de": "Nachricht"
      },
      "x-placeholders": {}
    }
  },
  "required": [
    "email",
    "message"
  ],
  "additionalProperties": false,
  "x-site-services": {
    "locales": [
      "en",
      "de"
    ],
    "honeypot_field": "website",
    "started_at_field": "started_at",
    "min_elapsed_ms": 3000,
    "max_elapsed_ms": 3600000,
    "max_body_bytes": 8192,
    "form_key": "contact"
  }
}

properties und required beschreiben nur fachliche Felder. locale und die beiden Spam-Schutz-Felder sind Transportmetadaten auf oberster Ebene und werden vor der Feldvalidierung entfernt. Lies ihre Namen und Zeitgrenzen aus x-site-services. Validiere bei additionalProperties: false nur die fachlichen Felder, nicht den gesamten Transport-Body. Das Backend bleibt massgeblich.

POST /api/site-services/forms/FORM_ENVIRONMENT_UUID/submissions · Content-Type: application/json

{
  "email": "alex@example.com",
  "message": "Können wir sprechen?",
  "locale": "de",
  "website": "",
  "started_at": 1790000000000
}

Dieser Zeitstempel dient nur der Illustration: Erfasse Date.now(), wenn das Browserformular bereit ist, niemals beim Build oder erst beim Absenden. Für menschliche Besucher bleibt der Honeypot leer. Sende flaches JSON, weder { data: … } noch FormData oder URL-kodiertes HTML. Konvertiere Boolean- und Integer-Werte explizit; erforderliche Boolean-Felder müssen true sein. Unbekannte fachliche Schlüssel scheitern an der Validierung.

04 · Kleinen Browser-Transport in Astro verwenden

Lade dieses Modul als src/lib/contact-form.js in dein Astro-Projekt und importiere seine beiden Funktionen in einem clientseitigen <script> deiner .astro-Komponente, nicht im Build-Time-Frontmatter. Der angezeigte Quelltext ist exakt die herunterladbare Datei, die von den Browsertests dieser Anleitung ausgeführt wird. Es ist ein Transportmodul, keine vollständige Formularkomponente und kein JSON-Schema-Validator.

contact-form.js herunterladen
// Browser transport only. The caller owns the form UI and catches rejected promises.
export async function loadContactForm(id, locale) {
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) {
    throw new Error('Use the provisioned form-environment UUID');
  }
  const base = `/api/site-services/forms/${id}`;
  const response = await fetch(`${base}/schema`, {
    credentials: 'omit', signal: AbortSignal.timeout(20000),
  });
  if (!response.ok) throw new Error(`Schema unavailable: ${response.status}`);
  const schema = await response.json(); // Raw JSON Schema, not result.data.
  const rules = schema['x-site-services'];
  if (!rules?.locales?.includes(locale)) throw new Error('Unsupported form locale');
  return { base, schema, rules, locale, startedAt: Date.now() };
}

export async function submitContactForm(session, fields, honeypot = '') {
  const { base, rules, locale, startedAt } = session;
  const elapsed = Date.now() - startedAt;
  if (rules.time_trap_enabled !== false) {
    if (elapsed < rules.min_elapsed_ms) return { kind: 'wait' };
    if (elapsed > rules.max_elapsed_ms) return { kind: 'expired' };
  }
  const body = JSON.stringify({
    ...fields, locale,
    [rules.honeypot_field]: honeypot,
    [rules.started_at_field]: startedAt,
  });
  if (new TextEncoder().encode(body).length > rules.max_body_bytes) {
    return { kind: 'failed', status: 413 };
  }
  const response = await fetch(`${base}/submissions`, {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    credentials: 'omit', body, signal: AbortSignal.timeout(20000),
  });
  const result = await response.json();
  if (response.status === 202 && result?.data?.status === 'accepted') {
    return { kind: 'accepted' };
  }
  if (response.status === 422 && result?.errors && typeof result.errors === 'object') {
    return { kind: 'invalid', errors: result.errors };
  }
  return { kind: 'failed', status: response.status };
}
  1. Rufe bei der Browser-Initialisierung einmal await loadContactForm(publicFormEnvironmentId, "de") (oder "en") auf und behalte die zurückgegebene Sitzung. Die öffentliche UUID stammt aus deiner Komponentenkonfiguration. Gleiche session.schema mit gerenderten Feldschlüsseln, Typen und Einschränkungen ab; bei Einrichtungsfehlern bleibt Senden deaktiviert.
  2. Verwende im Submit-Handler preventDefault(), prüfe native Validität, verhindere paralleles Senden und rufe await submitContactForm(session, { email: emailInput.value, message: messageInput.value }, honeypotInput.value) auf. Verwende deine eigenen Input-Referenzen und tatsächlichen Schema-Schlüssel. Nimm den Honeypot mit tabindex="-1", aria-hidden="true" und autocomplete="off" am versteckten Wrapper/Input entsprechend aus Tastatur- und Accessibility-Abläufen.
  3. Behandle jedes Ergebnis unten und fange Netzwerk-, Timeout- und Nicht-JSON-Fehler ab. Beende den Busy-Zustand immer in finally. Behalte Eingaben bei Fehlern, nutze einen aria-live-Status, ordne Fehler bekannten Feldern zu und fokussiere das erste ungültige Feld oder den Status. Rendere Meldungen als Text, niemals als Server-HTML. Erkläre ohne JavaScript, dass Senden nicht verfügbar ist, und biete eine echte Kontaktalternative; natives HTML-Absenden wird von diesem JSON-Endpunkt nicht unterstützt.

05 · Bestätigung und Fehler präzise behandeln

{ "data": { "status": "accepted" }, "meta": {} }

Nur HTTP 202 mit data.status === "accepted" ergibt kind: "accepted". Zeige «Anfrage angenommen» und verhindere erneutes Senden. Dieselbe Antwort gilt für still erkannten Spam, etwa einen gefüllten Honeypot oder zu schnelles Absenden. Sie beweist weder legitime Speicherung noch E-Mail-Zustellung, menschliche Antwort, Kontoerstellung oder Einladung.

HTTP 422 verwendet application/problem+json (RFC 9457). Lies die errors-Map auf oberster Ebene, nicht data.errors. Unten steht ein Ausschnitt des errors-Members, nicht das gesamte Problemdokument; Meldungen sind lokalisiert und konfigurierbar.

{
  "errors": {
    "email": [
      {
        "code": "required",
        "message": "Dieses Feld ist erforderlich."
      }
    ]
  }
}
  • kind: "invalid": Ordne bekannte Feldschlüssel deiner Oberfläche zu. Ein Zeitstempelfehler (expired, required oder invalid), locale-Fehler oder unbekannter Schlüssel benötigt Sitzungs-/Konfigurationswiederherstellung statt unsichtbarer Feldmarkierung.
  • kind: "wait": Warte bis min_elapsed_ms verstrichen ist; datiere den Zeitstempel nie zurück. kind: "expired": Biete einen ausdrücklichen Neustart über loadContactForm an, behalte Eingaben und warte die neue Mindestzeit. Sende nie automatisch erneut.
  • kind: "failed": 400 bedeutet fehlerhaftes JSON; 403 inaktive Zuordnung oder fehlende/nicht erlaubte Origin; 404 unbekannte UUID; 413 zu grosser Body; 415 falscher Medientyp. Korrigiere Konfiguration oder Nutzdaten statt blind zu wiederholen. 429 verlangt eine Pause (beachte Retry-After, falls vorhanden); 5xx ist ein Dienstfehler.
  • Das minimale Modul liefert den Status, nicht Retry-After; ergänze die Header-Auswertung im UI-Adapter, falls du zeitgesteuerte Wiederholungen einbaust. Ein abgelehntes Promise bedeutet, dass die Annahme nicht bestätigt werden konnte. Behalte Eingaben und biete manuelle Wiederherstellung; die Anfrage könnte bereits angekommen sein, automatische Wiederholung kann sie duplizieren.

06 · Auf der tatsächlichen Kunden-Origin prüfen

Die automatisierten Browserprüfungen dieser Anleitung führen das herunterladbare Modul gegen simulierte API-Antworten aus. Sie stellen keine Live-Verbindung von nginx zu Rails her und beweisen keine Zustellung. Schema und Anfragevertrag sind auf die öffentlichen Backend-Controller, den Schema-Generator und ausführende Request-Specs zurückgeführt.

  1. Prüfe nach der Einrichtung nginx-Konfiguration und Zertifikate. Lade das Schema von der Kunden-Origin und gleiche jedes gerenderte Feld und Limit ab. Prüfe, dass ein fremder /api/site-services/-Pfad 404 liefert.
  2. Sende nach der Mindestzeit eine gültige Anfrage; prüfe die exakte 202-Hülle und bestätige unabhängig den legitimen gespeicherten Datensatz über den vereinbarten Betreiberablauf. Prüfe nach Ablauf der Zeitsperre, dass ein bekanntes ungültiges Feld 422 liefert.
  3. Prüfe, dass fremde oder fehlende Origins abgewiesen werden, abgelaufene Sitzungen Eingaben behalten, Netzwerkfehler nichts löschen und Tastatur-, Mobil- und Fehleransagen funktionieren. Vereinbare Datenschutzhinweise und den operativen Bearbeitungsweg vor dem Go-live.