Přeskočit na obsah
Dokumentace

Doručovací webhook: příjem článků na vlastním webu

Podepsaný webhook, který odesílá články z AutoPSEO na váš web: hlavičky, jednotlivé události a payloady, odpovědi, které máte vracet, opakování, čekající příkazy a IndexNow.

Vlastnost → Obsah → Kde se články publikují18 min čtení

AutoPSEO odesílá každý článek, který publikuje, aktualizuje nebo stáhne, na váš vlastní web jako podepsaný HTTPS POST. Váš web článek uloží a vykreslí ho vlastními šablonami, takže obsah žije ve vašem systému a funguje dál, i kdybyste AutoPSEO někdy opustili. Tato stránka je referencí pro vývojáře, který staví příjemce: podepisování, všechny události s payloady, odpovědi, které AutoPSEO čte, opakování, čekající příkazy a IndexNow.

Nastavení webhooku

  1. Otevřete stránku Content dané property a přejděte do sekce Where articles are published.
  2. Vyplňte Webhook address on your site. Musí to být adresa https:// na veřejném názvu hostitele: IP adresy a localhost jsou odmítnuty. AutoPSEO nesleduje přesměrování, proto zadejte konečnou adresu.
  3. Vyplňte Your site's address, například https://example.com. Cesta každého typu obsahu na ni navazuje, takže článek typu na /answers najdete na https://example.com/answers/<slug>.
  4. Zvolte Save and send a test. AutoPSEO vytvoří podpisový tajný klíč (whsec_ následované 48 hexadecimálními znaky) a odešle na vaši adresu ping. Příjemce, který kontroluje podpisy, tento první ping nezvládne, protože tajný klíč ještě nemá.
  5. Zobrazte Signing secret pomocí Show nebo Copy, uložte ho na svém webu jako tajný klíč (secret) a poté zvolte Send a test.
  6. Přečtěte si What your site handles: každý příkaz ukazuje stav Connected, Not connected, Declared, not tested nebo Not tested.

Nové properties začínají tak, že články hostuje AutoPSEO na své vlastní adrese (viz Hosting), a hosting i webhook mohou běžet souběžně. Jakmile váš web články zobrazuje, zvolte Stop hosting on AutoPSEO: každý živý článek se přesune na adresu vašeho webu a odešle se na váš webhook jako article.published. Toto tlačítko vyžaduje webhook, který odpověděl na poslední test, a adresu vašeho webu. Dokud je hosting vypnutý, webhook nelze odebrat.

New secret okamžitě nahradí podpisový tajný klíč a starý přestane okamžitě ověřovat. Nový klíč nasaďte na svůj web hned po jeho vytvoření.

AutoPSEO řadí doručení do fronty jen pro webhook, jehož poslední ping byl zodpovězen kódem 2xx. Neúspěšný ping (při uložení, při Send a test nebo na začátku skenu webu) označí webhook jako chybný a články publikované během chyby se pro něj do fronty neřadí. Udržujte odpověď na ping funkční a po odstranění výpadku zvolte Send a test.

Hlavičky a podpis

Každý požadavek je POST s tělem ve formátu JSON a těmito hlavičkami:

HlavičkaHodnota
Content-Typeapplication/json
User-AgentAutoPSEO-Webhook/1
X-AutoPSEO-EventNázev události, také v těle jako event
X-AutoPSEO-DeliveryID doručení, také v těle jako id. Stejné při každém opakování jednoho doručení
X-AutoPSEO-TimestampUnixový čas v sekundách, kdy byl tento pokus podepsán
X-AutoPSEO-Signaturesha256= následované hexadecimálním HMAC-SHA256 řetězce <timestamp>.<raw body>

Ověření požadavku:

  1. Vezměte hlavičku s časovou značkou, tečku a surové tělo požadavku přesně tak, jak dorazilo, před jakýmkoli parsováním JSON.
  2. Spočítejte HMAC-SHA256 z tohoto řetězce. Klíčem je celý podpisový tajný klíč tak, jak ho zobrazuje aplikace, včetně prefixu whsec_, jako bajty UTF-8.
  3. Výsledek převeďte do hexadecimálního zápisu a konstantním časem ho porovnejte s částí X-AutoPSEO-Signature za sha256=. Pokud se liší, požadavek odmítněte.

Každý pokus je podepsán znovu s vlastní časovou značkou, takže můžete odmítat požadavky, jejichž časová značka se výrazně liší od vašich hodin (běžné je pětiminutové okno), aniž by se tím rozbila opakování. Samotný HMAC je definován v RFC 2104.

Každé tělo obsahuje id, event, occurredAt (ISO 8601, UTC) a site: { propertyId, domain }.

Události

UdálostDruhKdy ji AutoPSEO odešle
pingKontrolaPři uložení webhooku, při Send a test a na začátku každého skenu webu
testKontrolaHned po úspěšném pingu, jeden pro každý příkaz
article.publishedDoručeníČlánek poprvé vyjde; každý živý článek při ukončení hostingu
article.updatedDoručeníVyšla nová revize (obnova, optimalizace, vrácení zpět, opětovné zveřejnění po stažení, přidané jazyky) nebo byl přidán cover
article.unpublishedDoručeníČlánek je stažen nebo sloučen do jiné stránky
path.addDoručeníByla schválena nová hodnota typovaného typu obsahu
path.removeDoručeníNěkdo požádal o stažení cesty dané hodnoty
path.checkVoláníNež AutoPSEO naplánuje stránku na adrese, na které váš web žádnou stránku nemá, nebo navrhne novou hodnotu
site.exportVoláníBěhem skenu webu, při denní synchronizaci témat a před změnou živého článku
articles.adoptedVoláníPoté, co sken naimportoval články exportované vaším webem
site.handoverVoláníKdyž někdo předá psaní typů obsahu AutoPSEO, nebo zpět

Doručení procházejí přes outbox: zapisují se do stejné databázové dávky jako změna, odesílají se okamžitě, opakují se a mohou čekat na váš web. Volání jsou prostý požadavek a odpověď: AutoPSEO čte odpověď hned. Kdy se příkazy pro cesty odesílají, vysvětluje stránka Typed pages and new paths.

Payload článku

article.published a article.updated nesou celý článek včetně všech jazyků:

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

Německý záznam je zde zkrácen; každý překlad má stejný tvar jako copy.

PoleVýznam
idID článku v AutoPSEO. Nikdy se nemění, takže je bezpečným klíčem
slugPoslední část adresy. U typovaných stránek obsahuje segmenty spojené znakem /, například google/software-engineer
pathAdresa relativní k vašemu webu: cesta typu plus slug. Jakmile stránka existuje, je pevná
type{ key, name, path }. key je stabilní; path je místo, kde žijí články daného typu (pro kořen webu prázdná)
lang, langsPrimární jazyk a všechny jazyky této revize
revČíslo revize
urlÚplná adresa, jak ji zná AutoPSEO: adresa vašeho webu, jakmile je hosting vypnutý, nebo url, kterou váš příjemce naposledy vrátil
datePublished, dateModifiedYYYY-MM-DD. Datum publikace zůstává při dalších revizích stejné
contentHashOtisk revize
writerKdo revizi napsal: platform (AutoPSEO), mcp (vaše vlastní AI), user, import (vlastní text vašeho webu) nebo aggregate (stránka se seznamem)
copyČlánek v primárním jazyce
translationsOstatní jazyky, indexované kódem jazyka
categoryKategorie vašeho webu pro tento článek, pokud váš formát kategorie používá
topic{ externalId, keyword }, když byl článek napsán k tématu, které váš web předal, abyste mohli toto téma uzavřít
partsU typovaných stránek hodnota každé části: { "company": "google", "role": "software-engineer" }
kind"list" u stránky se seznamem. Její copy je { title, h1, items: [{ from, text }] }, kde from je cesta stránky, z níž každá položka pochází, a parts pojmenovává hodnotu, kterou seznam sdružuje
coverCover, který pro článek vytvořilo AutoPSEO. Stáhněte si ho jednou a uchovejte vlastní kopii; adresa se nikdy nemění, protože název souboru je SHA-256 obrázku
basedOnU article.updated: verze vaší stránky, z níž tato revize vznikla (viz konflikty níže)
redirectToU article.unpublished po sloučení: kam nyní stará adresa vede. Odpovězte přesměrováním 301 a nic nového tam neukládejte

article.unpublished nese stejná pole article kromě copy, translations, category a cover.

Obsah copy

PoleVýznam
title, metaDescription, h1Titulek stránky, meta description a nadpis
directAnswerStručná odpověď zobrazená jako první
keyTakeawaysSeznam řetězců
sections[{ heading, bodyHtml }]
faq[{ q, a }]
sources[{ title, url, lastVerified? }]
questionVolitelné: otázka hledajícího jeho slovy, pokud ji váš formát vyžaduje
categoryVolitelné: jedna z kategorií vašeho webu
listsVolitelné: prostté seznamy podle klíče pro typy obsahu, které je vyžadují (ukázkové otázky v průvodci pohovorem)

V článcích, které píše AutoPSEO, bodyHtml používá jen p, ul, li, strong, em, a a code a odkazy si ponechávají pouze href. Text převzatý z vašeho vlastního webu si ponechává vaše HTML. Pokud to vaše šablony vyžadují, projděte ho i vlastním sanitizérem.

Co váš příjemce odpovídá

Doručení dokončí jakýkoli kód 2xx. AutoPSEO také čte volitelný objekt JSON z těla odpovědi; držte ho pod 64 KB, protože větší odpověď se počítá jako selhání.

PoleÚčinek
urlAdresa https://, na které článek nyní žije. AutoPSEO ji zaznamená jako URL článku
versionVerze vaší stránky, neprůhledný řetězec o délce nejvýše 128 znaků. Vrátí se jako basedOn při další aktualizaci
urlsAdresy https://, které toto doručení změnilo, pro IndexNow (všechny jazyky, nebo adresy, které jste přestali obsluhovat). [] znamená, že se nic nezměnilo
cover"wanted", když pro článek nemáte cover a doručení žádný nenese. AutoPSEO ho vytvoří a pošle v article.updated

Chybové odpovědi znamenají různé věci:

Vaše odpověďJak ji AutoPSEO čteCo se stane
4xx s { "error": "unknown_event" } nebo 501Tento příkaz zatím neobsluhujeteDoručení čeká, bez opakování
409 s { "error": "conflict" }Aktualizace vznikla z verze, kterou už vaše stránka nemáDoručení selže bez opakování a článek se před další změnou znovu přečte z vašeho webu
4xx jiné než 408 nebo 429 s { "error": "<code>", "details": [] } na path.add nebo path.removeZamítnutí s vaším důvodemZaznamenáno; viz Typed pages and new paths
Cokoli jiného: žádná odpověď do 10 sekund, chyba sítě, přesměrování, 408, 429, 5xx nebo jiné 4xxDočasné selháníOpakuje se s odstupňovanou prodlevou

Opakování a nahrazená doručení

Doručení se odešle, jakmile je změna uložena, a pětiminutový plánovač odesílá vše, co je na řadě, od nejstaršího. Po dočasném selhání čeká AutoPSEO mezi pokusy 1 minutu, pak 5 minut, 15 minut, 1 hodinu, 3 hodiny, 6 hodin a 12 hodin. Osmý neúspěšný pokus označí doručení jako neúspěšné a stránka Content ho započítá do červeného odznaku vedle webhooku.

Pokud existuje novější doručení pro stejný článek, starší, které stále čeká, se označí jako nahrazené a nikdy se neodešle. Každé doručení nese celý článek, takže vašemu webu stačí to nejnovější. Příkazy pro cesty týkající se stejné hodnoty fungují stejně.

Váš příjemce může jedno doručení vidět dvakrát, například když článek uložil, ale odpověděl až po 10sekundovém timeoutu. Deduplikujte podle X-AutoPSEO-Delivery nebo zajistěte idempotenci každého zápisu ukládáním podle article.id. article.updated může také dorazit k článku, který jste po article.unpublished smazali, protože opětovné zveřejnění staženého článku odesílá aktualizaci: berte aktualizace jako „vytvořit nebo nahradit".

Příkazy, které váš web zatím neobsluhuje

Web může protokol implementovat po jednotlivých příkazech. AutoPSEO uchovává stav každého příkazu a zjišťuje ho třemi způsoby:

  • Testy. Po každém úspěšném pingu pošle AutoPSEO jeden test pro každý příkaz. Tělo je { id, event: "test", occurredAt, site, test: { event: "<command>", ... } }, kde test obsahuje ukázku ve tvaru skutečného příkazu a týká se <type path>/autopseo-connection-test, adresy, kterou žádný web nemá. Zkontrolujte ji jako skutečný příkaz, nic neměňte a odpovězte kódem 2xx: { "test": "<command>", "ok": true } pro Connected, { "test": "<command>", "ok": false, "error": "unknown_event" } pro Not connected. Odpověď s ok: false a jiným kódem chyby, například při stažení adresy, kterou nemáte, se stále počítá jako připojeno. Ukázka je vždy pod test, nikdy na nejvyšší úrovni, takže příjemce, který o testech nic neví, v ní žádný článek nenajde.
  • Skutečná doručení a volání. Kód 2xx označí příkaz jako připojený; unknown_event ho označí jako nepřipojený.
  • Odpověď na ping. Příkazy, které vaše odpověď uvádí v autopseo.events, se zobrazují jako Declared, not tested, dokud test nebo doručení neřeknou víc.

Doručení příkazu, který není připojen, čeká. Stránka Content vypisuje, co čeká, například „Your site does not take articles down yet", a u všeho, co jste provedli ručně, nabízí tlačítko Done on the site; AutoPSEO pak takové doručení už nikdy neodešle. Jakmile pozdější test nebo doručení zjistí, že je příkaz připojen, odešle se vše, co na něj čeká.

Odpověď na ping

Na ping odpovězte kódem 2xx. Web, který články jen přijímá, nepotřebuje nic dalšího. Pokud chcete AutoPSEO sdělit, co váš web umí dál, přidejte objekt 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"
  }
}
PoleVýznam
version1. Objekt je bez verze 1 nebo vyšší ignorován
eventsPříkazy, které obsluhujete, zobrazené jako deklarované, dokud nejsou otestovány
coverstrue, když vaše stránky zobrazují cover u každého článku. Autor v AutoPSEO pak vytváří cover již při psaní, takže ho nese hned první doručení, a vaše vlastní AI je vyzvána, aby ho dodala. Viz Covers
indexNow{ key, keyLocation? }: klíč IndexNow, který poskytujete (8 až 128 písmen, číslic a pomlček), a pokud soubor s klíčem není na /<key>.txt, jeho umístění https://
exportCo site.export předává: articles, topics, profile
adoptPřijímáte articles.adopted, takže úplný sken může převzít vaše stávající články
handoverPřijímáte site.handover
typesKlíče typů obsahu, které přijímáte (nejvýše 50)
writerKdo dnes píše vaše nové články: site nebo autopseo

IndexNow pro adresy, které obsluhujete

Když vaše odpověď na ping deklaruje indexNow, AutoPSEO po doručeních, která přijmete, pingne IndexNow na api.indexnow.org a váš web sám nepinguje nic.

  • Odešle urls, které vaše odpověď uvedla. Bez urls odešle url z vaší odpovědi, jinak URL článku. urls: [] neodešle nic.
  • Zahrnuty jsou jen adresy na vašem vlastním hostiteli, jak vyžaduje protokol IndexNow: hostitel z keyLocation, jinak hostitel adresy vašeho webu.
  • Každý běh doručení odešle jeden ping na web, nejvýše s 10 000 adresami.
  • 200 a 202 jsou přijaty. Kód 429, 5xx nebo žádná odpověď adresy zachová a zkusí to znovu za hodinu, pokaždé s dvojnásobnou prodlevou až do jednoho dne (déle, pokud o to požádá Retry-After), spolu s adresami, které přidala pozdější doručení. Kód 403 (neplatný klíč) nebo 422 (adresy nejsou na daném hostiteli) se zaznamená a adresy se zahodí.
  • Stránka Content ukazuje poslední ping: kdy proběhl, kolik adres obsahoval, stav HTTP a další pokus.
  • Dokud AutoPSEO články také hostuje, hostované stránky se pingují vlastním klíčem AutoPSEO a váš klíč se nepoužije.

Nástroj IndexNow v sekci Indexing je samostatný, ruční odesílač; viz IndexNow.

Konflikty: text vašeho webu je originál

version z vaší odpovědi (nebo version článku ve vašem exportu) je to, na jaké verzi si AutoPSEO myslí, že vaše stránka je. Každý article.updated ji nese jako basedOn. Pokud se stránka na vašem webu od té doby změnila, například po opravě redaktora, odpovězte 409 s { "error": "conflict" } a nic neukládejte. Doručení selže bez opakování.

Než se zapíše čekající změna živého článku, AutoPSEO tento článek znovu přečte z vašeho webu pomocí site.export s paths, pokud váš web site.export obsluhuje. Pokud se vaše verze posunula dál, váš text se stane další revizí článku (writer import) a následná aktualizace nese vaši aktuální verzi jako basedOn.

path.add a path.remove

Tato dvě doručení se týkají jedné hodnoty typovaného typu obsahu, například společnosti v /interviews/{company}/{role}. Místo article nesou objekt path:

{
  "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"] }
  }
}
PoleVýznam
pathVlastní adresa hodnoty na vašem webu
typeTyp obsahu jako { key, name, path }
partČást adresy, kterou hodnota vyplňuje
value{ value, name }, u path.add navíc aliases, url, sameAs, description a facts, které shromáždil autor v AutoPSEO. Každý fakt má text, source, lastVerified a tier s hodnotou official nebo reported
takesU path.add: hodnoty ostatních částí, s nimiž lidé tuto hodnotu hledají
redirectToU path.remove: kam mají adresy hodnoty odteď vést přesměrováním 301

Pokud jste příkaz převzali, odpovězte kódem 2xx; u path.add můžete přidat url, kde hodnota nyní žije, a u path.remove urls, které jste přestali obsluhovat a které putují do IndexNow. Pro odmítnutí odpovězte kódem 4xx s { "error": "<code>", "details": ["<why>"] }. U path.add kód invalid_value vrátí hodnotu autorovi s vaším důvodem (třetí odmítnutí je konečné) a jakýkoli jiný kód ji odmítne natrvalo. U path.remove se not_found počítá jako hotovo a jakýkoli jiný kód, například 409 in_use, nechá vše tak, jak bylo. Označení čekajícího příkazu pro cestu jako Done on the site se počítá jako souhlas vašeho webu.

Volání: požadavky, jejichž odpověď AutoPSEO čte

Volání se podepisují stejně jako doručení. Volání ve výchozím nastavení čeká až 60 sekund a 30 sekund u opětovných čtení a kontrol cest, a čte odpovědi až do 24 MB. Jakákoli odpověď jiná než 2xx volání selže.

site.export

Požadavek nese export: { what, cursor, limit } a navíc since (čas ve formátu ISO) u témat nebo paths u opětovného čtení. Odpovězte { "items": [], "next": "<cursor>" }; AutoPSEO se ptá znovu s cursor nastaveným na vaše next, dokud není prázdné nebo null.

  • what: "articles" běží při úplných skenech webu, po 10 na stránku, pokud jste deklarovali export s articles a adopt: true. Odpověď 400 znamená „nepodporováno". S paths: ["/answers/x"] a limit: 1 znovu přečte jeden článek; odpovězte pouze tímto článkem.
  • what: "topics" běží při každém skenu a v denní synchronizaci, po 50 na stránku; denní synchronizace posílá since, čas posledního stažení.
  • what: "profile" běží při každém skenu. Odpovězte { "profile": { types, languages, rules, categories, format } }, přičemž každé pole je volitelné: types jako [{ key, name, path }] a rules s brandTerms, synonyms, noise, onTopic a offTopic.

Exportovaný článek potřebuje externalId, slug (písmena a číslice libovolného písma a pomlčky, nejvýše 200 znaků, segmenty spojené znakem /), path končící tímto slugem a copy alespoň s title, h1 a jednou sekcí. Volitelná pole: lang (výchozí en), type (klíč typu obsahu), url, keyword, category, datePublished, dateModified, version, translations, owner (ID článku v AutoPSEO, po převzetí) a maintenance: { kind: "refresh" or "optimize", reason }, což zařadí příslušnou úlohu do fronty. Každý exportovaný článek se stane spravovaným článkem s vaším textem jako první revizí a nic se neposílá zpět. Opakovaný export je bezpečný a článek, který AutoPSEO stáhlo, se přes export nikdy nevrátí.

Položka tématu je { keyword, status?, volume?, source?, externalId?, type?, reason? }, kde status je queued, planned nebo rejected.

articles.adopted

Po importu vás AutoPSEO požádá, abyste mu dovolili nové články spravovat: { "articles": [{ "externalId", "articleId", "slug", "path" }] }. Odpovězte { "adopted": ["<externalId>"], "refused": [{ "externalId": "<id>", "reason": "<why>" }] }. Článek, který nepotvrdíte, se z AutoPSEO znovu odstraní, pokud k němu AutoPSEO již nenapsalo revizi.

site.handover

{ "handover": { "types": ["answers"], "writer": "autopseo" } }, odesílá se, když někdo na stránce Your site dané property zvolí Let AutoPSEO write them nebo Hand writing back to the site. Odpovězte { "ok": true, "writer": "autopseo" }. Po předání AutoPSEO váš web témata, která prověřuje, předává AutoPSEO místo toho, aby je psal sám.

path.check

{ "paths": [{ "path": "/interviews/google/data-engineer" }, { "path": "/interviews/jane-street", "new": { "part": "company", "value": "jane-street", "name": "Jane Street" } }] }. Odpovězte jedním výsledkem na cestu: { "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."] }] }. Zamítnutí se uchová s vaším důvodem a znovu se na něj neptáme; cesta, kterou vynecháte, se zeptá znovu později.

Minimální příjemce

Cloudflare Worker, který ověřuje podpis pomocí Web Crypto API, obsluhuje tři příkazy pro články a ostatní odmítá jako unknown_event. Články ukládá do jmenného prostoru KV navázaného jako ARTICLES a podpisový tajný klíč drží v tajném klíči Workeru s názvem 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);
  },
};

V Node.js ověřujte stejným způsobem pomocí createHmac a timingSafeEqual nad surovým tělem, které váš framework přijal:

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

Dále: to, co se dostane na webhook, určují psaní a revize a údržba; celý průběh najdete v přehledu content engine.

Převeďte svůj SEO obsah na autopilota

Připojte Search Console a svůj web. AutoPSEO jej prohledá a hned naplánuje první témata. Bez platební karty.