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.
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
- Otevřete stránku Content dané property a přejděte do sekce Where articles are published.
- Vyplňte Webhook address on your site. Musí to být adresa
https://na veřejném názvu hostitele: IP adresy alocalhostjsou odmítnuty. AutoPSEO nesleduje přesměrování, proto zadejte konečnou adresu. - 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/answersnajdete nahttps://example.com/answers/<slug>. - 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á. - Zobrazte Signing secret pomocí Show nebo Copy, uložte ho na svém webu jako tajný klíč (secret) a poté zvolte Send a test.
- 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čka | Hodnota |
|---|---|
Content-Type | application/json |
User-Agent | AutoPSEO-Webhook/1 |
X-AutoPSEO-Event | Název události, také v těle jako event |
X-AutoPSEO-Delivery | ID doručení, také v těle jako id. Stejné při každém opakování jednoho doručení |
X-AutoPSEO-Timestamp | Unixový čas v sekundách, kdy byl tento pokus podepsán |
X-AutoPSEO-Signature | sha256= následované hexadecimálním HMAC-SHA256 řetězce <timestamp>.<raw body> |
Ověření požadavku:
- 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.
- 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. - Výsledek převeďte do hexadecimálního zápisu a konstantním časem ho porovnejte s částí
X-AutoPSEO-Signaturezasha256=. 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álost | Druh | Kdy ji AutoPSEO odešle |
|---|---|---|
ping | Kontrola | Při uložení webhooku, při Send a test a na začátku každého skenu webu |
test | Kontrola | Hned po úspěšném pingu, jeden pro každý příkaz |
article.published | Doručení | Článek poprvé vyjde; každý živý článek při ukončení hostingu |
article.updated | Doruč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.unpublished | Doručení | Článek je stažen nebo sloučen do jiné stránky |
path.add | Doručení | Byla schválena nová hodnota typovaného typu obsahu |
path.remove | Doručení | Někdo požádal o stažení cesty dané hodnoty |
path.check | Volání | Než AutoPSEO naplánuje stránku na adrese, na které váš web žádnou stránku nemá, nebo navrhne novou hodnotu |
site.export | Volání | Během skenu webu, při denní synchronizaci témat a před změnou živého článku |
articles.adopted | Volání | Poté, co sken naimportoval články exportované vaším webem |
site.handover | Volá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.
| Pole | Význam |
|---|---|
id | ID článku v AutoPSEO. Nikdy se nemění, takže je bezpečným klíčem |
slug | Poslední část adresy. U typovaných stránek obsahuje segmenty spojené znakem /, například google/software-engineer |
path | Adresa 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, langs | Primá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, dateModified | YYYY-MM-DD. Datum publikace zůstává při dalších revizích stejné |
contentHash | Otisk revize |
writer | Kdo 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 |
translations | Ostatní jazyky, indexované kódem jazyka |
category | Kategorie 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 |
parts | U 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 |
cover | Cover, 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 |
basedOn | U article.updated: verze vaší stránky, z níž tato revize vznikla (viz konflikty níže) |
redirectTo | U 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
| Pole | Význam |
|---|---|
title, metaDescription, h1 | Titulek stránky, meta description a nadpis |
directAnswer | Stručná odpověď zobrazená jako první |
keyTakeaways | Seznam řetězců |
sections | [{ heading, bodyHtml }] |
faq | [{ q, a }] |
sources | [{ title, url, lastVerified? }] |
question | Volitelné: otázka hledajícího jeho slovy, pokud ji váš formát vyžaduje |
category | Volitelné: jedna z kategorií vašeho webu |
lists | Volitelné: 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 |
|---|---|
url | Adresa https://, na které článek nyní žije. AutoPSEO ji zaznamená jako URL článku |
version | Verze vaší stránky, neprůhledný řetězec o délce nejvýše 128 znaků. Vrátí se jako basedOn při další aktualizaci |
urls | Adresy 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 čte | Co se stane |
|---|---|---|
4xx s { "error": "unknown_event" } nebo 501 | Tento příkaz zatím neobsluhujete | Doruč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.remove | Zamítnutí s vaším důvodem | Zaznamená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é 4xx | Doč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
testpro každý příkaz. Tělo je{ id, event: "test", occurredAt, site, test: { event: "<command>", ... } }, kdetestobsahuje 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ěď sok: falsea 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 podtest, 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_eventho 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"
}
}
| Pole | Význam |
|---|---|
version | 1. Objekt je bez verze 1 nebo vyšší ignorován |
events | Příkazy, které obsluhujete, zobrazené jako deklarované, dokud nejsou otestovány |
covers | true, 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:// |
export | Co site.export předává: articles, topics, profile |
adopt | Přijímáte articles.adopted, takže úplný sken může převzít vaše stávající články |
handover | Přijímáte site.handover |
types | Klíče typů obsahu, které přijímáte (nejvýše 50) |
writer | Kdo 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. Bezurlsodešleurlz 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"] }
}
}
| Pole | Význam |
|---|---|
path | Vlastní adresa hodnoty na vašem webu |
type | Typ 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 |
takes | U path.add: hodnoty ostatních částí, s nimiž lidé tuto hodnotu hledají |
redirectTo | U 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 deklarovaliexportsarticlesaadopt: true. Odpověď 400 znamená „nepodporováno". Spaths: ["/answers/x"]alimit: 1znovu 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é:typesjako[{ key, name, path }]arulessbrandTerms,synonyms,noise,onTopicaoffTopic.
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.