Przejdź do treści
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.

Usługa → Treści → Gdzie publikowane są artykuły18 min czytania

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

  1. Otwórz stronę Treści danej usługi i przejdź do sekcji Gdzie publikowane są artykuły.
  2. Wypełnij pole Adres webhooka na Twojej stronie. Musi to być adres URL https:// z publiczną nazwą hosta: adresy IP i localhost są odrzucane. AutoPSEO nie podąża za przekierowaniami, więc podaj adres docelowy.
  3. 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ą /answers znajduje się pod adresem https://example.com/answers/<slug>.
  4. 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.
  5. Odkryj Sekret podpisu przyciskiem Pokaż lub Kopiuj, zapisz go jako sekret na swojej stronie, a następnie wybierz Wyślij test.
  6. 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łówekWartość
Content-Typeapplication/json
User-AgentAutoPSEO-Webhook/1
X-AutoPSEO-EventNazwa zdarzenia, występuje też w ciele jako event
X-AutoPSEO-DeliveryIdentyfikator dostawy, występuje też w ciele jako id. Taki sam przy każdym ponowieniu jednej dostawy
X-AutoPSEO-TimestampCzas uniksowy w sekundach, w którym podpisano tę próbę
X-AutoPSEO-Signaturesha256= i szesnastkowy HMAC-SHA256 z <timestamp>.<raw body>

Aby zweryfikować żądanie:

  1. 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.
  2. 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.
  3. Zakoduj wynik szesnastkowo i porównaj go z częścią X-AutoPSEO-Signature występującą po sha256=, 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

ZdarzenieRodzajKiedy AutoPSEO je wysyła
pingSprawdzeniePrzy zapisie webhooka, przy Wyślij test i na początku każdego skanowania strony
testSprawdzenieZaraz po udanym pingu, po jednym na polecenie
article.publishedDostawaArtykuł po raz pierwszy trafia na żywo; każdy opublikowany artykuł po zatrzymaniu hostingu
article.updatedDostawaNowa wersja trafia na żywo (odświeżenie, optymalizacja, wycofanie zmian, ponowna publikacja po wycofaniu, dodanie języków) lub dodano okładkę
article.unpublishedDostawaArtykuł zostaje wycofany lub scalony z inną stroną
path.addDostawaZatwierdzono nową wartość typowanego typu treści
path.removeDostawaKtoś zażądał wycofania ścieżki danej wartości
path.checkWywołanieZanim AutoPSEO zaplanuje stronę pod adresem, pod którym Twoja strona nie ma żadnej strony, lub zaproponuje nową wartość
site.exportWywołaniePodczas skanowania strony, w codziennej synchronizacji tematów i przed zmianą opublikowanego artykułu przez AutoPSEO
articles.adoptedWywołaniePo skanowaniu, które zaimportowało artykuły wyeksportowane przez Twoją stronę
site.handoverWywołanieGdy 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.

PoleZnaczenie
idIdentyfikator artykułu w AutoPSEO. Nigdy się nie zmienia, więc jest bezpiecznym kluczem
slugOstatnia część adresu. Na stronach typowanych zawiera segmenty połączone znakiem /, na przykład google/software-engineer
pathAdres 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, langsJęzyk podstawowy i wszystkie języki tej wersji
revNumer wersji
urlPełny adres znany AutoPSEO: adres Twojej strony po wyłączeniu hostingu albo url, które Twój odbiornik zwrócił ostatnio
datePublished, dateModifiedYYYY-MM-DD. Data publikacji pozostaje stała w kolejnych wersjach
contentHashOdcisk wersji
writerKto napisał wersję: platform (AutoPSEO), mcp (Twoje własne AI), user, import (tekst z Twojej strony) lub aggregate (strona z listą)
copyArtykuł w języku podstawowym
translationsPozostałe języki, kluczowane kodem języka
categoryKategoria 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ąć
partsNa 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
coverOkł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
basedOnW article.updated: wersja Twojej strony, na której oparto tę wersję (zobacz konflikty poniżej)
redirectToW 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

PoleZnaczenie
title, metaDescription, h1Tytuł strony, meta opis i nagłówek
directAnswerKrótka odpowiedź wyświetlana na początku
keyTakeawaysLista ciągów znaków
sections[{ heading, bodyHtml }]
faq[{ q, a }]
sources[{ title, url, lastVerified? }]
questionOpcjonalne: pytanie szukającego, jego słowami, gdy Twój format o nie prosi
categoryOpcjonalne: jedna z kategorii Twojej strony
listsOpcjonalne: 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.

PoleSkutek
urlAdres https://, pod którym artykuł teraz się znajduje. AutoPSEO zapisuje go jako URL artykułu
versionWersja Twojej strony, nieprzezroczysty ciąg do 128 znaków. Wraca jako basedOn przy następnej aktualizacji
urlsAdresy 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ą AutoPSEOCo się dzieje
4xx z { "error": "unknown_event" } lub 501Nie obsługujesz jeszcze tego poleceniaDostawa czeka, bez ponowień
409 z { "error": "conflict" }Aktualizacja została napisana na podstawie wersji, której Twoja strona już nie maDostawa 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.removeOdmowa z Twoim uzasadnieniemZapisana; 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 4xxChwilowa awariaPonawiane 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 test na polecenie. Ciało to { id, event: "test", occurredAt, site, test: { event: "<command>", ... } }, gdzie test zawiera 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ź z ok: false i innym kodem błędu, na przykład przy wycofaniu adresu, którego nie masz, nadal liczy się jako połączone. Próbka znajduje się pod test, 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_event oznacza je jako niepołączone.
  • Odpowiedź na ping. Polecenia wymienione w autopseo.events w 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"
  }
}
PoleZnaczenie
version1. Obiekt jest ignorowany bez wersji 1 lub wyższej
eventsPolecenia, które obsługujesz, wyświetlane jako zadeklarowane do czasu przetestowania
coverstrue, 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://
exportCo przekazuje site.export: articles, topics, profile
adoptAkceptujesz articles.adopted, więc pełne skanowanie może przejąć Twoje istniejące artykuły
handoverAkceptujesz site.handover
typesKlucze typów treści, które akceptujesz (do 50)
writerKto 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 urls wymienione w Twojej odpowiedzi. Bez urls wysyła url z 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-After tego żą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"] }
  }
}
PoleZnaczenie
pathWłasny adres wartości na Twojej stronie
typeTyp treści jako { key, name, path }
partCzęść 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
takesW path.add: wartości pozostałych części, z którymi ludzie szukają tej wartości
redirectToW 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ś export z articles oraz adopt: true. Odpowiedź 400 oznacza „nieobsługiwane". Z paths: ["/answers/x"] i limit: 1 ponownie 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ła since, czyli czas ostatniego pobrania.
  • what: "profile" działa w każdym skanowaniu. Odpowiedz { "profile": { types, languages, rules, categories, format } }, każde pole opcjonalne: types jako [{ key, name, path }] oraz rules z brandTerms, synonyms, noise, onTopic i offTopic.

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.

Wrzuć treści SEO na autopilota

Połącz Search Console i swoją stronę. AutoPSEO przeskanuje ją i od razu zaplanuje pierwsze tematy. Karta kredytowa nie jest wymagana.