Docs

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 :

  1. 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.
  2. 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.
  3. 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.
Schéma en trois étapes : un clic sur Publier dans GrowWriter envoie une requête JSON signée que ton site web reçoit et enregistre comme billet publié.
Un clic dans GrowWriter, une requête vers ton site, un nouveau billet.

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 :

  1. 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.
  2. 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.
  3. 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.
  4. 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.

La page Canaux de GrowWriter avec la carte Webhook dans le groupe Automatisation et son bouton Connecter.
La carte Webhook se trouve sous Automatisation, sur la page Canaux.

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.

L'étape 1 de la fenêtre Connecter le webhook avec le champ URL de l'endpoint rempli et le bouton Créer la connexion.
Saisis l'adresse sur laquelle ton site répond, puis clique sur Créer la connexion.

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 :

.env
GROWWRITER_WEBHOOK_SECRET=whsec_your_secret_here

Un 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.

Étape 2 de la boîte de dialogue Connecter un webhook, avec le champ Secret de signature et les boutons Copier le secret et Envoyer un événement de test.
Copie le secret avant de fermer la boîte de dialogue. Il ne s'affiche qu'une fois.

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 :

Payload de l'événement de test
{
  "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é.

La page Canaux avec la ligne du webhook affichant le badge Connecté, Modifier l'URL et Déconnecter.
Après un événement de test livré, la ligne affiche Connecté, avec Modifier l'URL et Déconnecter.

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.

La boîte de dialogue Publier l'article avec le canal webhook coché et marqué Publiera un nouveau.
Coche ton webhook dans la boîte de dialogue Publier. La première publication envoie 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.

app/webhooks/growwriter/route.ts
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 :

StackCorps brut de la requête
Djangorequest.body
Laravel$request->getContent()
Sinatrarequest.body.read
Cloudflare Workersawait request.text()
Deno / Bunawait 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.id et 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 200 immé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épondu 200, 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 :

  1. Où vivra l'id GrowWriter ?
  2. Quel format acceptent tes champs de date ?
  3. Quel champ marque un billet comme publié, et quelle est sa valeur par défaut pour un nouveau contenu ?
  4. Où doivent aller les mots-clés, si quelque part ?
  5. 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é.

Copie les images, puis réécris le contenu
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 utilisant article.language (un code à deux lettres comme es).

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 :

send-test-ping.sh
# 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 :

ChampComment l'utiliser
article.idTa clé stable. Stocke-la dans un champ dédié et mets à jour le billet existant quand le même id revient.
article.slugDonné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.languageCode 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.contentL'article en trois formats : markdown, html prêt à l'emploi et blocks structurés. Prends celui qui convient à ton site.
article.datesfirstPublishedAt 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.imagesLa couverture plus chaque image du texte dans l'ordre. Télécharge-les et redimensionne-les.
article.keyword et article.relatedQuestionsLe 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.brandLe 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 :

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.

Sur cette page