Publier avec des webhooks
Envoie chaque article publié dans GrowWriter directement vers ton propre site web. Configure l'intégration webhook étape par étape, avec du code prêt à l'emploi.
Dernière mise à jour le
Ton site web ne tourne peut-être sur aucune des plateformes que GrowWriter connecte nativement. Aucun souci. Avec l'intégration webhook, GrowWriter livre chaque article directement sur ton site au moment où tu cliques sur Publier. Stack maison, CMS sur mesure, site statique, peu importe. S'il peut recevoir une requête web, il peut recevoir tes articles.
Ce guide t'accompagne dans toute la configuration. Aucune expérience des webhooks n'est nécessaire. Tu vas connecter le webhook dans GrowWriter, ajouter un petit bout de code à ton site web et le tester. À la fin, publier un article dans GrowWriter créera ou mettra à jour un billet sur ton propre site automatiquement. Il te faut juste un site web sur lequel tu peux ajouter du code.
Tu veux le chemin le plus court d'abord ? Le démarrage rapide amène une seule stack jusqu'à un événement de test livré, sur une seule page ; reviens ici pour enregistrer l'article. Quand tu as besoin de chaque détail, deux pages compagnes les ont : la référence du payload et le dépannage.
Tu sais déjà ce qu'est un webhook ? Passe à la configuration.
C'est quoi, un webhook ?
Un webhook est la façon la plus simple pour deux applications de se parler : quand quelque chose se passe dans l'une, elle envoie un message à une adresse web que tu choisis.
Imagine un service de livraison. Tu donnes ton adresse à GrowWriter (une URL de ton site web). Quand un article est prêt, GrowWriter frappe à cette porte et remet l'article complet sous forme de données structurées. Ton site fait le reste : l'enregistrer, le mettre en forme, le publier à ta façon.
Pourquoi c'est mieux que les alternatives :
- Zéro copier-coller. L'article arrive avec son titre, son contenu, ses images et ses champs SEO déjà organisés.
- Zéro vérification permanente. Ton site n'a pas besoin de demander « du nouveau ? » toutes les cinq minutes. GrowWriter n'appelle que lorsqu'il y a quelque chose à livrer.
- Zéro dépendance à une plateforme. Les webhooks parlent HTTPS et JSON purs, que toute stack web comprend.
Comment ça marche
Le flux complet tient en trois étapes :
- Tu donnes à GrowWriter l'adresse d'un endpoint sur ton site (une URL qui accepte des requêtes, servie par un petit bout de code qu'on appelle le récepteur), et GrowWriter te donne une clé secrète en échange.
- Quand tu publies un article, GrowWriter envoie une requête
POSTà cet endpoint. Le corps de la requête est l'article complet en JSON, et la requête est signée avec ton secret pour que tu puisses vérifier qu'elle vient bien de GrowWriter. - Ton site vérifie la signature, enregistre l'article et répond avec un
statut de succès (n'importe quel
2xx), en incluant si tu veux l'URL du nouveau billet pour que GrowWriter puisse y renvoyer. Une redirection (3xx) n'est pas un succès. C'est tout.
Pourquoi la signature et le secret ?
Ton endpoint est une URL publique. N'importe qui sur internet peut lui
envoyer une requête POST, et un faux « article » venu d'un inconnu
ressemblerait exactement à un vrai. La signature est ce qui te permet de les
distinguer. Voici toute l'idée en quatre étapes :
- Le secret est un mot de passe que seuls toi et GrowWriter connaissez. Tu le reçois une fois, pendant la configuration, et tu le stockes sur ton serveur. Il ne voyage jamais dans les requêtes.
- GrowWriter marque chaque requête d'une empreinte. Avant l'envoi, il calcule un HMAC : un hachage du corps de la requête et de l'heure courante, mélangés avec ton secret. Seul quelqu'un possédant le secret peut produire cette empreinte.
- Ton endpoint recalcule et compare. Tu refais le même calcul sur le
corps que tu as reçu. Si ton résultat correspond à l'empreinte du header
X-GrowWriter-Signature, deux choses sont prouvées d'un coup : la requête vient bien de GrowWriter, et personne n'a modifié le corps en chemin. - Le timestamp bloque les rejeux. L'heure d'envoi est intégrée à l'empreinte, un attaquant ne peut donc pas enregistrer une vraie requête et la renvoyer plus tard. Rejette tout ce qui est plus vieux que cinq minutes et une requête capturée expire avant de pouvoir être réutilisée.
Tu n'as rien de tout ça à construire toi-même. Chaque exemple de code ci-dessous fait les quatre parties. Cette section explique seulement à quoi sert ce code.
La configuration, étape par étape
Connecte le webhook dans GrowWriter
Ouvre la page Publier dans GrowWriter et repère la carte Webhook dans le groupe Automatisation. Clique sur Connecter.

Saisis ton URL de l'endpoint. C'est l'adresse de ton site web qui recevra
les articles, par exemple https://example.com/webhooks/growwriter. Trois
règles : elle doit commencer par https://, elle ne peut pas contenir de nom
d'utilisateur ni de mot de passe, et son hôte doit être accessible depuis
internet (localhost et les adresses de réseaux privés sont refusés).
L'endpoint n'existe pas encore ? Pas de problème. GrowWriter vérifie l'adresse mais ne l'appelle pas à ce stade, tu peux donc connecter d'abord et écrire le code juste après.
Utilise l'URL exacte sur laquelle ton site répond, sans redirection
Si ton hébergeur redirige example.com vers www.example.com (ou
l'inverse), saisis l'adresse vers laquelle il redirige. GrowWriter ne
suit jamais une redirection, une adresse qui redirige fait donc échouer
chaque livraison. Pas sûr de celle que ton site utilise ?
Vérifie-le dans le dépannage.
Tu pourras changer l'adresse plus tard avec Modifier l'URL sur la
ligne du webhook, et ton secret reste le même.
Clique sur Créer la connexion. La ligne du webhook affiche Non vérifié jusqu'à ce que le premier événement de test ou la première publication réussisse.

Copie ton secret de signature
La boîte de dialogue affiche maintenant ton Secret de signature. Il
commence par whsec_, et c'est le mot de passe partagé de
Pourquoi la signature et le secret ? : la clé que
ton code utilise pour vérifier qu'une requête vient réellement de
GrowWriter.
Clique sur Copier le secret et range-le en lieu sûr sur ton serveur, en général dans une variable d'environnement :
GROWWRITER_WEBHOOK_SECRET=whsec_your_secret_hereUn détail qui piège souvent : quand ton code vérifie une signature, la clé
est la chaîne entière, préfixe whsec_ inclus. Ne retire pas le préfixe.
Et si tu veux valider la valeur dans ta config, un secret est toujours
whsec_ suivi de 43 caractères base64 URL-safe.
Ce secret ne s'affiche qu'une seule fois
GrowWriter le stocke chiffré et ne peut pas le réafficher. Si tu le perds, déconnecte puis reconnecte le webhook pour en obtenir un nouveau. Se reconnecter crée toujours un nouveau secret, et l'ancien cesse de fonctionner.

Construis ton endpoint
À partir d'ici, c'est le travail d'un développeur. Envoie cette page à la personne qui s'occupe de ton site web ; elle a besoin du secret de l'étape 2. Tu reviens à l'étape 4 pour envoyer l'événement de test.
L'endpoint est un petit bout de code, le récepteur, à l'URL de l'étape 1. Il
lit la requête exactement telle qu'elle est arrivée, vérifie la signature,
répond avec un statut 2xx et enregistre l'article. S'il connaît déjà l'URL
publique du billet à ce moment-là, il peut la renvoyer sous la forme
{"url": "https://…"} et GrowWriter affiche un lien Voir l'article
dans la boîte de dialogue de publication.
Tu ne sais pas quel exemple choisir ? Si ton site tourne sur Next.js, Vercel ou Netlify, commence par l'onglet Next.js. Si tu fais tourner ton propre serveur, prends le langage dans lequel il est écrit. Copie le fichier depuis les exemples de code plus bas, mets le secret de l'étape 2 dans les variables d'environnement de ton hébergeur (des réglages que ton hébergeur garde en dehors de ton code), et déploie pour que le fichier réponde à l'URL de ton endpoint.
Chaque exemple répond déjà à l'événement de test. C'est dans l'enregistrement de l'article que ta propre configuration entre en jeu : si ton site utilise un CMS ou une base de données, Enregistre l'article dans ton CMS te dit quoi préparer.
Envoie un événement de test
De retour dans la boîte de dialogue GrowWriter, clique sur Envoyer un
événement de test. GrowWriter envoie un petit ping signé à ton endpoint.
Il ressemble à ceci :
{
"version": "2026-09-06",
"event": "ping",
"deliveryId": "5f0c9a1e-4d2b-4f6a-9c3e-8b7d6a5e4f3c",
"sentAt": "2026-09-01T09:30:00.000Z"
}Remarque : il n'y a pas d'article dedans. C'est comme ça que ton code distingue un test d'une vraie livraison.
GrowWriter signe le ping exactement comme une vraie livraison et l'envoie de
la même façon aussi : jusqu'à trois tentatives, chacune attendant 10 secondes
n'importe quelle réponse 2xx, un endpoint qui ne répond jamais met donc
environ une demi-minute à échouer. Quand ton endpoint répond, la boîte de
dialogue affiche Événement de test livré. Clique sur Terminé et te
voilà connecté.

Si le test échoue, la boîte de dialogue nomme la raison. L'URL de ton
endpoint redirige. Saisis l'adresse vers laquelle elle redirige et
réessaie veut dire que ton adresse envoie GrowWriter ailleurs, et le
message affiche l'adresse à utiliser à la place. Ton endpoint a rejeté la
livraison cite le statut renvoyé par ton endpoint et les 200 premiers
caractères du corps de sa réponse, un 404 ou un 500 avec un court
message d'erreur s'explique donc tout seul. Échec de la livraison après
plusieurs tentatives veut dire que GrowWriter n'a pas pu se connecter du
tout : l'adresse n'est pas publique, ou le site est hors ligne. Chaque
message a son entrée sur la
page de dépannage. Un test raté ne casse
jamais la connexion, tu peux donc réessayer autant de fois que tu veux.
Publie un article
Ouvre un de tes articles terminés et publie-le. Choisis ton webhook comme
canal et GrowWriter livrera l'article complet à ton endpoint sous forme
d'événement article.published.

Publie le même article plus tard (après des modifications, par exemple) et
ton endpoint recevra un événement article.updated avec le même id
d'article. C'est cet id qui permet à ton site de mettre à jour le billet
existant au lieu de créer un doublon. « Updated » veut simplement dire
qu'une publication précédente vers ce webhook a réussi ; une deuxième
connexion webhook reçoit à nouveau article.published, parce qu'elle n'a
jamais vu l'article.
Supprime l'article dans GrowWriter plus tard, ou déconnecte le webhook, et
ton endpoint recevra article.unpublished avec seulement l'id, le slug
et la language de l'article. Masquer ou supprimer le billet, c'est toi qui
décides ; les exemples ci-dessous se contentent de le logger.
Exemples de code
Choisis l'onglet qui correspond à ta stack. Chaque exemple vérifie la signature (les quatre étapes de Pourquoi la signature et le secret ?), répond au ping de test, ignore les livraisons déjà traitées et confie l'article à une fonction d'enregistrement de la forme qu'un CMS attend. Les exemples Next.js et Express loggent le billet, parce que les hébergeurs serverless donnent à ton code un système de fichiers en lecture seule ; les exemples Flask, PHP, Rails et Go écrivent un fichier JSON par article. Remplace l'un ou l'autre par ton propre appel à ta base de données. Les raisons derrière chaque ligne sont dans Enregistre l'article dans ton CMS.
Deux détails que tu verras dans le code. Les exemples rejettent toute livraison dont le timestamp est plus vieux que cinq minutes : c'est la fenêtre de rejeu de la section plus haut, assez stricte pour rendre les rejeux inutiles, assez souple pour absorber la dérive d'horloge entre serveurs. Et ils comparent les signatures en temps constant : la comparaison dure exactement aussi longtemps, que la tentative soit presque bonne ou complètement fausse, un attaquant ne peut donc rien apprendre en mesurant les temps de réponse.
Une règle compte plus que toutes les autres : vérifie la signature sur le corps brut de la requête, exactement tel qu'il est arrivé. Si ton framework parse le JSON d'abord et que tu le re-sérialises, les octets changent et la vérification de signature échoue. Chaque exemple lit le corps brut en premier exactement pour cette raison.
import { createHmac, timingSafeEqual } from 'node:crypto';
// Âge maximal d'une livraison, en secondes, avant de la rejeter.
// Protège contre le rejeu d'une ancienne requête capturée.
const MAX_AGE_SECONDS = 5 * 60;
// Ids des livraisons déjà traitées. Suffisant pour un seul serveur ;
// en production, utilise plutôt une table de ta base de données. Si ton
// hébergeur exécute ton code sous forme de fonctions éphémères
// (serverless), cette liste ne survit pas d'une requête à l'autre.
const processedDeliveries = new Set<string>();
function verifySignature(rawBody: string, signatureHeader: string, secret: string) {
// Le header ressemble à : t=1756718400,v1=5257a869e7...
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => part.split('='))
);
// 1. Le timestamp doit être récent. GrowWriter re-signe chaque nouvelle
// tentative, une livraison fraîche porte donc toujours un timestamp frais.
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!(age < MAX_AGE_SECONDS)) {
return false;
}
// 2. Recalcule la signature : HMAC-SHA256 de "timestamp.corpsBrut",
// avec ton secret de signature ENTIER comme clé (préfixe whsec_ inclus).
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
// 3. Compare en temps constant pour que la vérification ne soit pas mesurable.
const provided = Buffer.from(parts.v1 ?? '', 'hex');
const wanted = Buffer.from(expected, 'hex');
return provided.length === wanted.length && timingSafeEqual(provided, wanted);
}
// Transforme le payload en la forme qu'un CMS attend. Chaque ligne applique
// une des six règles de « Enregistre l'article dans ton CMS » plus bas.
function toPost(article: any) {
const images = [article.coverImage, ...article.images].filter(Boolean);
return {
growwriter_id: article.id, // règle 1 : un champ dédié, jamais une clé orpheline
title: article.title,
slug: article.slug,
summary: article.summary ?? '', // absent du payload quand non défini
body: article.content.markdown, // règle 6 : échappe si tu le compiles en MDX
published_at: article.dates.firstPublishedAt.slice(0, 10), // règle 2 : AAAA-MM-JJ
updated_at: article.dates.modifiedAt.slice(0, 10),
status: 'published', // règle 3 : le payload n'a pas d'indicateur publié
seo_keyword: article.keyword ?? '', // règle 4 : le mot-clé est un champ SEO, pas un tag
faq: article.relatedQuestions, // règle 4 : matière à FAQ, pas des tags
image_urls: images.map((image: any) => image.url), // règle 5 : télécharge et redimensionne
};
}
// Stocke le billet dans ta base de données ou ton CMS. Cet exemple se
// contente de le logger, volontairement : Vercel et la plupart des
// hébergeurs serverless montent le répertoire du code en lecture seule,
// écrire un fichier ici ferait donc planter la première vraie livraison
// avec un 500. Quoi que tu stockes, indexe-le sur article.id : quand le
// même article est publié à nouveau ("article.updated"), tu réécris le
// même enregistrement. Ce créer-ou-mettre-à-jour, c'est tout ce que veut
// dire « upsert ». Elle renvoie l'URL publique du billet pour que le
// handler puisse la retourner à GrowWriter.
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) {
// Lis le corps en texte brut EN PREMIER. N'utilise pas request.json()
// avant la vérification : la signature couvre exactement ces octets.
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);
// Le bouton « Envoyer un événement de test » envoie un ping sans article.
// Répondre 200 ici, c'est ce qui fait réussir le test.
if (payload.event === 'ping') {
return new Response('pong', { status: 200 });
}
// Supprimer l'article dans GrowWriter (ou déconnecter le webhook) envoie
// article.unpublished avec seulement l'id, le slug et la langue. Masque ou
// supprime ton billet ici ; cet exemple se contente de le logger.
if (payload.event === 'article.unpublished') {
console.log(`Unpublished ${payload.article.id}`);
return new Response('OK', { status: 200 });
}
// Les réessais réutilisent le même id de livraison. Si cet id est déjà
// traité, l'article est enregistré : réponds simplement OK et passe.
const deliveryId = request.headers.get('x-growwriter-delivery') ?? '';
if (processedDeliveries.has(deliveryId)) {
return new Response('OK', { status: 200 });
}
// payload.event indique le cas : "article.published" pour un nouvel
// article, "article.updated" quand un id déjà vu revient.
const postUrl = await saveArticle(payload.article, payload.event);
processedDeliveries.add(deliveryId);
// GrowWriter attend au maximum 10 secondes par tentative. Une écriture
// en base tient largement ; si ton enregistrement est plus lent, vois
// « Et la limite des 10 secondes ? » plus bas. Renvoyer l'URL du billet
// est optionnel : ça donne un lien « Voir l'article » à la boîte de
// dialogue de publication. Un simple 'OK' suffit si tu ne connais pas
// encore l'URL.
return Response.json({ url: postUrl }, { status: 200 });
}Tu utilises un autre stack ?
Les exemples se transposent dans n'importe quel langage capable de calculer un HMAC. La seule étape que les gens ratent est la première : vérifie la signature sur le corps brut de la requête, exactement tel qu'il est arrivé. Voici où le trouver dans d'autres stacks courantes :
| Stack | Corps brut de la requête |
|---|---|
| Django | request.body |
| Laravel | $request->getContent() |
| Sinatra | request.body.read |
| Cloudflare Workers | await request.text() |
| Deno / Bun | await request.text() |
Et la limite des 10 secondes ?
GrowWriter attend jusqu'à 10 secondes par tentative pour ta réponse. Enregistrer un fichier JSON tient largement dans ce délai. Si ton endpoint fait un travail plus lent, comme télécharger chaque image avant d'enregistrer, tu as deux options :
- Mise sur les réessais (recommandé pour les endpoints simples). Garde le
handler exactement comme montré : il crée ou met à jour sur
article.idet ignore les ids de livraison déjà traités, le lancer deux fois est donc sans danger. Si la première tentative expire, ton serveur continue de travailler. Le réessai trouve le travail fait, ou le refait, et répond dans la fenêtre. Rien n'est perdu, et tu n'écris pas une ligne de code en plus. - Réponds d'abord, travaille ensuite. Stocke le payload brut, réponds
200immédiatement et traite-le dans une tâche de fond. Tu bats toujours le chrono, mais connais la contrepartie : une fois que tu as répondu200, la livraison compte comme réussie. Si ta tâche de fond échoue ensuite, GrowWriter ne réessaie pas, et la seule façon de récupérer l'article est de le publier à nouveau. Et comme le billet n'existe pas encore quand tu réponds, tu ne peux pas renvoyer son URL, la boîte de dialogue de publication n'affiche donc aucun lien Voir l'article pour cette publication.
Enregistre l'article dans ton CMS
Le payload a la forme de GrowWriter. Ton CMS, ta base de données ou ton générateur de site statique a sa propre forme, et la plupart la vérifient : un schéma est la liste des champs qu'un type de contenu autorise et le format que chacun accepte. Le code ci-dessus traduit entre les deux. Cette section explique les six règles derrière lui, pour que tu puisses les adapter à ta configuration.
Avant ta première livraison, ouvre l'admin de ton CMS et réponds à cinq questions :
- Où vivra l'id GrowWriter ?
- Quel format acceptent tes champs de date ?
- Quel champ marque un billet comme publié, et quelle est sa valeur par défaut pour un nouveau contenu ?
- Où doivent aller les mots-clés, si quelque part ?
- Où seront stockées les images ?
Règle 1 : donne à l'id GrowWriter son propre champ
Quoi. article.id fait 25 lettres minuscules et chiffres, et commence
par c. Il est sûr dans les URLs et les noms de fichiers et ne change
jamais.
Pourquoi. La plupart des CMS rejettent les champs qu'ils ne déclarent
pas. Écrire l'id comme une clé orpheline à côté du titre casse le billet dans
l'admin, même si le site le rend encore (l'erreur signalée ressemblait à
Key on object value "growwriterId" is not allowed).
À faire. Ajoute un champ dédié comme growwriter_id à ton type de
contenu avant la première livraison, ou garde une table séparée qui associe
les ids à tes billets. Nomme les fichiers d'après l'id, jamais d'après le
slug.
Règle 2 : convertis les dates
Quoi. Chaque date est en ISO 8601, en UTC, avec les millisecondes, comme
2026-09-02T11:29:14.967Z.
Pourquoi. Les champs de date sans heure d'un CMS acceptent AAAA-MM-JJ
et refusent un timestamp complet.
À faire. Prends les dix premiers caractères pour les champs de date sans
heure. Utilise dates.firstPublishedAt comme date de publication et
dates.modifiedAt comme date de mise à jour. modifiedAt est l'heure de la
demande de publication, pas de la dernière modification, ne compare donc
jamais les deux dates pour détecter une mise à jour : l'event te le dit.
Règle 3 : définis toi-même l'état publié
Quoi. Le payload n'a pas d'indicateur publié ou brouillon. L'événement est l'indicateur, et il disparaît une fois la requête terminée.
Pourquoi. Beaucoup de CMS traitent un nouveau contenu comme un brouillon par défaut. Un billet qui était en ligne parce que ton build ignorait cette valeur par défaut passe hors ligne la première fois que quelqu'un l'enregistre dans l'admin.
À faire. Écris ton état publié explicitement (status: "published",
draft: false, ou ce que ton CMS utilise) sur article.published comme sur
article.updated. Quand article.unpublished arrive (l'article a été
supprimé dans GrowWriter, ou le webhook déconnecté), rebascule cet état ou
supprime le billet ; le payload ne porte que l'id, le slug et la langue, la
décision t'appartient donc.
Règle 4 : le mot-clé et les questions ne sont pas des tags
Quoi. article.keyword est le seul terme de recherche pour lequel
l'article a été écrit, tel que l'auteur l'a tapé. Il peut être absent.
article.relatedQuestions est la liste des questions que Google affiche
dans « Autres questions posées » pour ce mot-clé, dans la langue de
l'article ; elle est toujours présente et peut être vide.
Pourquoi. Un terme de recherche n'est pas un tag, et une question n'est pas une catégorie. Rendus comme des tags, ils mettent des expressions de recherche sur tes pages en ligne ; utilisées comme FAQ sous l'article, les questions méritent leur place.
À faire. Stocke le mot-clé pour tes outils SEO. Affiche les questions
comme un bloc FAQ, ou comme données structurées FAQPage si ton renderer le
permet. Si tu veux des tags, décide-les dans ton CMS.
Règle 5 : télécharge et optimise les images
Quoi. Les URLs d'images pointent vers le stockage public de GrowWriter. Elles n'expirent pas aujourd'hui, mais GrowWriter n'est pas un hébergeur d'images à long terme. Les fichiers arrivent tels que récupérés, à leur résolution d'origine, et une seule image peut dépasser 10 Mo.
Pourquoi. Commiter ou servir des originaux de plusieurs mégaoctets ralentit chaque page.
À faire. Télécharge chaque image (couverture et images du texte),
redimensionne-la à la largeur maximale dont ta mise en page a besoin,
compresse-la et stocke-la de ton côté. Puis réécris les URLs dans le format
de contenu que tu enregistres. Deux choses rendent ça facile : les images
inchangées gardent exactement leurs URLs d'une mise à jour à l'autre, tu
peux donc sauter tout ce que tu as déjà copié, et les mêmes URLs apparaissent
dans le contenu. Nomme tes fichiers d'après article.id et la position de
l'image ; images[].filename est optionnel et sans extension, et
sectionSlug dit seulement pour quelle section du plan l'image a été
créée, ne le parse donc pas et ne l'utilise pas comme clé.
let html = article.content.html;
for (const image of [article.coverImage, ...article.images].filter(Boolean)) {
const localUrl = await mirrorImage(image.url); // télécharge + enregistre dans ton stockage
html = html.replaceAll(image.url, localUrl);
}Règle 6 : échappe pour ton renderer
Quoi. Une prose ordinaire contient des caractères comme {, } et <.
Pourquoi. MDX, JSX et les moteurs de templates les lisent comme du code, et le build échoue sur le premier article qui les contient.
À faire. Stocke le markdown comme donnée et rends-le avec un renderer
Markdown, ou échappe ces caractères avant de compiler. content.html est
déjà échappé et utilise un jeu fixe de balises, c'est donc le choix sûr pour
une insertion directe.
Gérer les mises à jour et les traductions
- Mises à jour : même
id, réécris le billet. Modifier le titre garde le slug ; seul le choix d'un nouveau titre dans le writer en produit un nouveau. Quand il change, mets à jour l'adresse du billet et pense à rediriger l'ancienne. Les images remplacées reçoivent de nouvelles URLs. Considère la dernière livraison comme faisant foi pour chaque champ. - Traductions : chaque langue est un article séparé avec son propre
id. GrowWriter ne les relie pas. Associe les langues avec tes propres données, en utilisantarticle.language(un code à deux lettres commees).
Teste en local
Pas besoin de publier un vrai article pour exercer ton endpoint. Ce script
signe un ping de test exactement comme GrowWriter le fait, avec openssl, et
l'envoie à ton serveur local avec curl :
# Utilise le même secret que celui de ton serveur local dans GROWWRITER_WEBHOOK_SECRET.
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"}'
# Signe "timestamp.corps" en HMAC-SHA256, exactement comme 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"Adapte le port et le chemin à l'endroit où tourne ton serveur. Un endpoint
qui fonctionne répond pong avec un 200. Maintenant, change un seul
caractère dans BODY et relance : tu dois obtenir un 401, ce qui prouve
que ta vérification de signature vérifie vraiment. Pour répéter avec un
article complet, remplace BODY par le
payload d'exemple complet
(sur une seule ligne) et mets les deux headers d'événement à
article.published.
Pour recevoir de vrais événements de GrowWriter sur ta machine,
rappelle-toi que l'URL de l'endpoint doit être publique, localhost ne
fonctionnera donc pas. Un service de tunnel comme ngrok ou cloudflared donne
à ton serveur local une adresse HTTPS publique temporaire ; utilise-la comme
URL de l'endpoint pendant le développement, puis passe à la vraie.
Ce que contient le payload
Chaque livraison est un POST avec Content-Type: application/json, trois
headers X-GrowWriter-* (nom de l'événement, id de livraison, signature) et
un corps JSON qui porte l'article en trois formats. La
référence du payload est le contrat :
chaque header et chaque champ avec son type, sa présence ou non dans chaque
livraison, son format exact et
l'exemple complet.
Les champs qui font le gros du travail :
| Champ | Comment l'utiliser |
|---|---|
article.id | Ta clé stable. Stocke-la dans un champ dédié et mets à jour le billet existant quand le même id revient. |
article.slug | Donnée d'affichage pour l'adresse du billet. Il change quand un nouveau titre est choisi dans le writer, ne l'utilise donc jamais comme clé. |
article.language | Code de langue à deux lettres (fr, en, ...) pour router les billets sur un site multilingue. Chaque traduction est un article à part avec son propre id. |
article.content | L'article en trois formats : markdown, html prêt à l'emploi et blocks structurés. Prends celui qui convient à ton site. |
article.dates | firstPublishedAt est ta date de publication, modifiedAt ta date de mise à jour. Prends les dix premiers caractères pour les champs de date sans heure. |
article.coverImage et article.images | La couverture plus chaque image du texte dans l'ordre. Télécharge-les et redimensionne-les. |
article.keyword et article.relatedQuestions | Le terme de recherche que l'article vise, et les questions que Google lui associe. Champ SEO et matière à FAQ ; aucun des deux n'est un tag. |
article.brand | Le nom et le site web de ta marque pour le balisage SEO, jamais une personne. Définis les signatures d'auteur dans ta propre logique d'enregistrement. |
De nouveaux champs optionnels peuvent apparaître sans que la date version
change, ignore donc les champs que tu ne reconnais pas. Les changements
incompatibles font avancer la date et sont annoncés d'abord dans le
journal des modifications ; la politique complète est dans
Versionnage.
Livraison et réessais
Une publication vers une connexion égale une livraison, et une livraison
fait jusqu'à trois tentatives (immédiatement, puis 1 seconde et 3 secondes
plus tard), chacune attendant jusqu'à 10 secondes n'importe quelle réponse
2xx. Toutes les tentatives partagent un seul id de livraison et sont
signées à neuf, les redirections ne sont jamais suivies, et GrowWriter ne
réessaie pas plus tard : après le troisième échec, le canal affiche un
bouton Réessayer dans la boîte de dialogue de publication, qui démarre
une nouvelle livraison. Les règles exactes, y compris la note « au moins une
fois » sur laquelle repose ta déduplication, sont dans la référence :
Livraison.
Dépannage
Chaque échec a sa propre entrée sur la page de dépannage, organisée par ce que tu vois. Les plus fréquents lors d'une première connexion :
- L'événement de test échoue avec un HTTP 301, 302, 303, 307 ou 308
- « Ton endpoint n'a pas répondu à temps »
- « Ton endpoint a rejeté la livraison »
- Le test réussit, mais le premier article échoue avec un 500 sur Vercel ou Netlify
- Le test réussit mais les vrais articles échouent
- L'article est en ligne, mais l'admin de mon CMS affiche une erreur de validation
- J'ai supprimé un article dans GrowWriter, mais il est toujours sur mon site
Toujours bloqué ? Écris-nous depuis l'application et on trouve la solution ensemble.
Journal des modifications
- Version actuelle :
2026-09-06. Le payload décrit dans la référence est le premier contrat publié. Les changements futurs sont listés ici, du plus récent au plus ancien : les ajouts gardent la date de version, les retraits ou renommages la font avancer et sont annoncés ici avant d'être déployés.
Démarrage rapide des webhooks
Connecte GrowWriter à ton propre site web et fais livrer ton premier événement de test signé, sur une seule page, avec un seul fichier de code.
Référence du payload webhook
Chaque header et chaque champ que GrowWriter envoie à ton endpoint webhook, avec son type, sa présence, son format exact et ce sur quoi tu peux compter.