Sari la conținut
Documentație

Webhook de livrare: primirea articolelor pe propriul site

Webhook-ul semnat prin care AutoPSEO trimite articole către site-ul tău: antete, fiecare eveniment și payload, răspunsurile de dat, reîncercări, comenzi în așteptare și IndexNow.

Proprietate → Conținut → Unde sunt publicate articolele20 min de citit

AutoPSEO trimite fiecare articol pe care îl publică, îl actualizează sau îl retrage către propriul tău site, sub forma unui POST HTTPS semnat. Site-ul tău stochează articolul și îl afișează cu propriile șabloane, astfel că conținutul rămâne în sistemul tău și continuă să funcționeze chiar dacă renunți vreodată la AutoPSEO. Această pagină este referința pentru dezvoltatorul care construiește receptorul: semnarea, fiecare eveniment cu payload-ul său, răspunsurile pe care le citește AutoPSEO, reîncercările, comenzile în așteptare și IndexNow.

Configurarea webhook-ului

  1. Deschide pagina Conținut a proprietății și mergi la Unde se publică articolele.
  2. Completează Adresa webhook-ului pe site-ul tău. Trebuie să fie un URL https:// pe un nume de gazdă public: adresele IP și localhost sunt refuzate. AutoPSEO nu urmează redirecționări, așa că indică adresa finală.
  3. Completează Adresa site-ului tău, de exemplu https://example.com. Calea fiecărui tip de conținut se adaugă sub ea, deci un articol de tipul de la /answers se află la https://example.com/answers/<slug>.
  4. Alege Salvează și trimite un test. AutoPSEO creează un secret de semnare (whsec_ urmat de 48 de caractere hexazecimale) și trimite un ping la adresa ta. Un receptor care verifică semnăturile va pica acest prim ping, pentru că nu are încă secretul.
  5. Afișează Secretul de semnare cu Arată sau Copiază, păstrează-l ca secret pe site-ul tău, apoi alege Trimite un test.
  6. Citește Ce gestionează site-ul tău: fiecare comandă apare ca Conectată, Neconectată, Declarată, netestată sau Netestată.

Proprietățile noi pornesc cu articolele găzduite de AutoPSEO la propria adresă (vezi Găzduire), iar găzduirea și webhook-ul pot funcționa în paralel. Odată ce site-ul tău afișează articolele, alege Oprește găzduirea pe AutoPSEO: fiecare articol live se mută la adresa site-ului tău și este trimis către webhook ca article.published. Butonul cere un webhook care a răspuns la ultimul test și adresa site-ului tău. Cât timp găzduirea este oprită, webhook-ul nu poate fi eliminat.

Secret nou înlocuiește imediat secretul de semnare, iar cel vechi încetează să mai fie verificat pe loc. Pune noul secret pe site-ul tău imediat după ce l-ai generat.

AutoPSEO pune livrări în coadă doar pentru un webhook al cărui ultim ping a primit un răspuns 2xx. Un ping eșuat (la salvare, la Trimite un test sau la începutul unei scanări a site-ului) marchează webhook-ul ca fiind în eroare, iar articolele publicate cât timp este în eroare nu sunt puse în coadă pentru el. Menține ping funcțional și alege Trimite un test după ce ai remediat o pană.

Antete și semnătură

Fiecare cerere este un POST cu corp JSON și aceste antete:

AntetValoare
Content-Typeapplication/json
User-AgentAutoPSEO-Webhook/1
X-AutoPSEO-EventNumele evenimentului, prezent și în corp ca event
X-AutoPSEO-DeliveryId-ul livrării, prezent și în corp ca id. Același la fiecare reîncercare a unei livrări
X-AutoPSEO-TimestampTimpul Unix, în secunde, la care a fost semnată această încercare
X-AutoPSEO-Signaturesha256= urmat de HMAC-SHA256 în hexazecimal al șirului <timestamp>.<raw body>

Pentru a verifica o cerere:

  1. Ia antetul cu timestamp-ul, un punct și corpul cererii brut, exact așa cum a sosit, înainte de orice parsare JSON.
  2. Calculează HMAC-SHA256 peste acest șir. Cheia este întregul secret de semnare, așa cum îl afișează aplicația, cu tot prefixul whsec_, ca octeți UTF-8.
  3. Codifică rezultatul în hexazecimal și compară-l cu partea din X-AutoPSEO-Signature de după sha256=, în timp constant. Respinge cererea dacă diferă.

Fiecare încercare este semnată din nou, cu propriul timestamp, așa că poți refuza cererile al căror timestamp se îndepărtează mult de ceasul tău (cinci minute este o fereastră uzuală) fără a afecta reîncercările. HMAC în sine este definit în RFC 2104.

Fiecare corp conține id, event, occurredAt (ISO 8601, UTC) și site: { propertyId, domain }.

Evenimente

EvenimentTipCând îl trimite AutoPSEO
pingVerificareCând salvezi webhook-ul, la Trimite un test și la începutul fiecărei scanări a site-ului
testVerificareImediat după un ping reușit, câte unul pentru fiecare comandă
article.publishedLivrareUn articol devine live pentru prima dată; fiecare articol live, când oprești găzduirea
article.updatedLivrareO revizie nouă devine live (reîmprospătare, optimizare, revenire la o versiune anterioară, republicare după o retragere, adăugare de limbi) sau a fost adăugată o copertă
article.unpublishedLivrareUn articol este retras sau comasat într-o altă pagină
path.addLivrareA fost aprobată o valoare nouă pentru un tip de conținut tipizat
path.removeLivrareCineva a cerut retragerea căii unei valori
path.checkApelÎnainte ca AutoPSEO să planifice o pagină la o adresă la care site-ul tău nu are nicio pagină sau să propună o valoare nouă
site.exportApelÎn timpul unei scanări a site-ului, la sincronizarea zilnică a subiectelor și înainte ca AutoPSEO să modifice un articol live
articles.adoptedApelDupă ce o scanare a importat articole exportate de site-ul tău
site.handoverApelCând cineva predă scrierea tipurilor de conținut către AutoPSEO sau o preia înapoi

Livrările trec printr-un outbox: sunt înregistrate în același lot de bază de date ca modificarea, sunt trimise imediat, reîncercate și pot aștepta site-ul tău. Apelurile sunt simple cereri cu răspuns: AutoPSEO citește răspunsul pe loc. Momentul în care se trimit comenzile de tip cale este explicat în Pagini tipizate și căi noi.

Payload-ul articolului

article.published și article.updated conțin articolul întreg, cu toate limbile:

{
  "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
    }
  }
}

Intrarea în germană este prescurtată aici; fiecare traducere are aceeași structură ca copy.

CâmpSemnificație
idId-ul articolului în AutoPSEO. Nu se schimbă niciodată, deci este o cheie sigură
slugUltima parte a adresei. Pe paginile tipizate conține segmente unite prin /, de exemplu google/software-engineer
pathAdresa relativă la site-ul tău: calea tipului plus slug-ul. Rămâne fixă după ce pagina există
type{ key, name, path }. key este stabil; path este locul unde se află articolele tipului (gol pentru rădăcina site-ului)
lang, langsLimba principală și toate limbile acestei revizii
revNumărul reviziei
urlAdresa completă, așa cum o cunoaște AutoPSEO: cea a site-ului tău după oprirea găzduirii sau url-ul la care a răspuns ultima dată receptorul tău
datePublished, dateModifiedYYYY-MM-DD. Data publicării rămâne fixă de-a lungul reviziilor ulterioare
contentHashO amprentă a reviziei
writerCine a scris revizia: platform (AutoPSEO), mcp (propriul tău AI), user, import (textul propriu al site-ului tău) sau aggregate (o pagină de listă)
copyArticolul în limba principală
translationsCelelalte limbi, indexate după codul limbii
categoryCategoria articolului pe site-ul tău, dacă formatul tău păstrează categorii
topic{ externalId, keyword } când articolul a fost scris pentru un subiect predat de site-ul tău, ca să poți închide acel subiect
partsPe paginile tipizate, valoarea fiecărei părți: { "company": "google", "role": "software-engineer" }
kind"list" pe o pagină de listă. Conținutul ei este { title, h1, items: [{ from, text }] }, unde from este calea paginii din care provine fiecare element, iar parts indică valoarea pe care o grupează
coverO copertă făcută de AutoPSEO pentru articol. Descarc-o o singură dată și păstrează propria copie; adresa nu se schimbă niciodată, fiindcă numele fișierului este SHA-256 al imaginii
basedOnPe article.updated: versiunea paginii tale de la care a pornit această revizie (vezi conflictele mai jos)
redirectToPe article.unpublished după o comasare: unde duce acum vechea adresă. Răspunde cu un 301 și nu publica nimic nou acolo

article.unpublished conține aceleași câmpuri article, cu excepția copy, translations, category și cover.

În interiorul lui copy

CâmpSemnificație
title, metaDescription, h1Titlul paginii, meta descrierea și titlul principal
directAnswerRăspunsul scurt afișat primul
keyTakeawaysO listă de șiruri de caractere
sections[{ heading, bodyHtml }]
faq[{ q, a }]
sources[{ title, url, lastVerified? }]
questionOpțional: întrebarea căutătorului, în cuvintele lui, când formatul tău o cere
categoryOpțional: una dintre categoriile site-ului tău
listsOpțional: liste de text simplu, indexate după cheie, pentru tipurile de conținut care le cer (întrebările-exemplu dintr-un ghid de interviu)

În articolele scrise de AutoPSEO, bodyHtml folosește doar p, ul, li, strong, em, a și code, iar linkurile păstrează doar href. Textul preluat de pe propriul tău site își păstrează HTML-ul. Trece-l și printr-un sanitizer propriu dacă șabloanele tale îl cer.

Ce răspunde receptorul tău

Orice 2xx încheie o livrare. AutoPSEO citește și un obiect JSON opțional din corpul răspunsului; ține-l sub 64 KB, deoarece un răspuns mai mare contează ca eșec.

CâmpEfect
urlAdresa https:// la care se află acum articolul. AutoPSEO o înregistrează ca URL-ul articolului
versionVersiunea paginii tale, un șir opac de cel mult 128 de caractere. Revine ca basedOn la următoarea actualizare
urlsAdresele https:// modificate de această livrare, pentru IndexNow (toate limbile sau adresele pe care ai încetat să le servești). [] înseamnă că nimic nu s-a schimbat
cover"wanted" când nu ai o copertă pentru articol, iar livrarea nu a adus niciuna. AutoPSEO face una și o trimite într-un article.updated

Răspunsurile de eroare au semnificații diferite:

Răspunsul tăuCum îl citește AutoPSEOCe se întâmplă
Un 4xx cu { "error": "unknown_event" } sau un 501Nu gestionezi încă această comandăLivrarea așteaptă, fără reîncercări
Un 409 cu { "error": "conflict" }Actualizarea a fost scrisă pornind de la o versiune pe care pagina ta nu o mai areLivrarea eșuează fără reîncercări, iar articolul este recitit de pe site-ul tău înainte de următoarea lui modificare
Un 4xx, altul decât 408 sau 429, cu { "error": "<code>", "details": [] } la path.add sau path.removeUn refuz cu motivul tăuÎnregistrat; vezi Pagini tipizate și căi noi
Orice altceva: niciun răspuns în 10 secunde, o eroare de rețea, o redirecționare, 408, 429, 5xx sau alt 4xxO defecțiune temporarăReîncercat cu backoff

Reîncercări și livrări înlocuite

O livrare este trimisă imediat ce modificarea este salvată, iar un program la fiecare cinci minute trimite tot ce este scadent, începând cu cele mai vechi. După o defecțiune temporară, AutoPSEO așteaptă 1 minut, apoi 5 minute, 15 minute, 1 oră, 3 ore, 6 ore și 12 ore între încercări. A opta încercare eșuată marchează livrarea ca eșuată, iar pagina Conținut o numără într-o insignă roșie de lângă webhook.

Când există o livrare mai nouă pentru același articol, una mai veche aflată încă în așteptare este marcată ca înlocuită și nu mai este trimisă niciodată. Fiecare livrare conține articolul întreg, așa că cea mai nouă este tot ce are nevoie site-ul tău. Comenzile de tip cale despre aceeași valoare funcționează la fel.

Receptorul tău poate vedea o livrare de două ori, de exemplu când a stocat articolul, dar a răspuns după timeout-ul de 10 secunde. Deduplică după X-AutoPSEO-Delivery sau fă fiecare scriere idempotentă, stocând după article.id. article.updated poate ajunge și la un articol pe care l-ai șters după un article.unpublished, fiindcă republicarea unui articol retras trimite o actualizare: tratează actualizările ca „creează sau înlocuiește”.

Comenzi pe care site-ul tău nu le gestionează încă

Un site poate implementa protocolul comandă cu comandă. AutoPSEO păstrează starea fiecărei comenzi și o află în trei moduri:

  • Teste. După fiecare ping reușit, AutoPSEO trimite câte un test pentru fiecare comandă. Corpul este { id, event: "test", occurredAt, site, test: { event: "<command>", ... } }, unde test conține un exemplu cu forma comenzii reale și referitor la <type path>/autopseo-connection-test, o adresă pe care niciun site nu o are. Verifică-l ca pe comanda reală, nu modifica nimic și răspunde cu un 2xx: { "test": "<command>", "ok": true } pentru Conectată, { "test": "<command>", "ok": false, "error": "unknown_event" } pentru Neconectată. Un răspuns cu ok: false și alt cod de eroare, cum ar fi retragerea unei adrese pe care nu o ai, contează tot ca „conectată”. Exemplul se află sub test, niciodată la nivelul superior, așa că un receptor care nu știe nimic despre teste nu găsește în el niciun articol.
  • Livrări și apeluri reale. Un 2xx marchează comanda ca fiind conectată; unknown_event o marchează ca neconectată.
  • Răspunsul la ping. Comenzile pe care răspunsul tău le listează în autopseo.events apar ca Declarată, netestată până când un test sau o livrare spune mai mult.

O livrare pentru o comandă neconectată așteaptă. Pagina Conținut listează ce așteaptă, de exemplu „Site-ul tău nu retrage încă articole”, cu un buton Făcut pe site pentru orice ai efectuat manual; AutoPSEO nu va mai trimite niciodată acel element. Când un test sau o livrare ulterioară găsește comanda conectată, tot ce aștepta după ea este trimis.

Răspunsul la ping

Răspunde la ping cu un 2xx. Un site care doar primește articole nu are nevoie de nimic în plus. Ca să îi spui AutoPSEO ce mai face site-ul tău, include un obiect 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"
  }
}
CâmpSemnificație
version1. Obiectul este ignorat dacă versiunea nu este cel puțin 1
eventsComenzile pe care le gestionezi, afișate ca declarate până sunt testate
coverstrue când paginile tale afișează o copertă la fiecare articol. Atunci autorul AutoPSEO face coperta în timp ce scrie, deci prima livrare o conține, iar propriului tău AI i se cere să furnizeze una. Vezi Coperte
indexNow{ key, keyLocation? }: cheia IndexNow pe care o servești (8 până la 128 de litere, cifre și cratime) și, când fișierul cheie nu se află la /<key>.txt, locația lui https://
exportCe predă site.export: articles, topics, profile
adoptAccepți articles.adopted, astfel că o scanare completă poate prelua articolele tale existente
handoverAccepți site.handover
typesCheile tipurilor de conținut pe care le accepți (până la 50)
writerCine scrie astăzi articolele tale noi: site sau autopseo

IndexNow pentru adresele pe care le servești

Când răspunsul tău la ping declară indexNow, AutoPSEO face ping la IndexNow, la api.indexnow.org, după livrările pe care le accepți, iar site-ul tău nu mai trimite el însuși niciun ping.

  • Trimite urls din răspunsul tău. Fără urls, trimite url din răspunsul tău, altfel URL-ul articolului. urls: [] nu trimite nimic.
  • Se includ doar adresele de pe propria ta gazdă, așa cum cere protocolul IndexNow: gazda din keyLocation, altfel gazda adresei site-ului tău.
  • Fiecare rulare de livrare trimite un singur ping pe site, cu până la 10.000 de adrese.
  • 200 și 202 sunt acceptate. Un 429, un 5xx sau lipsa unui răspuns păstrează adresele și reîncearcă după o oră, dublând intervalul de fiecare dată până la o zi (mai mult când Retry-After o cere), împreună cu ce au adăugat livrările ulterioare. Un 403 (cheie nevalidă) sau un 422 (adrese care nu sunt pe gazdă) este înregistrat, iar adresele sunt abandonate.
  • Pagina Conținut arată ultimul ping: când, câte adrese, statusul HTTP și următoarea încercare.
  • Cât timp AutoPSEO găzduiește și el articolele, paginile găzduite primesc ping cu cheia proprie a AutoPSEO, iar a ta nu este folosită.

Instrumentul IndexNow din secțiunea Indexare este un trimițător manual separat; vezi IndexNow.

Conflicte: textul site-ului tău este originalul

version din răspunsul tău (sau version al unui articol din exportul tău) este ceea ce crede AutoPSEO că este pagina ta. Fiecare article.updated îl poartă ca basedOn. Dacă pagina s-a schimbat între timp pe site-ul tău, de exemplu după o corectură a unui editor, răspunde cu 409 și { "error": "conflict" } și nu stoca nimic. Livrarea eșuează fără reîncercări.

Înainte ca o modificare aflată în coadă să fie scrisă pe un articol live, AutoPSEO recitește acel articol de pe site-ul tău cu site.export și paths, dacă site-ul tău gestionează site.export. Dacă versiunea ta a evoluat, textul tău devine următoarea revizie a articolului (writer import), iar actualizarea care urmează poartă versiunea ta curentă ca basedOn.

path.add și path.remove

Aceste două livrări privesc o singură valoare a unui tip de conținut tipizat, de exemplu o companie în /interviews/{company}/{role}. Ele poartă un obiect path în loc de 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"] }
  }
}
CâmpSemnificație
pathAdresa proprie a valorii pe site-ul tău
typeTipul de conținut, ca { key, name, path }
partPartea din adresă pe care o ocupă valoarea
value{ value, name }, plus, la path.add, aliases, url, sameAs, description și facts adunate de autorul AutoPSEO. Fiecare fapt are text, source, lastVerified și un tier care poate fi official sau reported
takesLa path.add: valorile celorlalte părți după care caută oamenii împreună cu aceasta
redirectToLa path.remove: unde trebuie să ducă de acum înainte adresele valorii, cu un 301

Răspunde cu un 2xx când ai preluat comanda; la path.add poți adăuga url-ul la care se află acum valoarea, iar la path.remove urls pe care ai încetat să le servești, care merg la IndexNow. Ca să refuzi, răspunde cu un 4xx și { "error": "<code>", "details": ["<why>"] }. La path.add, invalid_value trimite valoarea înapoi la autor cu motivul tău (al treilea refuz este definitiv), iar orice alt cod o refuză definitiv. La path.remove, not_found contează ca realizat, iar orice alt cod, cum ar fi 409 in_use, lasă totul neschimbat. Marcarea unei comenzi de cale aflate în așteptare drept Făcut pe site contează ca un da din partea site-ului tău.

Apeluri: cereri al căror răspuns îl citește AutoPSEO

Apelurile sunt semnate ca livrările. Un apel așteaptă implicit până la 60 de secunde și 30 de secunde pentru recitiri și verificări de cale, și citește răspunsuri de până la 24 MB. Orice răspuns diferit de 2xx face apelul să eșueze.

site.export

Cererea conține export: { what, cursor, limit }, plus since (o oră ISO) pentru subiecte sau paths pentru o recitire. Răspunde cu { "items": [], "next": "<cursor>" }; AutoPSEO cere din nou, cu cursor setat la next-ul tău, până când acesta este gol sau null.

  • what: "articles" rulează la scanările complete ale site-ului, câte 10 pe pagină, când ai declarat export cu articles și adopt: true. Un răspuns 400 înseamnă „nesuportat”. Cu paths: ["/answers/x"] și limit: 1 recitește un singur articol; răspunde doar cu acel articol.
  • what: "topics" rulează la fiecare scanare și la sincronizarea zilnică, câte 50 pe pagină; sincronizarea zilnică trimite since, momentul ultimei preluări.
  • what: "profile" rulează la fiecare scanare. Răspunde cu { "profile": { types, languages, rules, categories, format } }, fiecare opțional: types ca [{ key, name, path }], iar rules cu brandTerms, synonyms, noise, onTopic și offTopic.

Un articol exportat are nevoie de externalId, un slug (litere și cifre din orice scriere și cratime, cel mult 200 de caractere, segmente unite prin /), un path care se termină cu acel slug și copy cu cel puțin title, h1 și o secțiune. Câmpuri opționale: lang (implicit en), type (o cheie de tip de conținut), url, keyword, category, datePublished, dateModified, version, translations, owner (id-ul articolului în AutoPSEO, după adoptare) și maintenance: { kind: "refresh" or "optimize", reason }, care pune în coadă acea sarcină. Fiecare articol exportat devine un articol gestionat, cu textul tău ca primă revizie, și nu se trimite nimic înapoi. Exportul repetat este sigur, iar un articol retras de AutoPSEO nu mai revine niciodată printr-un export.

Un element de subiect este { keyword, status?, volume?, source?, externalId?, type?, reason? }, unde status este queued, planned sau rejected.

articles.adopted

După import, AutoPSEO îți cere să îi permiți să gestioneze articolele noi: { "articles": [{ "externalId", "articleId", "slug", "path" }] }. Răspunde cu { "adopted": ["<externalId>"], "refused": [{ "externalId": "<id>", "reason": "<why>" }] }. Un articol pe care nu îl confirmi este eliminat din nou din AutoPSEO, cu excepția cazului în care AutoPSEO a scris deja o revizie a lui.

site.handover

{ "handover": { "types": ["answers"], "writer": "autopseo" } }, trimis când cineva alege Lasă AutoPSEO să le scrie sau Predă scrierea înapoi site-ului pe pagina Site-ul tău a proprietății. Răspunde cu { "ok": true, "writer": "autopseo" }. După o predare către AutoPSEO, site-ul tău îi transmite subiectele pe care le validează, în loc să le scrie el.

path.check

{ "paths": [{ "path": "/interviews/google/data-engineer" }, { "path": "/interviews/jane-street", "new": { "part": "company", "value": "jane-street", "name": "Jane Street" } }] }. Răspunde cu câte un rezultat pentru fiecare cale: { "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 refuz este păstrat împreună cu motivul tău și nu mai este cerut din nou; o cale pe care o omiți este cerută din nou mai târziu.

Un receptor minimal

Un Cloudflare Worker care verifică semnătura cu Web Crypto API, gestionează cele trei comenzi pentru articole și le refuză pe celelalte cu unknown_event. Păstrează articolele într-un namespace KV legat ca ARTICLES, iar secretul de semnare într-un secret Worker numit 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);
  },
};

În Node.js, verifică la fel, cu createHmac și timingSafeEqual, peste corpul brut primit de framework-ul tău:

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));
}

Mai departe: ce ajunge la webhook este stabilit de scriere și revizuire și de mentenanță; fluxul complet este în prezentarea generală a motorului de conținut.

Pune conținutul tău SEO pe pilot automat

Conectează Search Console și site-ul tău. AutoPSEO îl scanează și planifică imediat primele subiecte. Nu ai nevoie de card de credit.