Salta al contenuto
Documentazione

Webhook di consegna: ricevere gli articoli sul proprio sito

Il webhook firmato che invia gli articoli di AutoPSEO al tuo sito: header, ogni evento e payload, le risposte da dare, i tentativi successivi, i comandi in attesa e IndexNow.

Proprietà → Contenuti → Dove vengono pubblicati gli articoli20 min di lettura

AutoPSEO invia al tuo sito ogni articolo che pubblica, aggiorna o rimuove, tramite una POST HTTPS firmata. Il tuo sito archivia l'articolo e lo mostra con i propri template, così i contenuti restano nel tuo sistema e continuano a funzionare anche se un giorno smetterai di usare AutoPSEO. Questa pagina è il riferimento per lo sviluppatore che realizza il ricevitore: firma, ogni evento con il suo payload, le risposte che AutoPSEO legge, i tentativi successivi, i comandi in attesa e IndexNow.

Configurare il webhook

  1. Apri la pagina Content della proprietà e vai a Where articles are published.
  2. Compila Webhook address on your site. Deve essere un URL https:// su un nome host pubblico: gli indirizzi IP e localhost vengono rifiutati. AutoPSEO non segue i reindirizzamenti, quindi indica l'indirizzo finale.
  3. Compila Your site's address, ad esempio https://example.com. Il percorso di ogni tipo di contenuto si aggiunge a questo indirizzo, quindi un articolo di un tipo situato in /answers si trova su https://example.com/answers/<slug>.
  4. Scegli Save and send a test. AutoPSEO crea un segreto di firma (whsec_ seguito da 48 caratteri esadecimali) e invia un ping al tuo indirizzo. Un ricevitore che verifica le firme fa fallire questo primo ping, perché non ha ancora il segreto.
  5. Mostra il Signing secret con Show o Copy, salvalo come segreto sul tuo sito, poi scegli Send a test.
  6. Leggi What your site handles: ogni comando mostra Connected, Not connected, Declared, not tested oppure Not tested.

Le nuove proprietà partono con gli articoli ospitati da AutoPSEO al proprio indirizzo (vedi Hosting), e hosting e webhook possono funzionare in parallelo. Quando il tuo sito mostra gli articoli, scegli Stop hosting on AutoPSEO: ogni articolo pubblicato passa all'indirizzo del tuo sito e viene inviato al tuo webhook come article.published. Questo pulsante richiede un webhook che abbia risposto all'ultimo test e l'indirizzo del tuo sito. Finché l'hosting è disattivato, il webhook non può essere rimosso.

New secret sostituisce subito il segreto di firma e quello vecchio smette di essere valido immediatamente. Inserisci il nuovo segreto sul tuo sito subito dopo averlo generato.

AutoPSEO mette in coda le consegne solo per un webhook il cui ultimo ping ha ricevuto una risposta 2xx. Un ping fallito (al salvataggio, con Send a test o all'inizio della scansione del sito) segna il webhook come in errore, e gli articoli pubblicati mentre è in errore non vengono messi in coda per esso. Fai in modo che ping risponda sempre e scegli Send a test dopo aver risolto un'interruzione.

Header e firma

Ogni richiesta è una POST con un corpo JSON e questi header:

HeaderValore
Content-Typeapplication/json
User-AgentAutoPSEO-Webhook/1
X-AutoPSEO-EventIl nome dell'evento, presente anche nel corpo come event
X-AutoPSEO-DeliveryL'id della consegna, presente anche nel corpo come id. Resta lo stesso a ogni nuovo tentativo della stessa consegna
X-AutoPSEO-TimestampTempo Unix in secondi in cui questo tentativo è stato firmato
X-AutoPSEO-Signaturesha256= seguito dall'HMAC-SHA256 in esadecimale di <timestamp>.<raw body>

Per verificare una richiesta:

  1. Prendi l'header del timestamp, un punto e il corpo grezzo della richiesta esattamente come è arrivato, prima di qualsiasi parsing JSON.
  2. Calcola l'HMAC-SHA256 su quella stringa. La chiave è l'intero segreto di firma così come lo mostra l'app, prefisso whsec_ compreso, come byte UTF-8.
  3. Codifica il risultato in esadecimale e confrontalo con la parte di X-AutoPSEO-Signature dopo sha256=, in tempo costante. Rifiuta la richiesta se non coincidono.

Ogni tentativo viene firmato di nuovo con il proprio timestamp, quindi puoi rifiutare le richieste il cui timestamp si discosta molto dal tuo orologio (cinque minuti è una finestra comune) senza compromettere i tentativi successivi. L'HMAC è definito nella RFC 2104.

Ogni corpo contiene id, event, occurredAt (ISO 8601, UTC) e site: { propertyId, domain }.

Eventi

EventoTipoQuando AutoPSEO lo invia
pingVerificaQuando salvi il webhook, con Send a test e all'inizio di ogni scansione del sito
testVerificaSubito dopo un ping riuscito, uno per comando
article.publishedConsegnaUn articolo va online per la prima volta; ogni articolo pubblicato quando smetti di usare l'hosting
article.updatedConsegnaVa online una nuova revisione (aggiornamento, ottimizzazione, ripristino, nuova pubblicazione dopo una rimozione, lingue aggiunte) oppure è stata aggiunta una copertina
article.unpublishedConsegnaUn articolo viene rimosso o unito a un'altra pagina
path.addConsegnaÈ stato approvato un nuovo valore di un tipo di contenuto tipizzato
path.removeConsegnaQualcuno ha chiesto di rimuovere il percorso di un valore
path.checkChiamataPrima che AutoPSEO pianifichi una pagina a un indirizzo su cui il tuo sito non ha alcuna pagina, o proponga un nuovo valore
site.exportChiamataDurante una scansione del sito, nella sincronizzazione giornaliera degli argomenti e prima che AutoPSEO modifichi un articolo online
articles.adoptedChiamataDopo una scansione che ha importato gli articoli esportati dal tuo sito
site.handoverChiamataQuando qualcuno affida la scrittura di tipi di contenuto ad AutoPSEO, o la restituisce al sito

Le consegne passano da un outbox: vengono registrate nello stesso batch del database della modifica, inviate subito, ritentate e possono restare in attesa del tuo sito. Le chiamate sono semplici richieste con risposta: AutoPSEO legge la risposta sul momento. Quando vengono inviati i comandi dei percorsi è spiegato in Pagine tipizzate e nuovi percorsi.

Il payload dell'articolo

article.published e article.updated contengono l'intero articolo, con tutte le lingue:

{
  "id": "0f6d2c1e-7a51-4c1b-9d0e-5b8f3a2e9c41",
  "event": "article.published",
  "occurredAt": "2026-10-12T08:30:00.000Z",
  "site": { "propertyId": "prop_123", "domain": "example.com" },
  "article": {
    "id": "a4c09e2b-6f1d-4e57-8b3a-1d2e3f4a5b6c",
    "slug": "plan-a-content-calendar",
    "path": "/answers/plan-a-content-calendar",
    "type": { "key": "answers", "name": "Answers", "path": "/answers" },
    "lang": "en",
    "langs": ["en", "de"],
    "rev": 1,
    "url": "https://example.com/answers/plan-a-content-calendar",
    "datePublished": "2026-10-12",
    "dateModified": "2026-10-12",
    "contentHash": "9f2c4e1a7b3d5f6081a2c3d4",
    "writer": "platform",
    "category": "Planning",
    "copy": {
      "title": "How to plan a content calendar",
      "metaDescription": "A content calendar in five steps, with a template.",
      "h1": "How to plan a content calendar",
      "directAnswer": "List your topics, give each a date and an owner, then review the plan weekly.",
      "keyTakeaways": ["Plan a quarter at a time.", "Give every piece one owner."],
      "sections": [{ "heading": "Start from your topics", "bodyHtml": "<p>Begin with the searches you want to win.</p>" }],
      "faq": [{ "q": "How far ahead should I plan?", "a": "One quarter is enough for most teams." }],
      "sources": [{ "title": "Creating helpful content", "url": "https://developers.google.com/search/docs/fundamentals/creating-helpful-content" }]
    },
    "translations": { "de": { "title": "Einen Redaktionsplan erstellen", "h1": "Einen Redaktionsplan erstellen" } },
    "cover": {
      "url": "https://app.autopseo.com/media/covers/prop_123/<sha256>.jpg",
      "sha256": "<sha256>",
      "contentType": "image/jpeg",
      "width": 1536,
      "height": 1024
    }
  }
}

La voce tedesca è abbreviata; ogni traduzione ha la stessa struttura di copy.

CampoSignificato
idL'id dell'articolo in AutoPSEO. Non cambia mai, quindi è una chiave sicura
slugL'ultima parte dell'indirizzo. Nelle pagine tipizzate contiene segmenti uniti da /, come google/software-engineer
pathL'indirizzo relativo al tuo sito: il percorso del tipo più lo slug. Resta fisso una volta creata la pagina
type{ key, name, path }. key è stabile; path è il punto in cui si trovano gli articoli del tipo (vuoto per la radice del sito)
lang, langsLa lingua principale e tutte le lingue di questa revisione
revIl numero di revisione
urlL'indirizzo completo così come lo conosce AutoPSEO: quello del tuo sito una volta disattivato l'hosting, oppure l'url a cui il tuo ricevitore ha risposto l'ultima volta
datePublished, dateModifiedYYYY-MM-DD. La data di pubblicazione resta fissa nelle revisioni successive
contentHashUn'impronta della revisione
writerChi ha scritto la revisione: platform (AutoPSEO), mcp (la tua AI), user, import (il testo del tuo sito) o aggregate (una pagina elenco)
copyL'articolo nella lingua principale
translationsLe altre lingue, indicizzate per codice lingua
categoryLa categoria del tuo sito per l'articolo, se il tuo formato prevede le categorie
topic{ externalId, keyword } quando l'articolo è stato scritto per un argomento passato dal tuo sito, così puoi chiudere quell'argomento
partsNelle pagine tipizzate, il valore di ciascuna parte: { "company": "google", "role": "software-engineer" }
kind"list" in una pagina elenco. Il suo copy è { title, h1, items: [{ from, text }] }, dove from è il percorso della pagina da cui proviene ciascuna voce, e parts indica il valore che raccoglie
coverUna copertina creata da AutoPSEO per l'articolo. Scaricala una volta e conserva la tua copia; l'indirizzo non cambia mai perché il nome del file è lo SHA-256 dell'immagine
basedOnIn article.updated: la versione della tua pagina da cui è stata scritta questa revisione (vedi i conflitti più sotto)
redirectToIn article.unpublished dopo un'unione: dove porta ora il vecchio indirizzo. Rispondi con un 301 e non pubblicare nulla di nuovo lì

article.unpublished contiene gli stessi campi di article tranne copy, translations, category e cover.

All'interno di copy

CampoSignificato
title, metaDescription, h1Il titolo della pagina, la meta description e l'intestazione
directAnswerLa risposta breve mostrata per prima
keyTakeawaysUn elenco di stringhe
sections[{ heading, bodyHtml }]
faq[{ q, a }]
sources[{ title, url, lastVerified? }]
questionFacoltativo: la domanda di chi cerca, con le sue parole, se il tuo formato la richiede
categoryFacoltativo: una delle categorie del tuo sito
listsFacoltativo: elenchi in testo semplice per chiave, per i tipi di contenuto che li richiedono (le domande di esempio di una guida ai colloqui)

Negli articoli scritti da AutoPSEO, bodyHtml usa solo p, ul, li, strong, em, a e code, e i link conservano solo href. Il testo ripreso dal tuo sito mantiene il tuo HTML. Fallo passare anche dal tuo sanitizzatore, se i tuoi template ne richiedono uno.

Cosa risponde il tuo ricevitore

Qualsiasi 2xx completa una consegna. AutoPSEO legge anche un oggetto JSON facoltativo dal corpo della risposta; mantienilo sotto i 64 KB, perché una risposta più grande conta come errore.

CampoEffetto
urlL'indirizzo https:// in cui l'articolo si trova ora. AutoPSEO lo registra come URL dell'articolo
versionLa versione della tua pagina, una stringa opaca di al massimo 128 caratteri. Torna come basedOn con l'aggiornamento successivo
urlsGli indirizzi https:// modificati da questa consegna, per IndexNow (ogni lingua, oppure gli indirizzi che hai smesso di servire). [] significa che nulla è cambiato
cover"wanted" quando non hai una copertina per l'articolo e la consegna non ne conteneva. AutoPSEO ne crea una e la invia in un article.updated

Le risposte di errore hanno significati diversi:

La tua rispostaCome la legge AutoPSEOCosa succede
Un 4xx con { "error": "unknown_event" }, oppure un 501Non gestisci ancora questo comandoLa consegna resta in attesa, senza nuovi tentativi
Un 409 con { "error": "conflict" }L'aggiornamento è stato scritto a partire da una versione che la tua pagina non ha piùLa consegna fallisce senza nuovi tentativi e l'articolo viene riletto dal tuo sito prima della modifica successiva
Un 4xx diverso da 408 o 429 con { "error": "<code>", "details": [] } a path.add o path.removeUn rifiuto con la tua motivazioneRegistrato; vedi Pagine tipizzate e nuovi percorsi
Qualsiasi altra cosa: nessuna risposta entro 10 secondi, un errore di rete, un reindirizzamento, 408, 429, 5xx o un altro 4xxUn errore temporaneoRitentato con backoff

Nuovi tentativi e consegne sostituite

Una consegna viene inviata appena la modifica è salvata, e una pianificazione ogni cinque minuti invia tutto ciò che è in scadenza, partendo dal più vecchio. Dopo un errore temporaneo AutoPSEO attende 1 minuto, poi 5 minuti, 15 minuti, 1 ora, 3 ore, 6 ore e 12 ore tra un tentativo e l'altro. L'ottavo tentativo fallito segna la consegna come non riuscita, e la pagina Content la conta in un badge rosso accanto al webhook.

Quando esiste una consegna più recente per lo stesso articolo, una più vecchia ancora in sospeso viene segnata come sostituita e non viene mai inviata. Ogni consegna contiene l'intero articolo, quindi la più recente è tutto ciò che serve al tuo sito. I comandi dei percorsi relativi allo stesso valore funzionano allo stesso modo.

Il tuo ricevitore può vedere due volte la stessa consegna, ad esempio quando ha archiviato l'articolo ma ha risposto dopo il timeout di 10 secondi. Deduplica su X-AutoPSEO-Delivery, oppure rendi idempotente ogni scrittura salvando in base a article.id. Anche article.updated può arrivare per un articolo che hai eliminato dopo un article.unpublished, perché la nuova pubblicazione di un articolo rimosso invia un aggiornamento: tratta gli aggiornamenti come "crea o sostituisci".

Comandi che il tuo sito non gestisce ancora

Un sito può implementare il protocollo un comando alla volta. AutoPSEO tiene lo stato di ogni comando e lo apprende in tre modi:

  • Test. Dopo ogni ping riuscito, AutoPSEO invia un test per comando. Il corpo è { id, event: "test", occurredAt, site, test: { event: "<command>", ... } }, dove test contiene un esempio con la stessa struttura del comando reale e riferito a <type path>/autopseo-connection-test, un indirizzo che nessun sito possiede. Verificalo come faresti con il comando reale, non modificare nulla e rispondi con un 2xx: { "test": "<command>", "ok": true } per Connected, { "test": "<command>", "ok": false, "error": "unknown_event" } per Not connected. Una risposta con ok: false e un altro codice di errore, come la rimozione di un indirizzo che non hai, conta comunque come connesso. L'esempio si trova sotto test, mai al livello principale, quindi un ricevitore che non sa nulla dei test non vi trova alcun articolo.
  • Consegne e chiamate reali. Un 2xx segna il comando come connesso; unknown_event lo segna come non connesso.
  • La risposta al ping. I comandi che la tua risposta elenca in autopseo.events compaiono come Declared, not tested finché un test o una consegna non dice di più.

Una consegna per un comando non connesso resta in attesa. La pagina Content elenca ciò che è in attesa, ad esempio "Your site does not take articles down yet", con un pulsante Done on the site per tutto ciò che hai eseguito a mano; AutoPSEO non invierà più quel comando. Quando un test o una consegna successivi trovano il comando connesso, tutto ciò che era in attesa viene inviato.

La risposta al ping

Rispondi a ping con un 2xx. Un sito che si limita a ricevere articoli non ha bisogno d'altro. Per dire ad AutoPSEO che cos'altro fa il tuo sito, includi un oggetto autopseo:

{
  "ok": true,
  "autopseo": {
    "version": 1,
    "events": ["article.published", "article.updated", "article.unpublished"],
    "covers": true,
    "indexNow": { "key": "3f1c9a7e5b2d4f60", "keyLocation": "https://example.com/3f1c9a7e5b2d4f60.txt" },
    "export": ["articles", "topics", "profile"],
    "adopt": true,
    "handover": true,
    "types": ["answers"],
    "writer": "site"
  }
}
CampoSignificato
version1. L'oggetto viene ignorato se la versione non è almeno 1
eventsI comandi che gestisci, mostrati come dichiarati finché non vengono testati
coverstrue quando le tue pagine mostrano una copertina con ogni articolo. Il redattore di AutoPSEO crea allora la copertina durante la scrittura, così la prima consegna la include, e alla tua AI viene chiesto di fornirne una. Vedi Covers
indexNow{ key, keyLocation? }: la chiave IndexNow che servi (da 8 a 128 lettere, cifre e trattini) e, quando il file della chiave non è in /<key>.txt, la sua posizione https://
exportCiò che site.export fornisce: articles, topics, profile
adoptAccetti articles.adopted, così una scansione completa può prendere in carico i tuoi articoli esistenti
handoverAccetti site.handover
typesLe chiavi dei tipi di contenuto che accetti (fino a 50)
writerChi scrive oggi i tuoi nuovi articoli: site o autopseo

IndexNow per gli indirizzi che servi

Quando la tua risposta al ping dichiara indexNow, AutoPSEO invia un ping a IndexNow su api.indexnow.org dopo le consegne che accetti, e il tuo sito non ne invia alcuno per conto proprio.

  • Invia gli urls elencati nella tua risposta. Senza urls invia l'url della tua risposta, altrimenti l'URL dell'articolo. urls: [] non invia nulla.
  • Vengono inclusi solo gli indirizzi sul tuo host, come richiede il protocollo IndexNow: l'host di keyLocation, altrimenti l'host dell'indirizzo del tuo sito.
  • Ogni ciclo di consegna invia un ping per sito, con fino a 10.000 indirizzi.
  • 200 e 202 sono accettati. Un 429, un 5xx o l'assenza di risposta mantengono gli indirizzi e riprovano dopo un'ora, raddoppiando ogni volta fino a un giorno (di più se lo chiede Retry-After), insieme a quanto aggiunto dalle consegne successive. Un 403 (chiave non valida) o un 422 (indirizzi non presenti sull'host) viene registrato e scartato.
  • La pagina Content mostra l'ultimo ping: quando, quanti indirizzi, lo stato HTTP e il prossimo tentativo.
  • Finché AutoPSEO ospita anche gli articoli, le pagine ospitate vengono notificate con la chiave di AutoPSEO, e la tua non viene usata.

Lo strumento IndexNow in Indexing è un invio manuale separato; vedi IndexNow.

Conflitti: il testo del tuo sito è l'originale

La version della tua risposta (o la version di un articolo nella tua esportazione) è la versione a cui AutoPSEO ritiene che si trovi la tua pagina. Ogni article.updated la porta come basedOn. Se la pagina è cambiata sul tuo sito nel frattempo, ad esempio dopo la correzione di un redattore, rispondi 409 con { "error": "conflict" } e non archiviare nulla. La consegna fallisce senza nuovi tentativi.

Prima che una modifica in coda a un articolo online venga scritta, AutoPSEO rilegge quell'articolo dal tuo sito con site.export e paths, se il tuo sito gestisce site.export. Se la tua versione è andata avanti, il tuo testo diventa la revisione successiva dell'articolo (writer import), e l'aggiornamento che segue porta la tua versione attuale come basedOn.

path.add e path.remove

Questi due comandi di consegna riguardano un valore di un tipo di contenuto tipizzato, ad esempio un'azienda in /interviews/{company}/{role}. Portano un oggetto path invece di article:

{
  "id": "7c1e9d2a-3b4f-4e8a-9c0d-1e2f3a4b5c6d",
  "event": "path.add",
  "occurredAt": "2026-10-12T08:30:00.000Z",
  "site": { "propertyId": "prop_123", "domain": "example.com" },
  "path": {
    "path": "/interviews/jane-street",
    "type": { "key": "interviews", "name": "Interviews", "path": "/interviews" },
    "part": "company",
    "value": {
      "value": "jane-street",
      "name": "Jane Street",
      "aliases": [],
      "url": "https://<its own site>/",
      "sameAs": ["https://<its careers page>/"],
      "description": "<what it is, in a few words>",
      "facts": [
        { "text": "<one sentence, as the source states it>", "source": "https://<the page that says it>", "lastVerified": "2026-10-01", "tier": "official" }
      ]
    },
    "takes": { "role": ["software-engineer"] }
  }
}
CampoSignificato
pathL'indirizzo proprio del valore sul tuo sito
typeIl tipo di contenuto, come { key, name, path }
partLa parte dell'indirizzo che il valore riempie
value{ value, name }, più, in path.add, gli aliases, url, sameAs, description e facts raccolti dal redattore di AutoPSEO. Ogni fatto ha text, source, lastVerified e un tier pari a official o reported
takesIn path.add: i valori delle altre parti con cui le persone lo cercano
redirectToIn path.remove: dove devono portare d'ora in poi gli indirizzi del valore, con un 301

Rispondi con un 2xx quando hai preso in carico il comando; in path.add puoi aggiungere l'url in cui il valore si trova ora, e in path.remove gli urls che hai smesso di servire, che vanno a IndexNow. Per rifiutare, rispondi con un 4xx e { "error": "<code>", "details": ["<why>"] }. In path.add, invalid_value rimanda il valore al redattore con la tua motivazione (il terzo rifiuto è definitivo) e qualsiasi altro codice lo rifiuta per sempre. In path.remove, not_found conta come eseguito, e qualsiasi altro codice, come un 409 in_use, lascia tutto com'era. Segnare un comando di percorso in attesa come Done on the site conta come il sì del tuo sito.

Chiamate: richieste di cui AutoPSEO legge la risposta

Le chiamate sono firmate come le consegne. Una chiamata attende fino a 60 secondi per impostazione predefinita e 30 secondi per le riletture e i controlli dei percorsi, e legge risposte fino a 24 MB. Qualsiasi risposta diversa da un 2xx fa fallire la chiamata.

site.export

La richiesta contiene export: { what, cursor, limit }, più since (un orario ISO) per gli argomenti o paths per una rilettura. Rispondi con { "items": [], "next": "<cursor>" }; AutoPSEO chiede di nuovo con cursor impostato al tuo next finché non è vuoto o null.

  • what: "articles" viene eseguito nelle scansioni complete del sito, 10 per pagina, quando hai dichiarato export con articles e adopt: true. Una risposta 400 significa "non supportato". Con paths: ["/answers/x"] e limit: 1 rilegge un solo articolo; rispondi solo con quell'articolo.
  • what: "topics" viene eseguito a ogni scansione e nella sincronizzazione giornaliera, 50 per pagina; la sincronizzazione giornaliera invia since, l'orario dell'ultimo prelievo.
  • what: "profile" viene eseguito a ogni scansione. Rispondi con { "profile": { types, languages, rules, categories, format } }, tutti facoltativi: types come [{ key, name, path }], e rules con brandTerms, synonyms, noise, onTopic e offTopic.

Un articolo esportato richiede externalId, uno slug (lettere e cifre di qualsiasi alfabeto e trattini, al massimo 200 caratteri, segmenti uniti da /), un path che termina con quello slug e copy con almeno title, h1 e una sezione. Campi facoltativi: lang (predefinito en), type (una chiave di tipo di contenuto), url, keyword, category, datePublished, dateModified, version, translations, owner (l'id articolo di AutoPSEO, una volta adottato) e maintenance: { kind: "refresh" or "optimize", reason }, che mette in coda quell'attività. Ogni articolo esportato diventa un articolo gestito con il tuo testo come prima revisione, e nulla viene rimandato indietro. Esportare di nuovo è sicuro, e un articolo che AutoPSEO ha rimosso non torna mai tramite un'esportazione.

Una voce di argomento è { keyword, status?, volume?, source?, externalId?, type?, reason? }, dove status è queued, planned o rejected.

articles.adopted

Dopo l'importazione, AutoPSEO ti chiede di lasciargli gestire i nuovi articoli: { "articles": [{ "externalId", "articleId", "slug", "path" }] }. Rispondi con { "adopted": ["<externalId>"], "refused": [{ "externalId": "<id>", "reason": "<why>" }] }. Un articolo che non confermi viene rimosso di nuovo da AutoPSEO, a meno che AutoPSEO non ne abbia già scritto una revisione.

site.handover

{ "handover": { "types": ["answers"], "writer": "autopseo" } }, inviato quando qualcuno sceglie Let AutoPSEO write them o Hand writing back to the site nella pagina Your site della proprietà. Rispondi con { "ok": true, "writer": "autopseo" }. Dopo il passaggio ad AutoPSEO, il tuo sito passa ad AutoPSEO gli argomenti che approva, invece di scriverli.

path.check

{ "paths": [{ "path": "/interviews/google/data-engineer" }, { "path": "/interviews/jane-street", "new": { "part": "company", "value": "jane-street", "name": "Jane Street" } }] }. Rispondi con un risultato per ogni percorso: { "results": [{ "path": "/interviews/google/data-engineer", "ok": true }, { "path": "/interviews/jane-street", "ok": false, "error": "not_allowed", "details": ["We only list companies with a careers page."] }] }. Un rifiuto viene conservato con la tua motivazione e non viene chiesto di nuovo; un percorso che ometti viene richiesto più tardi.

Un ricevitore minimo

Un Cloudflare Worker che verifica la firma con la Web Crypto API, gestisce i tre comandi degli articoli e rifiuta gli altri come unknown_event. Conserva gli articoli in un namespace KV associato come ARTICLES e il segreto di firma in un secret del Worker chiamato AUTOPSEO_SECRET.

const HANDLED = ["article.published", "article.updated", "article.unpublished"];
const MAX_SKEW_SECONDS = 300;
const SITE = "https://example.com";

const json = (data, status = 200) =>
  new Response(JSON.stringify(data), { status, headers: { "content-type": "application/json" } });

function hexToBytes(hex) {
  if (!/^[0-9a-f]{64}$/.test(hex)) return null;
  const out = new Uint8Array(32);
  for (let i = 0; i < 32; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
  return out;
}

async function verified(request, body, secret) {
  const timestamp = request.headers.get("X-AutoPSEO-Timestamp") || "";
  const signature = (request.headers.get("X-AutoPSEO-Signature") || "").replace(/^sha256=/, "");
  if (!/^\d+$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > MAX_SKEW_SECONDS) return false;
  const bytes = hexToBytes(signature);
  if (!bytes) return false;
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
  // verify() compares in constant time.
  return crypto.subtle.verify("HMAC", key, bytes, enc.encode(`${timestamp}.${body}`));
}

export default {
  async fetch(request, env) {
    if (request.method !== "POST") return new Response("Method not allowed", { status: 405 });
    const body = await request.text();
    if (!(await verified(request, body, env.AUTOPSEO_SECRET))) return json({ error: "bad_signature" }, 401);
    let msg;
    try {
      msg = JSON.parse(body);
    } catch {
      return json({ error: "bad_json" }, 400);
    }

    if (msg.event === "ping") return json({ ok: true, autopseo: { version: 1, events: HANDLED } });

    if (msg.event === "test") {
      const command = msg.test && msg.test.event;
      // Check the sample as you would the real command, and store nothing.
      return HANDLED.includes(command)
        ? json({ test: command, ok: true })
        : json({ test: command, ok: false, error: "unknown_event" });
    }

    if (msg.event === "article.published" || msg.event === "article.updated") {
      const a = msg.article;
      const stored = JSON.parse((await env.ARTICLES.get(a.id)) || "null");
      if (a.basedOn && stored && stored.version && stored.version !== a.basedOn) {
        return json({ error: "conflict", details: ["The page changed on the site."] }, 409);
      }
      // Any change to the page, here or in your own editor, gets a new version.
      const version = crypto.randomUUID();
      await env.ARTICLES.put(a.id, JSON.stringify({ ...a, version }));
      return json({ url: SITE + a.path, version });
    }

    if (msg.event === "article.unpublished") {
      const a = msg.article;
      if (a.redirectTo) await env.ARTICLES.put(a.id, JSON.stringify({ path: a.path, redirectTo: a.redirectTo }));
      else await env.ARTICLES.delete(a.id);
      return json({ ok: true });
    }

    return json({ error: "unknown_event" }, 400);
  },
};

Su Node.js, verifica allo stesso modo con createHmac e timingSafeEqual, sul corpo grezzo ricevuto dal tuo framework:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyAutopseo(rawBody, headers, secret) {
  const timestamp = String(headers["x-autopseo-timestamp"] || "");
  const given = String(headers["x-autopseo-signature"] || "");
  if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = "sha256=" + createHmac("sha256", secret).update(timestamp + ".").update(rawBody).digest("hex");
  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

Passo successivo: ciò che arriva al webhook è deciso da scrittura e revisione e dalla manutenzione; l'intero flusso è nella panoramica del content engine.

Metti i tuoi contenuti SEO in pilota automatico

Collega Search Console e il tuo sito. AutoPSEO lo analizza e pianifica subito i primi argomenti. Nessuna carta di credito richiesta.