Dokumentacja
Webhook dostarczania: odbieranie artykułów na własnej stronie
Podpisany webhook, który wysyła artykuły AutoPSEO na Twoją stronę: nagłówki, każde zdarzenie i jego payload, odpowiedzi, które masz zwracać, ponowienia, oczekujące polecenia i IndexNow.
AutoPSEO wysyła każdy artykuł, który publikuje, aktualizuje lub wycofuje, na Twoją stronę jako podpisane żądanie HTTPS POST. Twoja strona zapisuje artykuł i renderuje go we własnych szablonach, więc treść żyje w Twoim systemie i nadal działa, jeśli kiedykolwiek zrezygnujesz z AutoPSEO. Ta strona to dokumentacja dla programisty, który buduje odbiornik: podpisywanie, każde zdarzenie wraz z payloadem, odpowiedzi odczytywane przez AutoPSEO, ponowienia, oczekujące polecenia i IndexNow.
Konfiguracja webhooka
- Otwórz stronę Treści danej usługi i przejdź do sekcji Gdzie publikowane są artykuły.
- Wypełnij pole Adres webhooka na Twojej stronie. Musi to być adres URL
https://z publiczną nazwą hosta: adresy IP ilocalhostsą odrzucane. AutoPSEO nie podąża za przekierowaniami, więc podaj adres docelowy. - Wypełnij pole Adres Twojej strony, na przykład
https://example.com. Ścieżka każdego typu treści jest doklejana pod tym adresem, więc artykuł typu ze ścieżką/answersznajduje się pod adresemhttps://example.com/answers/<slug>. - Wybierz Zapisz i wyślij test. AutoPSEO tworzy sekret podpisu (
whsec_i 48 znaków szesnastkowych) i wysyła ping na Twój adres. Odbiornik, który weryfikuje podpisy, nie przejdzie tego pierwszego pingu, ponieważ nie zna jeszcze sekretu. - Odkryj Sekret podpisu przyciskiem Pokaż lub Kopiuj, zapisz go jako sekret na swojej stronie, a następnie wybierz Wyślij test.
- Sprawdź sekcję Co obsługuje Twoja strona: każde polecenie ma status Połączone, Niepołączone, Zadeklarowane, nie przetestowane lub Nie przetestowane.
Nowe usługi zaczynają od hostowania artykułów przez AutoPSEO pod jego własnym adresem (zobacz Hosting), a hosting i webhook mogą działać równolegle. Gdy Twoja strona już wyświetla artykuły, wybierz Zatrzymaj hosting w AutoPSEO: każdy opublikowany artykuł przechodzi pod adres Twojej strony i jest wysyłany na Twój webhook jako article.published. Ten przycisk wymaga webhooka, który odpowiedział na ostatni test, oraz adresu Twojej strony. Dopóki hosting jest wyłączony, webhooka nie można usunąć.
Nowy sekret natychmiast zastępuje sekret podpisu, a stary od razu przestaje przechodzić weryfikację. Wstaw nowy sekret na swojej stronie zaraz po jego utworzeniu.
AutoPSEO kolejkuje dostawy tylko dla webhooka, którego ostatni ping zakończył się odpowiedzią 2xx. Nieudany ping (przy zapisie, przy Wyślij test lub na początku skanowania strony) oznacza webhook jako błędny, a artykuły opublikowane w czasie błędu nie są dla niego kolejkowane. Dbaj o to, by ping zawsze dostawał odpowiedź, a po usunięciu awarii wybierz Wyślij test.
Nagłówki i podpis
Każde żądanie to POST z ciałem JSON i tymi nagłówkami:
| Nagłówek | Wartość |
|---|---|
Content-Type | application/json |
User-Agent | AutoPSEO-Webhook/1 |
X-AutoPSEO-Event | Nazwa zdarzenia, występuje też w ciele jako event |
X-AutoPSEO-Delivery | Identyfikator dostawy, występuje też w ciele jako id. Taki sam przy każdym ponowieniu jednej dostawy |
X-AutoPSEO-Timestamp | Czas uniksowy w sekundach, w którym podpisano tę próbę |
X-AutoPSEO-Signature | sha256= i szesnastkowy HMAC-SHA256 z <timestamp>.<raw body> |
Aby zweryfikować żądanie:
- Weź nagłówek z sygnaturą czasową, kropkę i surowe ciało żądania dokładnie w takiej postaci, w jakiej dotarło, przed jakimkolwiek parsowaniem JSON.
- Oblicz HMAC-SHA256 z tego ciągu. Kluczem jest cały sekret podpisu w takiej postaci, jak pokazuje go aplikacja, razem z prefiksem
whsec_, jako bajty UTF-8. - Zakoduj wynik szesnastkowo i porównaj go z częścią
X-AutoPSEO-Signaturewystępującą posha256=, w stałym czasie. Odrzuć żądanie, jeśli się różnią.
Każda próba jest podpisywana od nowa z własną sygnaturą czasową, więc możesz odrzucać żądania, których czas znacznie odbiega od Twojego zegara (typowe okno to pięć minut), bez psucia ponowień. Sam HMAC jest zdefiniowany w RFC 2104.
Każde ciało zawiera id, event, occurredAt (ISO 8601, UTC) oraz site: { propertyId, domain }.
Zdarzenia
| Zdarzenie | Rodzaj | Kiedy AutoPSEO je wysyła |
|---|---|---|
ping | Sprawdzenie | Przy zapisie webhooka, przy Wyślij test i na początku każdego skanowania strony |
test | Sprawdzenie | Zaraz po udanym pingu, po jednym na polecenie |
article.published | Dostawa | Artykuł po raz pierwszy trafia na żywo; każdy opublikowany artykuł po zatrzymaniu hostingu |
article.updated | Dostawa | Nowa wersja trafia na żywo (odświeżenie, optymalizacja, wycofanie zmian, ponowna publikacja po wycofaniu, dodanie języków) lub dodano okładkę |
article.unpublished | Dostawa | Artykuł zostaje wycofany lub scalony z inną stroną |
path.add | Dostawa | Zatwierdzono nową wartość typowanego typu treści |
path.remove | Dostawa | Ktoś zażądał wycofania ścieżki danej wartości |
path.check | Wywołanie | Zanim AutoPSEO zaplanuje stronę pod adresem, pod którym Twoja strona nie ma żadnej strony, lub zaproponuje nową wartość |
site.export | Wywołanie | Podczas skanowania strony, w codziennej synchronizacji tematów i przed zmianą opublikowanego artykułu przez AutoPSEO |
articles.adopted | Wywołanie | Po skanowaniu, które zaimportowało artykuły wyeksportowane przez Twoją stronę |
site.handover | Wywołanie | Gdy ktoś przekazuje pisanie typów treści do AutoPSEO lub z powrotem |
Dostawy przechodzą przez skrzynkę nadawczą (outbox): są zapisywane w tej samej partii bazy danych co zmiana, wysyłane od razu, ponawiane i mogą czekać na Twoją stronę. Wywołania to zwykłe żądanie i odpowiedź: AutoPSEO odczytuje odpowiedź na miejscu. Kiedy wysyłane są polecenia ścieżek, wyjaśnia strona Strony typowane i nowe ścieżki.
Payload artykułu
article.published i article.updated zawierają cały artykuł, ze wszystkimi językami:
{
"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
}
}
}
Wpis niemiecki jest tu skrócony; każde tłumaczenie ma taki sam kształt jak copy.
| Pole | Znaczenie |
|---|---|
id | Identyfikator artykułu w AutoPSEO. Nigdy się nie zmienia, więc jest bezpiecznym kluczem |
slug | Ostatnia część adresu. Na stronach typowanych zawiera segmenty połączone znakiem /, na przykład google/software-engineer |
path | Adres względem Twojej strony: ścieżka typu plus slug. Niezmienny po utworzeniu strony |
type | { key, name, path }. key jest stały; path to miejsce, w którym znajdują się artykuły tego typu (puste dla katalogu głównego strony) |
lang, langs | Język podstawowy i wszystkie języki tej wersji |
rev | Numer wersji |
url | Pełny adres znany AutoPSEO: adres Twojej strony po wyłączeniu hostingu albo url, które Twój odbiornik zwrócił ostatnio |
datePublished, dateModified | YYYY-MM-DD. Data publikacji pozostaje stała w kolejnych wersjach |
contentHash | Odcisk wersji |
writer | Kto napisał wersję: platform (AutoPSEO), mcp (Twoje własne AI), user, import (tekst z Twojej strony) lub aggregate (strona z listą) |
copy | Artykuł w języku podstawowym |
translations | Pozostałe języki, kluczowane kodem języka |
category | Kategoria w Twojej witrynie, jeśli Twój format przechowuje kategorie |
topic | { externalId, keyword }, gdy artykuł został napisany do tematu przekazanego przez Twoją stronę, żebyś mógł ten temat zamknąć |
parts | Na stronach typowanych: wartość każdej części, { "company": "google", "role": "software-engineer" } |
kind | "list" na stronie z listą. Jej treść to { title, h1, items: [{ from, text }] }, gdzie from to ścieżka strony, z której pochodzi dany element, a parts wskazuje wartość, którą lista zbiera |
cover | Okładka wykonana przez AutoPSEO dla artykułu. Pobierz ją raz i zachowaj własną kopię; adres nigdy się nie zmienia, ponieważ nazwą pliku jest SHA-256 obrazu |
basedOn | W article.updated: wersja Twojej strony, na której oparto tę wersję (zobacz konflikty poniżej) |
redirectTo | W article.unpublished po scaleniu: dokąd prowadzi teraz stary adres. Odpowiedz na niego przekierowaniem 301 i nie umieszczaj tam nic nowego |
article.unpublished zawiera te same pola article oprócz copy, translations, category i cover.
Zawartość copy
| Pole | Znaczenie |
|---|---|
title, metaDescription, h1 | Tytuł strony, meta opis i nagłówek |
directAnswer | Krótka odpowiedź wyświetlana na początku |
keyTakeaways | Lista ciągów znaków |
sections | [{ heading, bodyHtml }] |
faq | [{ q, a }] |
sources | [{ title, url, lastVerified? }] |
question | Opcjonalne: pytanie szukającego, jego słowami, gdy Twój format o nie prosi |
category | Opcjonalne: jedna z kategorii Twojej strony |
lists | Opcjonalne: listy w postaci zwykłego tekstu, kluczowane nazwą, dla typów treści, które ich wymagają (przykładowe pytania w poradniku rozmowy kwalifikacyjnej) |
W artykułach pisanych przez AutoPSEO bodyHtml używa wyłącznie p, ul, li, strong, em, a i code, a linki zachowują tylko atrybut href. Tekst przejęty z Twojej strony zachowuje Twój HTML. Jeśli Twoje szablony tego wymagają, przepuść go także przez własny sanitizer.
Co odpowiada Twój odbiornik
Każda odpowiedź 2xx kończy dostawę. AutoPSEO odczytuje też opcjonalny obiekt JSON z ciała odpowiedzi; utrzymaj go poniżej 64 KB, ponieważ większa odpowiedź jest liczona jako niepowodzenie.
| Pole | Skutek |
|---|---|
url | Adres https://, pod którym artykuł teraz się znajduje. AutoPSEO zapisuje go jako URL artykułu |
version | Wersja Twojej strony, nieprzezroczysty ciąg do 128 znaków. Wraca jako basedOn przy następnej aktualizacji |
urls | Adresy https://, które ta dostawa zmieniła, dla IndexNow (każdy język lub adresy, których przestałeś udostępniać). [] oznacza, że nic się nie zmieniło |
cover | "wanted", gdy nie masz okładki artykułu, a dostawa jej nie zawierała. AutoPSEO tworzy ją i wysyła w article.updated |
Odpowiedzi błędów oznaczają różne rzeczy:
| Twoja odpowiedź | Jak odczytuje ją AutoPSEO | Co się dzieje |
|---|---|---|
4xx z { "error": "unknown_event" } lub 501 | Nie obsługujesz jeszcze tego polecenia | Dostawa czeka, bez ponowień |
409 z { "error": "conflict" } | Aktualizacja została napisana na podstawie wersji, której Twoja strona już nie ma | Dostawa kończy się niepowodzeniem bez ponowień, a artykuł zostanie ponownie odczytany z Twojej strony przed kolejną zmianą |
4xx inne niż 408 lub 429 z { "error": "<code>", "details": [] } w odpowiedzi na path.add lub path.remove | Odmowa z Twoim uzasadnieniem | Zapisana; zobacz Strony typowane i nowe ścieżki |
| Cokolwiek innego: brak odpowiedzi w ciągu 10 sekund, błąd sieci, przekierowanie, 408, 429, 5xx lub inny 4xx | Chwilowa awaria | Ponawiane z rosnącymi odstępami |
Ponowienia i dostawy zastąpione
Dostawa jest wysyłana od razu po zapisaniu zmiany, a pięciominutowy harmonogram wysyła wszystko, co jest należne, od najstarszych. Po chwilowej awarii AutoPSEO czeka 1 minutę, potem 5 minut, 15 minut, 1 godzinę, 3 godziny, 6 godzin i 12 godzin między próbami. Ósma nieudana próba oznacza dostawę jako nieudaną, a strona Treści pokazuje ją w czerwonej plakietce obok webhooka.
Gdy istnieje nowsza dostawa dla tego samego artykułu, starsza, wciąż oczekująca, zostaje oznaczona jako zastąpiona i nigdy nie jest wysyłana. Każda dostawa zawiera cały artykuł, więc najnowsza wystarcza Twojej stronie. Polecenia ścieżek dotyczące tej samej wartości działają tak samo.
Twój odbiornik może zobaczyć jedną dostawę dwa razy, na przykład gdy zapisał artykuł, ale odpowiedział po 10-sekundowym limicie czasu. Deduplikuj po X-AutoPSEO-Delivery albo spraw, by każdy zapis był idempotentny, zapisując dane pod article.id. article.updated może też dotrzeć do artykułu, który usunąłeś po article.unpublished, ponieważ ponowna publikacja wycofanego artykułu wysyła aktualizację: traktuj aktualizacje jako „utwórz lub zastąp".
Polecenia, których Twoja strona jeszcze nie obsługuje
Strona może wdrażać protokół po jednym poleceniu. AutoPSEO przechowuje stan każdego polecenia i poznaje go na trzy sposoby:
- Testy. Po każdym udanym pingu AutoPSEO wysyła jeden
testna polecenie. Ciało to{ id, event: "test", occurredAt, site, test: { event: "<command>", ... } }, gdzietestzawiera próbkę ukształtowaną jak prawdziwe polecenie i dotyczącą<type path>/autopseo-connection-test, adresu, którego żadna strona nie ma. Sprawdź ją tak, jak sprawdzasz prawdziwe polecenie, niczego nie zmieniaj i odpowiedz 2xx:{ "test": "<command>", "ok": true }dla Połączone,{ "test": "<command>", "ok": false, "error": "unknown_event" }dla Niepołączone. Odpowiedź zok: falsei innym kodem błędu, na przykład przy wycofaniu adresu, którego nie masz, nadal liczy się jako połączone. Próbka znajduje się podtest, nigdy na najwyższym poziomie, więc odbiornik, który nic nie wie o testach, nie znajdzie w niej artykułu. - Prawdziwe dostawy i wywołania. Odpowiedź 2xx oznacza polecenie jako połączone;
unknown_eventoznacza je jako niepołączone. - Odpowiedź na ping. Polecenia wymienione w
autopseo.eventsw Twojej odpowiedzi są wyświetlane jako Zadeklarowane, nie przetestowane, dopóki test lub dostawa nie powie czegoś więcej.
Dostawa dla polecenia, które nie jest połączone, czeka. Strona Treści wyświetla listę oczekujących, na przykład „Twoja strona jeszcze nie wycofuje artykułów", z przyciskiem Zrobione na stronie dla wszystkiego, co wykonałeś ręcznie; AutoPSEO nigdy wtedy tego nie wyśle. Gdy późniejszy test lub dostawa wykaże, że polecenie jest połączone, wszystko, co na nie czekało, zostaje wysłane.
Odpowiedź na ping
Odpowiedz na ping kodem 2xx. Strona, która tylko odbiera artykuły, nie potrzebuje niczego więcej. Aby poinformować AutoPSEO, co jeszcze robi Twoja strona, dołącz obiekt 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 | Znaczenie |
|---|---|
version | 1. Obiekt jest ignorowany bez wersji 1 lub wyższej |
events | Polecenia, które obsługujesz, wyświetlane jako zadeklarowane do czasu przetestowania |
covers | true, gdy Twoje strony pokazują okładkę przy każdym artykule. Autor AutoPSEO tworzy wtedy okładkę podczas pisania, więc pierwsza dostawa ją zawiera, a Twoje własne AI jest proszone o jej dostarczenie. Zobacz Okładki |
indexNow | { key, keyLocation? }: klucz IndexNow, który udostępniasz (od 8 do 128 liter, cyfr i myślników) oraz, gdy plik klucza nie leży pod /<key>.txt, jego lokalizacja https:// |
export | Co przekazuje site.export: articles, topics, profile |
adopt | Akceptujesz articles.adopted, więc pełne skanowanie może przejąć Twoje istniejące artykuły |
handover | Akceptujesz site.handover |
types | Klucze typów treści, które akceptujesz (do 50) |
writer | Kto dziś pisze Twoje nowe artykuły: site lub autopseo |
IndexNow dla adresów, które udostępniasz
Gdy Twoja odpowiedź na ping deklaruje indexNow, AutoPSEO wysyła ping do IndexNow pod adresem api.indexnow.org po dostawach, które akceptujesz, a Twoja strona sama niczego nie pinguje.
- Wysyła
urlswymienione w Twojej odpowiedzi. Bezurlswysyłaurlz Twojej odpowiedzi, a w jego braku URL artykułu.urls: []nie wysyła nic. - Uwzględniane są tylko adresy na Twoim własnym hoście, zgodnie z wymaganiami protokołu IndexNow: host z
keyLocation, a w jego braku host z adresu Twojej strony. - Każde przejście dostaw wysyła jeden ping na stronę, z maksymalnie 10 000 adresów.
- Odpowiedzi 200 i 202 są akceptowane. Kod 429, 5xx lub brak odpowiedzi zachowuje adresy i ponawia próbę po godzinie, podwajając odstęp za każdym razem aż do doby (dłużej, jeśli
Retry-Aftertego żąda), razem z adresami dodanymi przez późniejsze dostawy. Kod 403 (nieprawidłowy klucz) lub 422 (adresy spoza hosta) jest zapisywany, a adresy odrzucane. - Strona Treści pokazuje ostatni ping: kiedy, ile adresów, status HTTP i następną próbę.
- Dopóki AutoPSEO hostuje też artykuły, hostowane strony są pingowane kluczem AutoPSEO, a Twój nie jest używany.
Narzędzie IndexNow w sekcji Indeksowanie to osobne, ręczne narzędzie do zgłaszania; zobacz IndexNow.
Konflikty: oryginałem jest tekst Twojej strony
version z Twojej odpowiedzi (lub version artykułu w Twoim eksporcie) to wersja, w której według AutoPSEO znajduje się Twoja strona. Każde article.updated niesie ją jako basedOn. Jeśli strona zmieniła się od tamtej pory na Twojej witrynie, na przykład po poprawce redaktora, odpowiedz kodem 409 z { "error": "conflict" } i niczego nie zapisuj. Dostawa kończy się niepowodzeniem bez ponowień.
Zanim zakolejkowana zmiana opublikowanego artykułu zostanie zapisana, AutoPSEO ponownie odczytuje ten artykuł z Twojej strony przez site.export z paths, jeśli Twoja strona obsługuje site.export. Jeśli Twoja wersja poszła naprzód, Twój tekst staje się następną wersją artykułu (writer import), a kolejna aktualizacja niesie Twoją bieżącą wersję jako basedOn.
path.add i path.remove
Te dwie dostawy dotyczą jednej wartości typowanego typu treści, na przykład firmy w /interviews/{company}/{role}. Zamiast article niosą obiekt 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 | Znaczenie |
|---|---|
path | Własny adres wartości na Twojej stronie |
type | Typ treści jako { key, name, path } |
part | Część adresu, którą wypełnia wartość |
value | { value, name }, a w path.add dodatkowo aliases, url, sameAs, description i facts zebrane przez autora AutoPSEO. Każdy fakt ma text, source, lastVerified oraz tier o wartości official lub reported |
takes | W path.add: wartości pozostałych części, z którymi ludzie szukają tej wartości |
redirectTo | W path.remove: dokąd od teraz mają prowadzić adresy tej wartości, przekierowaniem 301 |
Odpowiedz kodem 2xx, gdy przyjmiesz polecenie; w path.add możesz dodać url, pod którym wartość teraz się znajduje, a w path.remove urls, których przestałeś udostępniać i które trafiają do IndexNow. Aby odmówić, odpowiedz kodem 4xx z { "error": "<code>", "details": ["<why>"] }. W path.add kod invalid_value odsyła wartość do autora z Twoim uzasadnieniem (trzecia odmowa jest ostateczna), a każdy inny kod odrzuca ją na stałe. W path.remove kod not_found liczy się jako wykonane, a każdy inny kod, na przykład 409 in_use, pozostawia wszystko bez zmian. Oznaczenie oczekującego polecenia ścieżki jako Zrobione na stronie liczy się jako zgoda Twojej strony.
Wywołania: żądania, na których odpowiedź czeka AutoPSEO
Wywołania są podpisywane tak samo jak dostawy. Wywołanie czeka domyślnie do 60 sekund, a przy ponownych odczytach i sprawdzaniu ścieżek 30 sekund, i odczytuje odpowiedzi do 24 MB. Każda odpowiedź inna niż 2xx oznacza niepowodzenie wywołania.
site.export
Żądanie zawiera export: { what, cursor, limit } oraz since (czas ISO) dla tematów lub paths dla ponownego odczytu. Odpowiedz { "items": [], "next": "<cursor>" }; AutoPSEO pyta ponownie z cursor ustawionym na Twoje next, aż będzie puste lub null.
what: "articles"działa w pełnych skanowaniach strony, po 10 na stronę, gdy zadeklarowałeśexportzarticlesorazadopt: true. Odpowiedź 400 oznacza „nieobsługiwane". Zpaths: ["/answers/x"]ilimit: 1ponownie odczytuje jeden artykuł; odpowiedz wyłącznie tym artykułem.what: "topics"działa w każdym skanowaniu i w codziennej synchronizacji, po 50 na stronę; codzienna synchronizacja wysyłasince, czyli czas ostatniego pobrania.what: "profile"działa w każdym skanowaniu. Odpowiedz{ "profile": { types, languages, rules, categories, format } }, każde pole opcjonalne:typesjako[{ key, name, path }]orazruleszbrandTerms,synonyms,noise,onTopicioffTopic.
Wyeksportowany artykuł wymaga externalId, slug (litery i cyfry dowolnego pisma oraz łączniki, najwyżej 200 znaków, segmenty połączone znakiem /), path kończącego się tym slugiem oraz copy z co najmniej title, h1 i jedną sekcją. Pola opcjonalne: lang (domyślnie en), type (klucz typu treści), url, keyword, category, datePublished, dateModified, version, translations, owner (identyfikator artykułu w AutoPSEO, po przejęciu) oraz maintenance: { kind: "refresh" or "optimize", reason }, które kolejkuje takie zadanie. Każdy wyeksportowany artykuł staje się artykułem zarządzanym, z Twoim tekstem jako pierwszą wersją, i nic nie jest odsyłane. Ponowny eksport jest bezpieczny, a artykuł wycofany przez AutoPSEO nigdy nie wraca przez eksport.
Element tematu to { keyword, status?, volume?, source?, externalId?, type?, reason? }, gdzie status to queued, planned lub rejected.
articles.adopted
Po imporcie AutoPSEO prosi Cię o zgodę na zarządzanie nowymi artykułami: { "articles": [{ "externalId", "articleId", "slug", "path" }] }. Odpowiedz { "adopted": ["<externalId>"], "refused": [{ "externalId": "<id>", "reason": "<why>" }] }. Artykuł, którego nie potwierdzisz, jest ponownie usuwany z AutoPSEO, chyba że AutoPSEO napisało już jego wersję.
site.handover
{ "handover": { "types": ["answers"], "writer": "autopseo" } }, wysyłane, gdy ktoś wybierze Niech AutoPSEO je pisze lub Oddaj pisanie stronie na stronie Twoja strona danej usługi. Odpowiedz { "ok": true, "writer": "autopseo" }. Po przekazaniu pisania do AutoPSEO Twoja strona przekazuje do AutoPSEO tematy, które zatwierdza, zamiast je pisać.
path.check
{ "paths": [{ "path": "/interviews/google/data-engineer" }, { "path": "/interviews/jane-street", "new": { "part": "company", "value": "jane-street", "name": "Jane Street" } }] }. Odpowiedz jednym wynikiem na ścieżkę: { "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."] }] }. Odmowa jest zachowywana z Twoim uzasadnieniem i nie jest pytana ponownie; pominięta ścieżka zostanie zapytana ponownie później.
Minimalny odbiornik
Cloudflare Worker, który weryfikuje podpis za pomocą Web Crypto API, obsługuje trzy polecenia artykułów, a resztę odrzuca jako unknown_event. Przechowuje artykuły w przestrzeni nazw KV powiązanej jako ARTICLES, a sekret podpisu w sekrecie Workera o nazwie 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);
},
};
W Node.js weryfikuj w ten sam sposób za pomocą createHmac i timingSafeEqual, na surowym ciele otrzymanym przez Twój 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));
}
Dalej: to, co trafia na webhook, jest określane przez pisanie i recenzję oraz utrzymanie; cały przepływ opisuje przegląd silnika treści.