Docs

Pubblicare con i webhook

Invia ogni articolo che pubblichi in GrowWriter direttamente al tuo sito web. Configura l'integrazione webhook passo dopo passo, con codice pronto all'uso.

Ultimo aggiornamento il

Il tuo sito web magari non gira su nessuna delle piattaforme che GrowWriter collega di serie. Nessun problema. Con l'integrazione webhook, GrowWriter consegna ogni articolo direttamente al tuo sito nel momento in cui premi Pubblica. Stack personalizzato, CMS fatto in casa, sito statico, qualsiasi cosa. Se può ricevere una richiesta web, può ricevere i tuoi articoli.

Questa guida ti accompagna in tutta la configurazione. Non serve esperienza con i webhook. Connetterai il webhook in GrowWriter, aggiungerai un piccolo pezzo di codice al tuo sito web e lo metterai alla prova. Alla fine, pubblicare un articolo in GrowWriter creerà o aggiornerà un post sul tuo sito web in automatico. Ti serve solo un sito web a cui puoi aggiungere codice.

Vuoi prima la strada più breve? L'avvio rapido porta un solo stack fino a un evento di prova consegnato, in una sola pagina; torna qui per salvare l'articolo. Quando ti serve ogni dettaglio, lo trovi in due pagine di accompagnamento: il riferimento del payload e la risoluzione dei problemi.

Sai già cos'è un webhook? Vai alla configurazione.

Che cos'è un webhook?

Un webhook è il modo più semplice che due applicazioni hanno per parlarsi: quando succede qualcosa in una, questa invia un messaggio a un indirizzo web che scegli tu.

Pensa a un servizio di consegne. Dai a GrowWriter il tuo indirizzo (un URL del tuo sito web). Quando un articolo è pronto, GrowWriter bussa a quella porta e consegna l'articolo completo come dati strutturati. Il tuo sito fa il resto: salvarlo, formattarlo, pubblicarlo a modo tuo.

Perché batte le alternative:

  • Niente copia e incolla. L'articolo arriva con titolo, contenuto, immagini e campi SEO già organizzati.
  • Niente controlli continui. Il tuo sito non deve chiedere "c'è qualcosa di nuovo?" ogni pochi minuti. GrowWriter chiama solo quando c'è qualcosa da consegnare.
  • Niente vincoli di piattaforma. I webhook parlano HTTPS e JSON puri, che ogni stack web capisce.

Come funziona

L'intero flusso è fatto di tre passaggi:

  1. Dai a GrowWriter l'indirizzo di un endpoint del tuo sito (un URL che accetta richieste, servito da un piccolo pezzo di codice che chiamiamo ricevitore), e GrowWriter ti dà in cambio una chiave segreta.
  2. Quando pubblichi un articolo, GrowWriter invia una richiesta POST a quell'endpoint. Il corpo della richiesta è l'articolo completo in JSON, e la richiesta è firmata con il tuo secret così puoi essere certo che arrivi davvero da GrowWriter.
  3. Il tuo sito verifica la firma, salva l'articolo e risponde con uno stato di successo (un 2xx qualsiasi), includendo se vuoi l'URL del nuovo post così GrowWriter può linkarlo. Un redirect (3xx) non è un successo. Fatto.
Diagramma di flusso in tre passaggi: un clic su Pubblica in GrowWriter invia una richiesta JSON firmata che il tuo sito web riceve e salva come post pubblicato.
Un clic in GrowWriter, una richiesta al tuo sito, un nuovo post.

Perché la firma e il secret?

Il tuo endpoint è un URL pubblico. Chiunque su internet può inviargli una richiesta POST, e un finto "articolo" mandato da uno sconosciuto avrebbe lo stesso aspetto di uno vero. La firma è ciò che ti permette di distinguerli. Ecco l'idea completa in quattro passaggi:

  1. Il secret è una password che conoscete solo tu e GrowWriter. Lo ricevi una volta sola, durante la configurazione, e lo conservi sul tuo server. Non viaggia mai dentro le richieste.
  2. GrowWriter timbra ogni richiesta con un'impronta. Prima dell'invio calcola un HMAC: un hash del corpo della richiesta e dell'ora corrente, mescolati con il tuo secret. Solo chi possiede il secret può produrre quell'impronta.
  3. Il tuo endpoint ricalcola e confronta. Esegui lo stesso calcolo sul corpo che hai ricevuto. Se il tuo risultato corrisponde all'impronta nell'header X-GrowWriter-Signature, sono dimostrate due cose in un colpo solo: la richiesta arriva davvero da GrowWriter e nessuno ha modificato il corpo lungo la strada.
  4. Il timestamp blocca i replay. L'ora di invio è incorporata nell'impronta, quindi un attaccante non può registrare una richiesta vera e rimandarla più tardi. Rifiuta tutto ciò che è più vecchio di cinque minuti e una richiesta catturata scade prima di poter essere riusata.

Non devi costruire nulla di tutto questo da solo. Ogni esempio di codice qui sotto fa tutte e quattro le parti. Questa sezione spiega solo a cosa serve quel codice.

Configurazione passo dopo passo

Connetti il webhook in GrowWriter

Apri la pagina Pubblica in GrowWriter e cerca la scheda Webhook nel gruppo Automazione. Fai clic su Connetti.

La pagina Canali di GrowWriter con la scheda Webhook nel gruppo Automazione e il suo pulsante Connetti.
La scheda Webhook si trova sotto Automazione, nella pagina Canali.

Inserisci il tuo URL dell'endpoint. È l'indirizzo del tuo sito web che riceverà gli articoli, per esempio https://example.com/webhooks/growwriter. Tre regole: deve iniziare con https://, non può contenere nome utente o password, e il suo host deve essere raggiungibile da internet (localhost e gli indirizzi di reti private vengono rifiutati).

Non hai ancora costruito l'endpoint? Nessun problema. GrowWriter controlla l'indirizzo ma in questa fase non lo chiama, quindi puoi connettere prima e scrivere il codice subito dopo.

Usa l'URL esatto su cui risponde il tuo sito, senza redirect

Se il tuo host reindirizza example.com a www.example.com (o il contrario), inserisci l'indirizzo verso cui reindirizza. GrowWriter non segue mai un redirect, quindi un indirizzo che reindirizza fa fallire ogni consegna. Non sai quale dei due usa il tuo sito? Controllalo nella risoluzione dei problemi. Puoi cambiare l'indirizzo più avanti con Modifica URL nella riga del webhook, e il tuo secret resta lo stesso.

Fai clic su Crea connessione. La riga del webhook mostra Non verificato finché il primo evento di prova o la prima pubblicazione non riesce.

Il passaggio 1 della finestra Connetti webhook con il campo URL dell'endpoint compilato e il pulsante Crea connessione.
Inserisci l'indirizzo su cui risponde il tuo sito, poi fai clic su Crea connessione.

Copia il tuo secret di firma

La finestra ora mostra il tuo Secret di firma. Inizia con whsec_ ed è la password condivisa di Perché la firma e il secret?: la chiave che il tuo codice usa per controllare che una richiesta arrivi davvero da GrowWriter.

Fai clic su Copia secret e conservalo in un posto sicuro sul tuo server, di solito come variabile d'ambiente:

.env
GROWWRITER_WEBHOOK_SECRET=whsec_your_secret_here

Un dettaglio su cui in tanti inciampano: quando il tuo codice verifica una firma, la chiave è la stringa intera, prefisso whsec_ incluso. Non togliere il prefisso. E se vuoi validare il valore nella tua configurazione, un secret è sempre whsec_ seguito da 43 caratteri base64 URL-safe.

Questo secret viene mostrato una sola volta

GrowWriter lo conserva cifrato e non può mostrarlo di nuovo. Se lo perdi, disconnetti e riconnetti il webhook per ottenerne uno nuovo. Riconnettersi crea sempre un secret nuovo, e quello vecchio smette di funzionare.

Passaggio 2 della finestra Connetti webhook, con il campo Secret di firma e i pulsanti Copia secret e Invia evento di prova.
Copia il secret prima di chiudere la finestra. Viene mostrato una sola volta.

Costruisci il tuo endpoint

Da qui in avanti è un lavoro da sviluppatore. Manda questa pagina alla persona che gestisce il tuo sito web; le serve il secret del passaggio 2. Tu torni al passaggio 4 per inviare l'evento di prova.

L'endpoint è un piccolo pezzo di codice, il ricevitore, all'URL del passaggio 1. Legge la richiesta esattamente com'è arrivata, controlla la firma, risponde con uno stato 2xx e salva l'articolo. Se a quel punto conosce l'URL pubblico del post, può restituirlo come {"url": "https://…"} e GrowWriter mostra un link Vedi articolo nella finestra di pubblicazione.

Non sai quale esempio usare? Se il tuo sito gira su Next.js, Vercel o Netlify, parti dalla scheda Next.js. Se hai un server tuo, scegli il linguaggio in cui è scritto. Copia il file dagli esempi di codice qui sotto, metti il secret del passaggio 2 nelle variabili d'ambiente del tuo hosting provider (impostazioni che il tuo host conserva fuori dal codice) e distribuisci, così che il file risponda all'URL del tuo endpoint.

Ogni esempio risponde già all'evento di prova. Salvare l'articolo è dove entra in gioco la tua configurazione: se il tuo sito usa un CMS o un database, Salva l'articolo nel tuo CMS ti dice cosa preparare.

Invia un evento di prova

Torna nella finestra di GrowWriter e fai clic su Invia evento di prova. GrowWriter invia un piccolo ping firmato al tuo endpoint. Ha questo aspetto:

Payload dell'evento di prova
{
  "version": "2026-09-06",
  "event": "ping",
  "deliveryId": "5f0c9a1e-4d2b-4f6a-9c3e-8b7d6a5e4f3c",
  "sentAt": "2026-09-01T09:30:00.000Z"
}

Nota che non contiene nessun articolo. È così che il tuo codice distingue una prova da una consegna vera.

GrowWriter firma il ping esattamente come una consegna vera e lo invia anche nello stesso modo: fino a tre tentativi, ognuno con 10 secondi di attesa per una risposta 2xx qualsiasi, quindi un endpoint che non risponde mai impiega circa mezzo minuto a fallire. Quando il tuo endpoint risponde, la finestra mostra Evento di prova consegnato. Fai clic su Fatto e sei connesso.

La pagina Canali con la riga del webhook che mostra il badge Connesso, Modifica URL e Disconnetti.
Dopo un evento di prova consegnato, la riga mostra Connesso, con Modifica URL e Disconnetti.

Se la prova fallisce, la finestra ne indica il motivo. L'URL del tuo endpoint reindirizza. Inserisci l'indirizzo a cui reindirizza e riprova significa che il tuo indirizzo manda GrowWriter da un'altra parte, e il messaggio mostra l'indirizzo da usare al suo posto. Il tuo endpoint ha rifiutato la consegna riporta lo stato restituito dal tuo endpoint e i primi 200 caratteri del corpo della risposta, quindi un 404 o un 500 con un breve messaggio di errore si spiega da solo. Consegna non riuscita dopo diversi tentativi significa che GrowWriter non è riuscito a connettersi affatto: l'indirizzo non è pubblico, oppure il sito è giù. Ogni messaggio ha la sua voce nella pagina di risoluzione dei problemi. Una prova fallita non rompe mai la connessione, quindi puoi riprovare tutte le volte che vuoi.

Pubblica un articolo

Apri uno dei tuoi articoli finiti e pubblicalo. Scegli il tuo webhook come canale e GrowWriter consegnerà l'articolo completo al tuo endpoint come evento article.published.

La finestra Pubblica articolo con il canale webhook spuntato e segnato Pubblicherà un nuovo.
Spunta il tuo webhook nella finestra Pubblica. La prima pubblicazione invia article.published.

Se ripubblichi lo stesso articolo più avanti (dopo una modifica, per esempio), il tuo endpoint riceverà un evento article.updated con lo stesso id dell'articolo. È quell'id che permette al tuo sito di aggiornare il post esistente invece di crearne un duplicato. "Aggiornato" significa semplicemente che una pubblicazione precedente su questo webhook è riuscita; una seconda connessione webhook riceve di nuovo article.published, perché non ha mai visto l'articolo.

Se più avanti elimini l'articolo in GrowWriter, o disconnetti il webhook, il tuo endpoint riceve article.unpublished con solo l'id, lo slug e la language dell'articolo. Nascondere o eliminare il post è una tua scelta; gli esempi qui sotto lo registrano soltanto nei log.

Esempi di codice

Scegli la scheda che corrisponde al tuo stack. Ogni esempio verifica la firma (i quattro passaggi di Perché la firma e il secret?), risponde al ping di prova, salta le consegne già elaborate e passa l'articolo a una funzione di salvataggio nella forma che un CMS si aspetta. Gli esempi Next.js ed Express registrano il post nei log, perché gli host serverless danno al tuo codice un filesystem in sola lettura; gli esempi Flask, PHP, Rails e Go scrivono un file JSON per articolo. Sostituisci l'uno o l'altro con una chiamata al tuo database. Le ragioni dietro ogni riga sono in Salva l'articolo nel tuo CMS.

Due dettagli che vedrai nel codice. Gli esempi rifiutano qualsiasi consegna con un timestamp più vecchio di cinque minuti: è la finestra anti-replay della sezione qui sopra, abbastanza stretta da rendere inutili i replay e abbastanza larga da assorbire lo scarto di orologio tra server. E confrontano le firme in tempo costante: il confronto dura lo stesso tempo sia quando un tentativo è quasi giusto sia quando è completamente sbagliato, così un attaccante non può imparare nulla misurando i tempi di risposta.

Una regola conta più di tutte le altre: verifica la firma sul corpo grezzo della richiesta, esattamente com'è arrivato. Se il tuo framework fa il parsing del JSON prima e tu lo ri-serializzi, i byte cambiano e la verifica della firma fallisce. Ogni esempio legge il corpo grezzo per primo esattamente per questo motivo.

app/webhooks/growwriter/route.ts
import { createHmac, timingSafeEqual } from 'node:crypto';

// Età massima in secondi di una consegna prima di rifiutarla.
// Protegge dal replay di una vecchia richiesta catturata.
const MAX_AGE_SECONDS = 5 * 60;

// Id di consegna già elaborati fino in fondo. Basta per un server solo;
// in produzione usa invece una tabella nel tuo database. Se il tuo host
// esegue il codice come funzioni di breve durata (serverless), questa
// lista non sopravvive tra una richiesta e l'altra.
const processedDeliveries = new Set<string>();

function verifySignature(rawBody: string, signatureHeader: string, secret: string) {
  // L'header ha questa forma: t=1756718400,v1=5257a869e7...
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((part) => part.split('='))
  );

  // 1. Il timestamp deve essere recente. GrowWriter firma di nuovo ogni
  //    tentativo, quindi una consegna fresca porta sempre un timestamp
  //    fresco.
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!(age < MAX_AGE_SECONDS)) {
    return false;
  }

  // 2. Ricalcola la firma: HMAC-SHA256 di "timestamp.corpoGrezzo", con
  //    come chiave il tuo secret di firma INTERO (prefisso whsec_ incluso).
  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');

  // 3. Confronta in tempo costante così il controllo non è misurabile.
  const provided = Buffer.from(parts.v1 ?? '', 'hex');
  const wanted = Buffer.from(expected, 'hex');
  return provided.length === wanted.length && timingSafeEqual(provided, wanted);
}

// Trasforma il payload nella forma che un CMS si aspetta. Ogni riga applica
// una delle sei regole di "Salva l'articolo nel tuo CMS" qui sotto.
function toPost(article: any) {
  const images = [article.coverImage, ...article.images].filter(Boolean);
  return {
    growwriter_id: article.id, // regola 1: un campo dedicato, mai una chiave sparsa
    title: article.title,
    slug: article.slug,
    summary: article.summary ?? '', // omesso dal payload se assente
    body: article.content.markdown, // regola 6: fai l'escape se lo compili come MDX
    published_at: article.dates.firstPublishedAt.slice(0, 10), // regola 2: YYYY-MM-DD
    updated_at: article.dates.modifiedAt.slice(0, 10),
    status: 'published', // regola 3: il payload non ha un flag di pubblicazione
    seo_keyword: article.keyword ?? '', // regola 4: la parola chiave è un campo SEO, non un tag
    faq: article.relatedQuestions, // regola 4: materiale per le FAQ, non tag
    image_urls: images.map((image: any) => image.url), // regola 5: scarica e ridimensiona
  };
}

// Conserva il post nel tuo database o CMS. Questo esempio lo registra
// soltanto nei log, di proposito: Vercel e la maggior parte degli host
// serverless montano la cartella del codice in sola lettura, quindi
// scrivere un file qui farebbe crashare la prima consegna vera con un
// 500. Qualunque cosa conservi, usa article.id come chiave: quando lo
// stesso articolo viene ripubblicato ("article.updated") sovrascrivi lo
// stesso record. Quel crea-o-aggiorna è tutto ciò che "upsert" significa.
// Restituisce l'URL pubblico del post, così l'handler può passarlo a
// 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) {
  // Leggi il corpo come testo grezzo PRIMA. Non usare request.json()
  // prima della verifica: la firma copre esattamente questi byte.
  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);

  // Il pulsante "Invia evento di prova" manda un ping senza articolo.
  // Rispondere 200 qui è ciò che fa riuscire la prova.
  if (payload.event === 'ping') {
    return new Response('pong', { status: 200 });
  }

  // Eliminare l'articolo in GrowWriter (o disconnettere il webhook) manda
  // article.unpublished con solo id, slug e lingua. Nascondi o elimina qui
  // il tuo post; questo esempio lo registra soltanto nei log.
  if (payload.event === 'article.unpublished') {
    console.log(`Unpublished ${payload.article.id}`);
    return new Response('OK', { status: 200 });
  }

  // I nuovi tentativi riusano lo stesso id di consegna. Se questo id è
  // già completato, l'articolo è salvato: rispondi di nuovo OK e salta
  // il lavoro.
  const deliveryId = request.headers.get('x-growwriter-delivery') ?? '';
  if (processedDeliveries.has(deliveryId)) {
    return new Response('OK', { status: 200 });
  }

  // payload.event ti dice il caso: "article.published" per un articolo
  // nuovo, "article.updated" quando torna un id che hai già visto.
  const postUrl = await saveArticle(payload.article, payload.event);
  processedDeliveries.add(deliveryId);

  // GrowWriter aspetta al massimo 10 secondi per tentativo. Una scrittura
  // nel database ci sta comodamente; se il tuo salvataggio è più lento,
  // vedi "E il limite dei 10 secondi?" qui sotto. Restituire l'URL del
  // post è facoltativo: dà alla finestra di pubblicazione un link "Vedi
  // articolo". Un semplice 'OK' va bene se l'URL non lo conosci ancora.
  return Response.json({ url: postUrl }, { status: 200 });
}

Usi un altro stack?

Gli esempi si traducono in qualsiasi linguaggio in grado di calcolare un HMAC. L'unico passaggio che si tende a sbagliare è il primo: verifica la firma sul corpo grezzo della richiesta, esattamente com'è arrivato. Ecco dove trovarlo in altri stack comuni:

StackCorpo grezzo della richiesta
Djangorequest.body
Laravel$request->getContent()
Sinatrarequest.body.read
Cloudflare Workersawait request.text()
Deno / Bunawait request.text()

E il limite dei 10 secondi?

GrowWriter aspetta la tua risposta fino a 10 secondi per tentativo. Salvare un record JSON ci sta comodamente. Se il tuo endpoint fa lavoro più lento, come scaricare ogni immagine prima di salvare, hai due opzioni:

  • Appoggiati ai nuovi tentativi (consigliato per gli endpoint semplici). Lascia l'handler esattamente com'è mostrato: crea o aggiorna in base ad article.id e salta gli id di consegna già completati, quindi eseguirlo due volte è innocuo. Se il primo tentativo va in timeout, il tuo server continua a lavorare. Il nuovo tentativo trova il lavoro già fatto, o lo rifà, e risponde entro la finestra. Non si perde nulla, e non scrivi codice extra.
  • Prima rispondi, poi lavora. Salva il payload grezzo, rispondi 200 subito ed elabora tutto in un job in background. Così batti sempre il cronometro, ma conosci il prezzo: una volta risposto 200, la consegna conta come riuscita. Se poi il tuo job in background fallisce, GrowWriter non riproverà, e l'unico modo per ricevere di nuovo l'articolo è pubblicarlo di nuovo. E siccome il post non esiste ancora quando rispondi, non puoi restituirne l'URL, quindi la finestra di pubblicazione non mostra nessun link Vedi articolo per quella pubblicazione.

Salva l'articolo nel tuo CMS

Il payload ha la forma di GrowWriter. Il tuo CMS, il tuo database o il tuo generatore di siti statici hanno la loro forma, e quasi tutti la controllano: uno schema è la lista dei campi che un tipo di contenuto ammette e il formato che ognuno accetta. Il codice qui sopra traduce tra le due forme. Questa sezione spiega le sei regole che ci stanno dietro, così puoi adattarle alla tua configurazione.

Prima della tua prima consegna, apri il pannello di amministrazione del tuo CMS e rispondi a cinque domande:

  1. Dove vivrà l'id di GrowWriter?
  2. Che formato accettano i tuoi campi data?
  3. Quale campo segna un post come pubblicato, e qual è il suo valore predefinito per i contenuti nuovi?
  4. Dove devono andare le parole chiave, se da qualche parte?
  5. Dove verranno conservate le immagini?

Regola 1: dai all'id di GrowWriter un campo tutto suo

Cosa. article.id è composto da 25 lettere minuscole e cifre, e inizia con c. È sicuro negli URL e nei nomi di file e non cambia mai.

Perché. La maggior parte dei CMS rifiuta i campi che non dichiara. Scrivere l'id come chiave sparsa accanto al titolo rompe il post nel pannello di amministrazione, anche se il sito continua a renderizzarlo (l'errore segnalato aveva questo aspetto: Key on object value "growwriterId" is not allowed).

Cosa fare. Aggiungi un campo dedicato come growwriter_id al tuo tipo di contenuto prima della prima consegna, oppure tieni una tabella separata che associa gli id ai tuoi post. Chiama i file con l'id, mai con lo slug.

Regola 2: converti le date

Cosa. Ogni data è in formato ISO 8601, UTC, con i millisecondi, come 2026-09-02T11:29:14.967Z.

Perché. I campi data senza orario di un CMS accettano YYYY-MM-DD e rifiutano un timestamp completo.

Cosa fare. Prendi i primi dieci caratteri per i campi data senza orario. Usa dates.firstPublishedAt come data di pubblicazione e dates.modifiedAt come data di aggiornamento. modifiedAt è l'ora della richiesta di pubblicazione, non dell'ultima modifica, quindi non confrontare mai le due date per rilevare un aggiornamento: te lo dice l'event.

Regola 3: imposta tu lo stato di pubblicazione

Cosa. Il payload non ha nessun flag di pubblicazione o di bozza. Il flag è l'evento, e sparisce quando la richiesta finisce.

Perché. Molti CMS trattano i contenuti nuovi come bozze per impostazione predefinita. Un post che era online perché la tua build ignorava quel valore predefinito va offline la prima volta che qualcuno lo salva nel pannello di amministrazione.

Cosa fare. Scrivi il tuo stato di pubblicazione in modo esplicito (status: "published", draft: false, o qualunque cosa usi il tuo CMS) sia su article.published sia su article.updated. Quando arriva article.unpublished (l'articolo è stato eliminato in GrowWriter, oppure il webhook è stato disconnesso), riporta indietro quello stato o elimina il post; il payload porta solo id, slug e lingua, quindi la decisione è tua.

Regola 4: la parola chiave e le domande non sono tag

Cosa. article.keyword è l'unico termine di ricerca per cui l'articolo è stato scritto, così come l'ha digitato l'autore. Può mancare. article.relatedQuestions è la lista delle domande che Google mostra come "le persone hanno chiesto anche" per quella parola chiave, nella lingua dell'articolo; è sempre presente e può essere vuota.

Perché. Un termine di ricerca non è un tag, e una domanda non è una categoria. Renderizzati come tag mettono frasi di ricerca sulle tue pagine pubbliche; usate come FAQ sotto l'articolo, le domande si guadagnano il loro posto.

Cosa fare. Conserva la parola chiave per i tuoi strumenti SEO. Mostra le domande come blocco FAQ, o come dati strutturati FAQPage se il tuo renderer li supporta. Se vuoi dei tag, decidili nel tuo CMS.

Regola 5: scarica e ottimizza le immagini

Cosa. Gli URL delle immagini puntano allo storage pubblico di GrowWriter. Oggi non scadono, ma GrowWriter non è un hosting di immagini a lungo termine. I file arrivano così come sono stati recuperati, alla risoluzione originale, e una singola immagine può superare i 10 MB.

Perché. Committare o servire originali da diversi megabyte rallenta ogni pagina.

Cosa fare. Scarica ogni immagine (copertina e immagini nel testo), ridimensionala alla larghezza massima che serve al tuo layout, comprimila e conservala dalla tua parte. Poi riscrivi gli URL nel formato di contenuto che salvi. Due cose te lo rendono facile: le immagini invariate mantengono esattamente gli stessi URL tra un aggiornamento e l'altro, quindi puoi saltare tutto ciò che hai già copiato, e gli stessi URL compaiono dentro il contenuto. Chiama i tuoi file a partire da article.id e dalla posizione dell'immagine; images[].filename è opzionale e non ha estensione, e sectionSlug dice solo per quale sezione della scaletta è stata creata l'immagine, quindi non analizzarlo e non usarlo come chiave.

Copia le immagini, poi riscrivi il contenuto
let html = article.content.html;
for (const image of [article.coverImage, ...article.images].filter(Boolean)) {
  const localUrl = await mirrorImage(image.url); // scarica + salva nel tuo storage
  html = html.replaceAll(image.url, localUrl);
}

Regola 6: fai l'escape per il tuo renderer

Cosa. La prosa normale contiene caratteri come {, } e <.

Perché. MDX, JSX e i motori di template li leggono come codice, e la build fallisce al primo articolo che li contiene.

Cosa fare. Conserva il markdown come dati e renderizzalo con un renderer Markdown, oppure fai l'escape di quei caratteri prima di compilare. content.html ha già l'escape e usa un insieme fisso di tag, quindi è la scelta sicura per l'inserimento diretto.

Gestire aggiornamenti e traduzioni

  • Aggiornamenti: stesso id, sovrascrivi il post. Modificare il titolo mantiene lo slug; solo scegliere un nuovo titolo nel writer ne produce uno nuovo. Quando cambia, aggiorna l'indirizzo del post e valuta un redirect da quello vecchio. Le immagini sostituite ricevono URL nuovi. Tratta l'ultima consegna come autorevole per ogni campo.
  • Traduzioni: ogni lingua è un articolo a sé con il proprio id. GrowWriter non le collega tra loro. Abbina le lingue con i tuoi dati, usando article.language (un codice di due lettere come es).

Prova in locale

Non ti serve pubblicare un articolo vero per mettere alla prova il tuo endpoint. Questo snippet firma un ping di prova esattamente come fa GrowWriter, usando openssl, e lo invia al tuo server locale con curl:

send-test-ping.sh
# Usa lo stesso secret che il tuo server locale ha in 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"}'

# Firma "timestamp.corpo" con HMAC-SHA256, esattamente come 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"

Regola la porta e il percorso in base a dove gira il tuo server. Un endpoint funzionante risponde pong con un 200. Ora cambia un solo carattere dentro BODY ed esegui di nuovo: dovresti ricevere un 401, la prova che la tua verifica della firma verifica davvero. Per provare un articolo completo, sostituisci BODY con il payload di esempio completo (su una sola riga) e imposta i due header dell'evento su article.published.

Per ricevere eventi veri da GrowWriter sulla tua macchina, ricorda che l'URL dell'endpoint deve essere pubblico, quindi localhost non funzionerà. Un servizio di tunnel come ngrok o cloudflared dà al tuo server locale un indirizzo HTTPS pubblico temporaneo; usalo come URL dell'endpoint mentre sviluppi, poi passa a quello vero.

Cosa contiene il payload

Ogni consegna è un POST con Content-Type: application/json, tre header X-GrowWriter-* (nome dell'evento, id di consegna, firma) e un corpo JSON che porta l'articolo in tre formati. Il riferimento del payload è il contratto: ogni header e ogni campo con il tipo, se è sempre presente, il formato esatto e l'esempio completo. I campi che fanno il grosso del lavoro:

CampoCome usarlo
article.idLa tua chiave stabile. Conservala in un campo dedicato e aggiorna il post esistente quando torna lo stesso id.
article.slugDato di visualizzazione per l'indirizzo del post. Cambia quando si sceglie un nuovo titolo nel writer, quindi non usarlo mai come chiave.
article.languageCodice lingua di due lettere (en, es, ...) per instradare i post su un sito multilingue. Ogni traduzione è un articolo a sé con il proprio id.
article.contentL'articolo in tre formati: markdown, html pronto all'uso e blocks strutturati. Usa quello adatto al tuo sito.
article.datesfirstPublishedAt è la tua data di pubblicazione, modifiedAt quella di aggiornamento. Prendi i primi dieci caratteri per i campi data senza orario.
article.coverImage e article.imagesLa copertina più ogni immagine del testo in ordine. Scaricale e ridimensionale.
article.keyword e article.relatedQuestionsIl termine di ricerca a cui punta l'articolo, e le domande che Google gli abbina. Campo SEO e materiale per le FAQ; nessuno dei due è un tag.
article.brandNome e sito del tuo brand per il markup SEO, mai una persona. Imposta l'autore dei post nella tua logica di salvataggio.

Nuovi campi opzionali possono comparire senza che la data version cambi, quindi ignora i campi che non riconosci. I cambiamenti incompatibili fanno avanzare la data e vengono annunciati prima nel changelog; la policy completa è in Versionamento.

Consegna e nuovi tentativi

Una pubblicazione su una connessione è una consegna, e una consegna fa fino a tre tentativi (subito, poi 1 secondo e 3 secondi più tardi), ognuno con 10 secondi di attesa per una risposta 2xx qualsiasi. Tutti i tentativi condividono un solo id di consegna e vengono firmati di nuovo, i redirect non vengono mai seguiti e GrowWriter non riprova più tardi: dopo il terzo fallimento il canale mostra un pulsante Riprova nella finestra di pubblicazione, che avvia una nuova consegna. Le regole esatte, compresa la nota "almeno una volta" su cui si basa il tuo controllo dei doppioni, sono nel riferimento: Consegna.

Risoluzione dei problemi

Ogni errore ha la sua voce nella pagina di risoluzione dei problemi, organizzata in base a quello che vedi. I più comuni alla prima connessione:

Ancora bloccato? Scrivici dall'app e lo risolviamo insieme.

Changelog

  • Versione attuale: 2026-09-06. Il payload descritto nel riferimento è il primo contratto pubblicato. I cambiamenti futuri saranno elencati qui, dal più recente: i cambiamenti additivi mantengono la data di versione, le rimozioni o le rinomine la fanno avanzare e vengono annunciate qui prima di andare online.

In questa pagina