Mit Webhooks veröffentlichen
Sende jeden Artikel, den du in GrowWriter veröffentlichst, direkt an deine eigene Website. Richte die Webhook-Integration Schritt für Schritt ein, mit fertigem Code.
Zuletzt aktualisiert am
Deine Website läuft vielleicht auf keiner der Plattformen, die GrowWriter von Haus aus anbindet. Kein Problem. Mit der Webhook-Integration liefert GrowWriter jeden Artikel direkt an deine Website, sobald du auf Veröffentlichen klickst. Eigener Stack, selbst gebautes CMS, statische Seite, ganz egal. Wenn sie eine Web-Anfrage empfangen kann, kann sie deine Artikel empfangen.
Diese Anleitung führt dich durch die komplette Einrichtung. Du brauchst keine Webhook-Erfahrung. Du verbindest den Webhook in GrowWriter, fügst ein kleines Stück Code zu deiner Website hinzu und testest es. Am Ende legt jeder veröffentlichte GrowWriter-Artikel automatisch einen Beitrag auf deiner eigenen Website an oder aktualisiert ihn. Alles, was du brauchst, ist eine Website, in die du Code einbauen kannst.
Willst du zuerst den kürzesten Weg? Der Schnellstart bringt einen Stack auf einer einzigen Seite bis zum zugestellten Testereignis; komm danach hierher zurück, um den Artikel zu speichern. Wenn du jedes Detail brauchst, findest du es auf zwei Begleitseiten: der Payload-Referenz und der Fehlerbehebung.
Du weißt schon, was ein Webhook ist? Spring zur Einrichtung.
Was ist ein Webhook?
Ein Webhook ist die einfachste Art, wie zwei Anwendungen miteinander reden: Wenn in der einen etwas passiert, schickt sie eine Nachricht an eine Web-Adresse deiner Wahl.
Stell dir einen Lieferdienst vor. Du gibst GrowWriter deine Adresse (eine URL auf deiner Website). Sobald ein Artikel fertig ist, klopft GrowWriter an diese Tür und übergibt den kompletten Artikel als strukturierte Daten. Den Rest erledigt deine Website: speichern, formatieren, auf deine Art veröffentlichen.
Warum das besser ist als die Alternativen:
- Kein Copy-Paste. Der Artikel kommt mit Titel, Inhalt, Bildern und SEO-Feldern fertig sortiert an.
- Kein ständiges Nachschauen. Deine Website muss nicht alle paar Minuten fragen, ob es etwas Neues gibt. GrowWriter meldet sich nur, wenn es etwas zu liefern gibt.
- Keine Plattform-Bindung. Webhooks sprechen reines HTTPS und JSON, und das versteht jeder Web-Stack.
So funktioniert es
Der ganze Ablauf besteht aus drei Schritten:
- Du gibst GrowWriter die Adresse eines Endpoints auf deiner Website (eine URL, die Anfragen entgegennimmt, bedient von einem kleinen Stück Code, das wir den Empfänger nennen), und GrowWriter gibt dir dafür einen geheimen Schlüssel.
- Wenn du einen Artikel veröffentlichst, sendet GrowWriter eine
POST-Anfrage an diesen Endpoint. Der Anfrage-Body ist der komplette Artikel als JSON, und die Anfrage ist mit deinem Schlüssel signiert, damit du sicher sein kannst, dass sie wirklich von GrowWriter kommt. - Deine Website prüft die Signatur, speichert den Artikel und antwortet mit
einem Erfolgsstatus (irgendein
2xx), optional mit der URL des neuen Beitrags, damit GrowWriter darauf verlinken kann. Eine Weiterleitung (3xx) ist kein Erfolg. Fertig.
Warum die Signatur und der Schlüssel?
Dein Endpoint ist eine öffentliche URL. Jeder im Internet kann eine
POST-Anfrage an ihn senden, und ein gefälschter „Artikel" von einem Fremden
sähe genauso aus wie ein echter. Über die Signatur hältst du beide
auseinander. Hier die ganze Idee in vier Schritten:
- Der Schlüssel ist ein Passwort, das nur du und GrowWriter kennen. Du bekommst ihn einmal, bei der Einrichtung, und legst ihn auf deinem Server ab. Er reist nie in den Anfragen mit.
- GrowWriter stempelt jede Anfrage mit einem Fingerabdruck. Vor dem Senden berechnet es einen HMAC: einen Hash aus dem Anfrage-Body und der aktuellen Zeit, gemischt mit deinem Schlüssel. Nur wer den Schlüssel besitzt, kann diesen Fingerabdruck erzeugen.
- Dein Endpoint rechnet nach und vergleicht. Du führst dieselbe
Berechnung über den Body aus, den du empfangen hast. Stimmt dein Ergebnis
mit dem Fingerabdruck im Header
X-GrowWriter-Signatureüberein, sind zwei Dinge auf einmal bewiesen: Die Anfrage kommt wirklich von GrowWriter, und niemand hat den Body unterwegs verändert. - Der Zeitstempel blockt Replays. Die Sendezeit steckt mit im Fingerabdruck, ein Angreifer kann eine echte Anfrage also nicht aufzeichnen und später erneut senden. Lehne alles ab, was älter als fünf Minuten ist, und eine abgefangene Anfrage verfällt, bevor sie wiederverwendet werden kann.
Du musst nichts davon selbst bauen. Jedes Code-Beispiel unten erledigt alle vier Teile. Dieser Abschnitt erklärt nur, wofür dieser Code da ist.
Schritt für Schritt einrichten
Verbinde den Webhook in GrowWriter
Öffne die Seite Veröffentlichen in GrowWriter und suche die Karte Webhook in der Gruppe Automatisierung. Klicke auf Verbinden.

Gib deine Endpoint-URL ein. Das ist die Adresse auf deiner Website, die
die Artikel empfängt, zum Beispiel https://example.com/webhooks/growwriter.
Drei Regeln: Sie muss mit https:// beginnen, sie darf keinen Benutzernamen
und kein Passwort enthalten, und ihr Host muss aus dem Internet erreichbar
sein (localhost und private Netzwerkadressen werden abgelehnt).
Der Endpoint existiert noch gar nicht? Kein Problem. GrowWriter prüft die Adresse, ruft sie an dieser Stelle aber nicht auf. Du kannst also erst verbinden und direkt danach den Code schreiben.
Nutze exakt die URL, unter der deine Website antwortet, ohne Weiterleitung
Leitet dein Host example.com auf www.example.com weiter (oder
umgekehrt), gib die Adresse ein, zu der er weiterleitet. GrowWriter
folgt nie einer Weiterleitung, eine weiterleitende Adresse lässt also jede
Zustellung fehlschlagen. Nicht sicher, welche deine Website nutzt? Prüfe
es in der Fehlerbehebung.
Du kannst die Adresse später mit URL bearbeiten in der Webhook-Zeile
ändern, und dein Schlüssel bleibt gleich.
Klicke auf Verbindung erstellen. Die Webhook-Zeile zeigt Nicht bestätigt, bis das erste Testereignis oder die erste Veröffentlichung gelingt.

Kopiere deinen Signaturschlüssel
Der Dialog zeigt jetzt deinen Signaturschlüssel. Er beginnt mit whsec_
und ist das gemeinsame Passwort aus Warum die Signatur und der
Schlüssel?: der Schlüssel, mit dem dein Code prüft, dass
eine Anfrage wirklich von GrowWriter stammt.
Klicke auf Schlüssel kopieren und lege ihn sicher auf deinem Server ab, üblicherweise als Umgebungsvariable:
GROWWRITER_WEBHOOK_SECRET=whsec_your_secret_hereEin Detail, über das viele stolpern: Wenn dein Code eine Signatur prüft, ist
der Schlüssel der komplette String, whsec_-Präfix inklusive. Schneide
das Präfix nicht ab. Und falls du den Wert in deiner Konfiguration validieren
willst: Ein Schlüssel ist immer whsec_ gefolgt von 43 URL-sicheren
Base64-Zeichen.
Dieser Schlüssel wird nur ein einziges Mal angezeigt
GrowWriter speichert ihn verschlüsselt und kann ihn nicht erneut anzeigen. Falls du ihn verlierst, trenne den Webhook und verbinde ihn neu, um einen frischen Schlüssel zu bekommen. Neu verbinden erzeugt immer einen neuen Schlüssel, und der alte funktioniert nicht mehr.

Baue deinen Endpoint
Ab hier ist das Entwicklerarbeit. Schick diese Seite der Person, die deine Website betreibt; sie braucht den Schlüssel aus Schritt 2. Du kommst bei Schritt 4 zurück, um das Testereignis zu senden.
Der Endpoint ist ein kleines Stück Code, der Empfänger, unter der URL aus
Schritt 1. Er liest die Anfrage exakt so, wie sie angekommen ist, prüft die
Signatur, antwortet mit einem 2xx-Status und speichert den Artikel. Kennt
er bis dahin die öffentliche URL des Beitrags, kann er sie als
{"url": "https://…"} zurückgeben, und GrowWriter zeigt im
Veröffentlichen-Dialog einen Link Beitrag ansehen.
Nicht sicher, welches Beispiel du nehmen sollst? Läuft deine Website auf Next.js, Vercel oder Netlify, starte mit dem Tab Next.js. Betreibst du einen eigenen Server, wähle die Sprache, in der er geschrieben ist. Kopiere die Datei aus den Code-Beispielen weiter unten, lege den Schlüssel aus Schritt 2 in den Umgebungsvariablen deines Hosting-Anbieters ab (Einstellungen, die dein Host außerhalb deines Codes aufbewahrt) und deploye, damit die Datei unter deiner Endpoint-URL antwortet.
Jedes Beispiel antwortet bereits auf das Testereignis. Beim Speichern des Artikels kommt dein eigenes Setup ins Spiel: Nutzt deine Website ein CMS oder eine Datenbank, sagt dir Speichere den Artikel in deinem CMS, was du vorbereiten musst.
Sende ein Testereignis
Zurück im GrowWriter-Dialog klickst du auf Testereignis senden. GrowWriter
schickt einen kleinen, signierten ping an deinen Endpoint. Er sieht so aus:
{
"version": "2026-09-06",
"event": "ping",
"deliveryId": "5f0c9a1e-4d2b-4f6a-9c3e-8b7d6a5e4f3c",
"sentAt": "2026-09-01T09:30:00.000Z"
}Beachte: Es ist kein Artikel enthalten. Daran erkennt dein Code den Unterschied zwischen Test und echter Lieferung.
GrowWriter signiert den Ping genau wie eine echte Lieferung und sendet ihn
auch genauso: bis zu drei Versuche, die jeweils 10 Sekunden auf irgendein
2xx warten, ein Endpoint, der nie antwortet, braucht also etwa eine halbe
Minute, bis er fehlschlägt. Antwortet dein Endpoint, zeigt der Dialog
Testereignis zugestellt. Klicke auf Fertig und du bist verbunden.

Schlägt der Test fehl, nennt der Dialog den Grund. Deine Endpoint-URL
leitet weiter. Gib die Adresse ein, zu der sie weiterleitet, und versuche es
erneut bedeutet, dass deine Adresse GrowWriter woandershin schickt, und
die Meldung zeigt die Adresse, die du stattdessen nutzen sollst. Dein
Endpoint hat die Zustellung abgelehnt zitiert den Status, den dein
Endpoint zurückgegeben hat, und die ersten 200 Zeichen seines Antwort-Bodys,
ein 404 oder ein 500 mit einer kurzen Fehlermeldung erklärt sich also
von selbst. Zustellung nach mehreren Versuchen fehlgeschlagen bedeutet,
dass GrowWriter gar keine Verbindung aufbauen konnte: Die Adresse ist nicht
öffentlich, oder die Website ist down. Jede Meldung hat einen Eintrag auf der
Seite zur Fehlerbehebung. Ein
fehlgeschlagener Test macht die Verbindung nie kaputt, du kannst es also so
oft probieren, wie du willst.
Veröffentliche einen Artikel
Öffne einen deiner fertigen Artikel und veröffentliche ihn. Wähle deinen
Webhook als Kanal, und GrowWriter liefert den kompletten Artikel als
article.published-Ereignis an deinen Endpoint.

Veröffentlichst du denselben Artikel später erneut (zum Beispiel nach einer
Überarbeitung), erhält dein Endpoint stattdessen ein
article.updated-Ereignis mit derselben Artikel-id. Über diese id weiß
deine Website, dass sie den bestehenden Beitrag aktualisieren soll, statt ein
Duplikat anzulegen. „Updated" heißt einfach, dass eine frühere
Veröffentlichung an diesen Webhook erfolgreich war; eine zweite
Webhook-Verbindung bekommt wieder article.published, weil sie den Artikel
noch nie gesehen hat.
Löschst du den Artikel später in GrowWriter oder trennst du den Webhook,
erhält dein Endpoint article.unpublished mit nur id, slug und
language des Artikels. Ob du den Beitrag versteckst oder löschst, ist deine
Entscheidung; die Beispiele unten loggen es.
Code-Beispiele
Wähle den Tab, der zu deinem Stack passt. Jedes Beispiel prüft die Signatur (die vier Schritte aus Warum die Signatur und der Schlüssel?), antwortet auf den Test-Ping, überspringt schon verarbeitete Lieferungen und übergibt den Artikel an eine Speicherfunktion in der Form, die ein CMS erwartet. Die Beispiele für Next.js und Express loggen den Beitrag, weil Serverless-Hosts deinem Code ein schreibgeschütztes Dateisystem geben; die Beispiele für Flask, PHP, Rails und Go schreiben eine JSON-Datei pro Artikel. Tausche beides gegen deinen eigenen Datenbankaufruf. Die Gründe hinter jeder Zeile stehen in Speichere den Artikel in deinem CMS.
Zwei Details, die dir im Code begegnen. Die Beispiele lehnen jede Lieferung ab, deren Zeitstempel älter als fünf Minuten ist: Das ist das Replay-Fenster aus dem Abschnitt oben, streng genug, um Replays nutzlos zu machen, und locker genug, um Uhrenabweichungen zwischen Servern abzufedern. Und sie vergleichen Signaturen in konstanter Zeit: Der Vergleich dauert immer gleich lang, egal ob ein Rateversuch fast stimmt oder komplett daneben liegt, ein Angreifer lernt also nichts, wenn er Antwortzeiten misst.
Eine Regel ist wichtiger als alle anderen: Prüfe die Signatur über den rohen Anfrage-Body, exakt so, wie er angekommen ist. Wenn dein Framework das JSON zuerst parst und du es neu serialisierst, ändern sich die Bytes und die Signaturprüfung schlägt fehl. Genau deshalb liest jedes Beispiel zuerst den rohen Body.
import { createHmac, timingSafeEqual } from 'node:crypto';
// Wie viele Sekunden eine Lieferung alt sein darf, bevor wir sie ablehnen.
// Schützt davor, dass jemand eine alte abgefangene Anfrage wieder einspielt.
const MAX_AGE_SECONDS = 5 * 60;
// Liefer-Ids, die wir fertig verarbeitet haben. Reicht für einen Server;
// in Produktion nimm stattdessen eine Tabelle in deiner Datenbank. Führt
// dein Host deinen Code als kurzlebige Funktionen aus (Serverless),
// überlebt diese Liste nicht zwischen zwei Anfragen.
const processedDeliveries = new Set<string>();
function verifySignature(rawBody: string, signatureHeader: string, secret: string) {
// Der Header sieht so aus: t=1756718400,v1=5257a869e7...
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => part.split('='))
);
// 1. Der Zeitstempel muss frisch sein. GrowWriter signiert jeden
// Wiederholungsversuch neu, eine frische Lieferung trägt also immer
// einen frischen Zeitstempel.
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!(age < MAX_AGE_SECONDS)) {
return false;
}
// 2. Berechne die Signatur nach: HMAC-SHA256 über "zeitstempel.rawBody",
// mit deinem KOMPLETTEN Signaturschlüssel (whsec_-Präfix inklusive).
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
// 3. Vergleiche in konstanter Zeit, damit die Prüfung nicht messbar ist.
const provided = Buffer.from(parts.v1 ?? '', 'hex');
const wanted = Buffer.from(expected, 'hex');
return provided.length === wanted.length && timingSafeEqual(provided, wanted);
}
// Bringt den Payload in die Form, die ein CMS erwartet. Jede Zeile wendet
// eine der sechs Regeln aus "Speichere den Artikel in deinem CMS" unten an.
function toPost(article: any) {
const images = [article.coverImage, ...article.images].filter(Boolean);
return {
growwriter_id: article.id, // Regel 1: ein eigenes Feld, nie ein loser Schlüssel
title: article.title,
slug: article.slug,
summary: article.summary ?? '', // fehlt im Payload, wenn nicht gesetzt
body: article.content.markdown, // Regel 6: escapen, wenn du es als MDX kompilierst
published_at: article.dates.firstPublishedAt.slice(0, 10), // Regel 2: YYYY-MM-DD
updated_at: article.dates.modifiedAt.slice(0, 10),
status: 'published', // Regel 3: der Payload hat kein Veröffentlicht-Flag
seo_keyword: article.keyword ?? '', // Regel 4: das Keyword ist ein SEO-Feld, kein Tag
faq: article.relatedQuestions, // Regel 4: FAQ-Material, keine Tags
image_urls: images.map((image: any) => image.url), // Regel 5: herunterladen und verkleinern
};
}
// Speichere den Beitrag in deiner Datenbank oder deinem CMS. Dieses Beispiel
// loggt ihn nur, mit Absicht: Vercel und die meisten Serverless-Hosts mounten
// das Code-Verzeichnis schreibgeschützt, eine Datei hier zu schreiben würde
// also die erste echte Lieferung mit einem 500 abstürzen lassen. Was immer du
// speicherst, verwende article.id als Schlüssel: Wird derselbe Artikel erneut
// veröffentlicht ("article.updated"), überschreibst du denselben Datensatz.
// Dieses Anlegen-oder-Aktualisieren ist alles, was „Upsert" bedeutet. Die
// Funktion gibt die öffentliche URL des Beitrags zurück, damit der Handler
// sie an GrowWriter zurückreichen kann.
async function saveArticle(article: any, event: string) {
const post = toPost(article);
const verb = event === 'article.published' ? 'Created' : 'Updated';
console.log(`${verb} ${post.growwriter_id}: ${post.title}`);
return `https://www.example.com/blog/${post.slug}`;
}
export async function POST(request: Request) {
// Lies den Body ZUERST als rohen Text. Verwende request.json() nicht vor
// der Prüfung: Die Signatur deckt genau diese Bytes ab.
const rawBody = await request.text();
const signature = request.headers.get('x-growwriter-signature') ?? '';
const secret = process.env.GROWWRITER_WEBHOOK_SECRET ?? '';
if (!verifySignature(rawBody, signature, secret)) {
return new Response('Invalid signature', { status: 401 });
}
const payload = JSON.parse(rawBody);
// Der Button "Testereignis senden" schickt einen Ping ohne Artikel.
// Die 200-Antwort hier lässt den Test gelingen.
if (payload.event === 'ping') {
return new Response('pong', { status: 200 });
}
// Den Artikel in GrowWriter zu löschen (oder den Webhook zu trennen) sendet
// article.unpublished mit nur id, slug und language. Verstecke oder lösche
// deinen Beitrag hier; dieses Beispiel loggt es nur.
if (payload.event === 'article.unpublished') {
console.log(`Unpublished ${payload.article.id}`);
return new Response('OK', { status: 200 });
}
// Wiederholungen nutzen dieselbe Liefer-Id. Ist diese Id schon erledigt,
// ist der Artikel gespeichert: Antworte einfach wieder OK und spar dir
// die Arbeit.
const deliveryId = request.headers.get('x-growwriter-delivery') ?? '';
if (processedDeliveries.has(deliveryId)) {
return new Response('OK', { status: 200 });
}
// payload.event sagt dir den Fall: "article.published" für einen neuen
// Artikel, "article.updated", wenn eine bekannte id wiederkommt.
const postUrl = await saveArticle(payload.article, payload.event);
processedDeliveries.add(deliveryId);
// GrowWriter wartet höchstens 10 Sekunden pro Versuch. Ein Schreibvorgang
// in die Datenbank passt locker; dauert dein Speichern länger, sieh dir
// "Was ist mit dem 10-Sekunden-Limit?" weiter unten an. Die URL des
// Beitrags zurückzugeben ist optional: Sie gibt dem Veröffentlichen-Dialog
// einen Link "Beitrag ansehen". Ein schlichtes 'OK' reicht, wenn du die
// URL noch nicht kennst.
return Response.json({ url: postUrl }, { status: 200 });
}Nutzt du einen anderen Stack?
Die Beispiele lassen sich in jede Sprache übertragen, die einen HMAC berechnen kann. Der einzige Schritt, den viele falsch machen, ist der erste: Prüfe die Signatur über den rohen Anfrage-Body, exakt so, wie er angekommen ist. Hier findest du ihn in anderen gängigen Stacks:
| Stack | Roher Anfrage-Body |
|---|---|
| Django | request.body |
| Laravel | $request->getContent() |
| Sinatra | request.body.read |
| Cloudflare Workers | await request.text() |
| Deno / Bun | await request.text() |
Was ist mit dem 10-Sekunden-Limit?
GrowWriter wartet pro Versuch bis zu 10 Sekunden auf deine Antwort. Einen JSON-Datensatz zu speichern passt bequem hinein. Macht dein Endpoint langsamere Arbeit, etwa jedes Bild vor dem Speichern herunterladen, hast du zwei Möglichkeiten:
- Verlass dich auf die Wiederholungen (empfohlen für einfache Endpoints).
Lass den Handler genau so, wie er hier steht: Er legt über
article.idan oder aktualisiert und überspringt erledigte Liefer-Ids, ihn zweimal laufen zu lassen ist also harmlos. Läuft der erste Versuch in den Timeout, arbeitet dein Server weiter. Die Wiederholung findet die Arbeit erledigt vor oder macht sie erneut und antwortet innerhalb des Fensters. Nichts geht verloren, und du schreibst keine zusätzliche Zeile Code. - Erst antworten, später arbeiten. Speichere den rohen Payload, antworte
sofort mit
200und verarbeite ihn in einem Hintergrund-Job. Das schlägt die Uhr immer, aber kenne den Preis: Sobald du200antwortest, gilt die Lieferung als erfolgreich. Schlägt dein Hintergrund-Job danach fehl, wiederholt GrowWriter nichts, und du bekommst den Artikel nur wieder, indem du ihn erneut veröffentlichst. Und weil der Beitrag noch nicht existiert, wenn du antwortest, kannst du seine URL nicht zurückgeben; der Veröffentlichen-Dialog zeigt für diese Veröffentlichung also keinen Link Beitrag ansehen.
Speichere den Artikel in deinem CMS
Der Payload hat die Form von GrowWriter. Dein CMS, deine Datenbank oder dein Static-Site-Generator hat eine eigene Form, und die meisten prüfen sie: Ein Schema ist die Liste der Felder, die ein Inhaltstyp erlaubt, und das Format, das jedes Feld akzeptiert. Der Code oben übersetzt zwischen beiden. Dieser Abschnitt erklärt die sechs Regeln dahinter, damit du sie an dein Setup anpassen kannst.
Bevor die erste Lieferung ankommt, öffne die Verwaltung deines CMS und beantworte fünf Fragen:
- Wo soll die GrowWriter-id leben?
- Welches Format akzeptieren deine Datumsfelder?
- Welches Feld markiert einen Beitrag als veröffentlicht, und was ist sein Standardwert für neue Inhalte?
- Wohin sollen Keywords, falls überhaupt?
- Wo werden Bilder gespeichert?
Regel 1: Gib der GrowWriter-id ein eigenes Feld
Was. article.id besteht aus 25 Kleinbuchstaben und Ziffern und beginnt
mit c. Sie ist sicher in URLs und Dateinamen und ändert sich nie.
Warum. Die meisten CMS lehnen Felder ab, die sie nicht deklarieren.
Schreibst du die id als losen Schlüssel neben den Titel, ist der Beitrag in
der Verwaltung kaputt, auch wenn die Website ihn noch rendert (der gemeldete
Fehler sah so aus: Key on object value "growwriterId" is not allowed).
So geht's. Lege vor der ersten Lieferung ein eigenes Feld wie
growwriter_id in deinem Inhaltstyp an, oder führe eine separate Tabelle,
die ids deinen Beiträgen zuordnet. Benenne Dateien nach der id, nie nach dem
Slug.
Regel 2: Wandle die Datumswerte um
Was. Jedes Datum ist ISO 8601 in UTC mit Millisekunden, etwa
2026-09-02T11:29:14.967Z.
Warum. Reine Datumsfelder im CMS akzeptieren YYYY-MM-DD und lehnen
einen vollen Zeitstempel ab.
So geht's. Nimm für reine Datumsfelder die ersten zehn Zeichen. Nutze
dates.firstPublishedAt als Veröffentlichungsdatum und dates.modifiedAt
als Änderungsdatum. modifiedAt ist der Zeitpunkt der
Veröffentlichungsanfrage, nicht der letzten Bearbeitung. Vergleiche die
beiden Daten also nie, um ein Update zu erkennen: Das sagt dir das event.
Regel 3: Setze den Veröffentlichungsstatus selbst
Was. Der Payload hat kein Veröffentlicht- oder Entwurfs-Flag. Das Ereignis ist das Flag, und es ist weg, sobald die Anfrage endet.
Warum. Viele CMS behandeln neue Inhalte standardmäßig als Entwurf. Ein Beitrag, der nur live war, weil dein Build diesen Standard ignoriert hat, geht offline, sobald ihn jemand in der Verwaltung speichert.
So geht's. Schreibe deinen Veröffentlichungsstatus explizit
(status: "published", draft: false oder was dein CMS nutzt), sowohl bei
article.published als auch bei article.updated. Kommt
article.unpublished an (der Artikel wurde in GrowWriter gelöscht oder der
Webhook getrennt), setze diesen Status zurück oder lösche den Beitrag; der
Payload trägt nur id, slug und language, die Entscheidung liegt also bei dir.
Regel 4: Das Keyword und die Fragen sind keine Tags
Was. article.keyword ist der eine Suchbegriff, für den der Artikel
geschrieben wurde, so wie der Autor ihn eingetippt hat. Er kann fehlen.
article.relatedQuestions ist die Liste der Fragen, die Google zu diesem
Keyword als „Ähnliche Fragen" zeigt, in der Sprache des Artikels; sie ist
immer vorhanden und kann leer sein.
Warum. Ein Suchbegriff ist kein Tag, und eine Frage ist keine Kategorie. Als Tags gerendert bringen sie Suchphrasen auf deine Live-Seiten; als FAQ unter dem Artikel verdienen sich die Fragen ihren Platz.
So geht's. Speichere das Keyword für dein SEO-Tooling. Zeige die Fragen
als FAQ-Block, oder als strukturierte FAQPage-Daten, wenn dein Renderer das
unterstützt. Willst du Tags, lege sie in deinem CMS fest.
Regel 5: Lade die Bilder herunter und optimiere sie
Was. Bild-URLs zeigen auf den öffentlichen Speicher von GrowWriter. Sie laufen derzeit nicht ab, aber GrowWriter ist kein dauerhafter Bild-Host. Dateien kommen so an, wie sie geholt wurden, in Originalauflösung, und ein einzelnes Bild kann größer als 10 MB sein.
Warum. Originale mit mehreren Megabyte zu committen oder auszuliefern bremst jede Seite.
So geht's. Lade jedes Bild herunter (Titelbild und Inline-Bilder),
verkleinere es auf die größte Breite, die dein Layout braucht, komprimiere es
und speichere es auf deiner Seite. Schreibe dann die URLs in dem
Inhaltsformat um, das du speicherst. Zwei Dinge machen das leicht:
Unveränderte Bilder behalten über Updates hinweg exakt ihre URLs, du kannst
also alles überspringen, was du schon gespiegelt hast, und dieselben URLs
stehen auch im Inhalt. Benenne deine Dateien nach article.id und der
Position des Bildes; images[].filename ist optional und hat keine Endung,
und sectionSlug sagt nur, für welchen Gliederungsabschnitt das Bild
erstellt wurde, also parse ihn nicht und nutze ihn nicht als Schlüssel.
let html = article.content.html;
for (const image of [article.coverImage, ...article.images].filter(Boolean)) {
const localUrl = await mirrorImage(image.url); // herunterladen + in deinem Speicher ablegen
html = html.replaceAll(image.url, localUrl);
}Regel 6: Escape für deinen Renderer
Was. Normale Prosa enthält Zeichen wie {, } und <.
Warum. MDX, JSX und Template-Engines lesen sie als Code, und der Build bricht beim ersten Artikel ab, der sie enthält.
So geht's. Speichere das Markdown als Daten und rendere es mit einem
Markdown-Renderer, oder escape diese Zeichen vor dem Kompilieren.
content.html ist bereits escaped und nutzt einen festen Satz an Tags, es
ist also die sichere Wahl zum direkten Einfügen.
Updates und Übersetzungen behandeln
- Updates: gleiche
id, überschreibe den Beitrag. Den Titel zu bearbeiten behält den Slug; nur einen neuen Titel im Writer zu wählen erzeugt einen neuen. Ändert er sich doch, aktualisiere die Adresse des Beitrags und erwäge eine Weiterleitung von der alten. Ersetzte Bilder bekommen neue URLs. Behandle die neueste Lieferung als maßgeblich für jedes Feld. - Übersetzungen: Jede Sprache ist ein eigener Artikel mit eigener
id. GrowWriter verknüpft sie nicht. Paare Sprachen mit deinen eigenen Daten, überarticle.language(ein Zwei-Buchstaben-Code wiees).
Lokal testen
Du musst keinen echten Artikel veröffentlichen, um deinen Endpoint
auszuprobieren. Dieses Snippet signiert einen Test-Ping mit openssl genau
so, wie GrowWriter es tut, und schickt ihn mit curl an deinen lokalen
Server:
# Nimm denselben Schlüssel, den dein lokaler Server in GROWWRITER_WEBHOOK_SECRET hat.
SECRET="whsec_your_secret_here"
BODY='{"version":"2026-09-06","event":"ping","deliveryId":"9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e","sentAt":"2026-09-01T09:30:00.000Z"}'
# Signiere "zeitstempel.body" mit HMAC-SHA256, genau wie GrowWriter.
TIMESTAMP=$(date +%s)
SIGNATURE=$(printf '%s.%s' "$TIMESTAMP" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -X POST "http://localhost:3000/webhooks/growwriter" \
-H "Content-Type: application/json" \
-H "X-GrowWriter-Event: ping" \
-H "X-GrowWriter-Delivery: 9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e" \
-H "X-GrowWriter-Signature: t=$TIMESTAMP,v1=$SIGNATURE" \
--data "$BODY"Passe Port und Pfad daran an, wo dein Server läuft. Ein funktionierender
Endpoint antwortet mit pong und einem 200. Ändere jetzt ein einzelnes
Zeichen in BODY und führe das Skript erneut aus: Du solltest ein 401
bekommen, was beweist, dass deine Signaturprüfung wirklich prüft. Um einen
kompletten Artikel durchzuspielen, ersetze BODY durch den
vollständigen Beispiel-Payload
(als eine einzige Zeile) und setze die beiden Event-Header auf
article.published.
Um echte Ereignisse von GrowWriter auf deinem Rechner zu empfangen, denk
daran, dass die Endpoint-URL öffentlich sein muss, localhost funktioniert
also nicht. Ein Tunnel-Dienst wie ngrok oder cloudflared gibt deinem lokalen
Server eine temporäre öffentliche HTTPS-Adresse; nutze sie während der
Entwicklung als Endpoint-URL und wechsle danach zur echten.
Was im Payload steckt
Jede Lieferung ist ein POST mit Content-Type: application/json, drei
X-GrowWriter-*-Headern (Ereignisname, Liefer-Id, Signatur) und einem
JSON-Body, der den Artikel in drei Formaten trägt. Die
Payload-Referenz ist der Vertrag: jeder
Header und jedes Feld mit Typ, ob es immer vorhanden ist, exaktem Format und
dem vollständigen Beispiel.
Die Felder, die die eigentliche Arbeit machen:
| Feld | So nutzt du es |
|---|---|
article.id | Dein stabiler Schlüssel. Speichere ihn in einem eigenen Feld und aktualisiere den bestehenden Beitrag, wenn dieselbe id wiederkommt. |
article.slug | Anzeigedaten für die Adresse des Beitrags. Er ändert sich, wenn im Writer ein neuer Titel gewählt wird, nutze ihn also nie als Schlüssel. |
article.language | Zwei-Buchstaben-Sprachcode (en, es, ...), um Beiträge auf einer mehrsprachigen Website in den richtigen Bereich zu leiten. Jede Übersetzung ist ein eigener Artikel mit eigener id. |
article.content | Der Artikel in drei Formaten: markdown, fertiges html und strukturierte blocks. Nimm, was zu deiner Website passt. |
article.dates | firstPublishedAt ist dein Veröffentlichungsdatum, modifiedAt dein Änderungsdatum. Nimm für reine Datumsfelder die ersten zehn Zeichen. |
article.coverImage und article.images | Titelbild plus jedes Inline-Bild in Reihenfolge. Lade sie herunter und verkleinere sie. |
article.keyword und article.relatedQuestions | Der Suchbegriff, auf den der Artikel zielt, und die Fragen, die Google damit verbindet. SEO-Feld und FAQ-Material; keins von beiden ist ein Tag. |
article.brand | Name und Website deiner Marke für das SEO-Markup, nie eine Person. Setze Autorenzeilen in deiner eigenen Speicherlogik. |
Neue optionale Felder können auftauchen, ohne dass sich das Datum in
version ändert, ignoriere also Felder, die du nicht kennst. Brechende
Änderungen setzen ein neues Datum und werden zuerst im
Changelog angekündigt; die vollständigen Regeln stehen unter
Versionierung.
Zustellung und Wiederholungen
Eine Veröffentlichung an eine Verbindung ist eine Lieferung, und eine
Lieferung macht bis zu drei Versuche (sofort, dann 1 Sekunde und 3 Sekunden
später), die jeweils bis zu 10 Sekunden auf irgendein 2xx warten. Alle
Versuche teilen sich eine Liefer-Id und werden frisch signiert,
Weiterleitungen werden nie befolgt, und GrowWriter wiederholt später nichts:
Nach dem dritten Fehlschlag zeigt der Kanal im Veröffentlichen-Dialog einen
Button Erneut versuchen, der eine neue Lieferung startet. Die exakten
Regeln, inklusive des Hinweises zur mindestens einmaligen Zustellung, auf den
sich deine Duplikaterkennung verlässt, stehen in der Referenz:
Zustellung.
Fehlerbehebung
Jeder Fehler hat einen eigenen Eintrag auf der Seite zur Fehlerbehebung, sortiert nach dem, was du siehst. Die häufigsten bei einer ersten Verbindung:
- Das Testereignis schlägt mit HTTP 301, 302, 303, 307 oder 308 fehl
- „Dein Endpoint hat nicht rechtzeitig geantwortet"
- „Dein Endpoint hat die Zustellung abgelehnt"
- Der Test klappt, aber der erste Artikel schlägt auf Vercel oder Netlify mit einem 500 fehl
- Der Test klappt, aber echte Artikel schlagen fehl
- Der Artikel ist live, aber meine CMS-Verwaltung zeigt einen Validierungsfehler
- Ich habe einen Artikel in GrowWriter gelöscht, aber er ist noch auf meiner Website
Kommst du nicht weiter? Schreib uns aus der App heraus, wir lösen das gemeinsam.
Changelog
- Aktuelle Version:
2026-09-06. Der in der Referenz beschriebene Payload ist der erste veröffentlichte Vertrag. Künftige Änderungen stehen hier, das Neueste zuerst: Ergänzende Änderungen behalten das Versionsdatum, Entfernungen oder Umbenennungen setzen ein neues und werden hier angekündigt, bevor sie ausgeliefert werden.
Webhook-Schnellstart
Verbinde GrowWriter mit deiner eigenen Website und lass dir dein erstes signiertes Testereignis zustellen, auf einer Seite, mit einer einzigen Code-Datei.
Webhook-Payload-Referenz
Jeder Header und jedes Feld, das GrowWriter an deinen Webhook-Endpoint sendet, mit Typ, ob es immer vorhanden ist, exaktem Format und dem, worauf du dich verlassen kannst.