Chuyển đến nội dung
Tài liệu

Webhook phân phối: nhận bài viết trên website của bạn

Webhook có chữ ký gửi bài viết AutoPSEO tới website của bạn: header, từng sự kiện và payload, các phản hồi cần trả về, cơ chế thử lại, lệnh đang chờ và IndexNow.

Thuộc tính → Nội dung → Nơi bài viết được xuất bản24 phút đọc

AutoPSEO gửi mọi bài viết mà nó xuất bản, cập nhật hoặc gỡ xuống tới website của bạn dưới dạng một yêu cầu HTTPS POST có chữ ký. Website của bạn lưu bài viết và hiển thị bằng template riêng, nên nội dung nằm trong hệ thống của bạn và vẫn hoạt động nếu sau này bạn ngừng dùng AutoPSEO. Trang này là tài liệu tham chiếu dành cho lập trình viên xây dựng bên nhận: chữ ký, từng sự kiện cùng payload, các phản hồi mà AutoPSEO đọc, cơ chế thử lại, lệnh đang chờ và IndexNow.

Thiết lập webhook

  1. Mở trang Content của property và đi tới mục Where articles are published.
  2. Điền Webhook address on your site. Đó phải là một URL https:// trên tên miền công khai: địa chỉ IP và localhost đều bị từ chối. AutoPSEO không theo chuyển hướng, vì vậy hãy nhập địa chỉ cuối cùng.
  3. Điền Your site's address, ví dụ https://example.com. Đường dẫn của từng loại nội dung nằm dưới địa chỉ này, nên một bài viết thuộc loại có đường dẫn /answers sẽ nằm tại https://example.com/answers/<slug>.
  4. Chọn Save and send a test. AutoPSEO tạo một khóa ký (whsec_ theo sau là 48 ký tự hex) và ping địa chỉ của bạn. Bên nhận có kiểm tra chữ ký sẽ không vượt qua lần ping đầu này, vì nó chưa có khóa.
  5. Hiện Signing secret bằng Show hoặc Copy, lưu nó thành secret trên website của bạn, rồi chọn Send a test.
  6. Đọc mục What your site handles: mỗi lệnh hiển thị một trong các trạng thái Connected, Not connected, Declared, not tested hoặc Not tested.

Các property mới bắt đầu với việc AutoPSEO lưu trữ bài viết tại địa chỉ của chính nó (xem Hosting), và việc lưu trữ cùng webhook có thể chạy song song. Khi website của bạn đã hiển thị các bài viết, hãy chọn Stop hosting on AutoPSEO: mọi bài viết đang hoạt động sẽ chuyển sang địa chỉ website của bạn và được gửi tới webhook dưới dạng article.published. Nút đó yêu cầu webhook đã phản hồi ở lần kiểm tra gần nhất và phải có địa chỉ website của bạn. Khi việc lưu trữ đang tắt, webhook không thể bị gỡ.

New secret thay khóa ký ngay lập tức và khóa cũ ngừng xác minh được ngay. Hãy đặt khóa mới lên website của bạn ngay sau khi tạo.

AutoPSEO chỉ xếp hàng gửi cho webhook có lần ping gần nhất được phản hồi bằng mã 2xx. Một lần ping thất bại (khi lưu, khi Send a test, hoặc lúc bắt đầu quét site) sẽ đánh dấu webhook là đang lỗi, và các bài viết xuất bản trong lúc webhook đang lỗi sẽ không được xếp hàng gửi cho nó. Hãy giữ cho ping luôn phản hồi, và chọn Send a test sau khi bạn khắc phục sự cố.

Header và chữ ký

Mọi yêu cầu đều là POST với nội dung JSON và các header sau:

HeaderGiá trị
Content-Typeapplication/json
User-AgentAutoPSEO-Webhook/1
X-AutoPSEO-EventTên sự kiện, cũng có trong nội dung dưới dạng event
X-AutoPSEO-DeliveryID của lần phân phối, cũng có trong nội dung dưới dạng id. Giống nhau ở mọi lần thử lại của cùng một lần phân phối
X-AutoPSEO-TimestampThời gian Unix tính bằng giây tại lúc lần thử này được ký
X-AutoPSEO-Signaturesha256= theo sau là HMAC-SHA256 dạng hex của <timestamp>.<raw body>

Để xác minh một yêu cầu:

  1. Lấy header timestamp, một dấu chấm, rồi đến nội dung thô của yêu cầu đúng như khi nhận được, trước khi phân tích JSON.
  2. Tính HMAC-SHA256 trên chuỗi đó. Khóa là toàn bộ khóa ký như ứng dụng hiển thị, gồm cả tiền tố whsec_, dưới dạng byte UTF-8.
  3. Mã hóa kết quả sang hex và so sánh với phần của X-AutoPSEO-Signature đứng sau sha256=, theo cách so sánh thời gian không đổi. Từ chối yêu cầu nếu hai giá trị khác nhau.

Mỗi lần thử được ký lại với timestamp riêng, nên bạn có thể từ chối các yêu cầu có timestamp lệch nhiều so với đồng hồ của mình (năm phút là khoảng thông dụng) mà không làm hỏng việc thử lại. Bản thân HMAC được định nghĩa trong RFC 2104.

Mọi nội dung đều có id, event, occurredAt (ISO 8601, UTC) và site: { propertyId, domain }.

Sự kiện

Sự kiệnLoạiKhi nào AutoPSEO gửi
pingKiểm traKhi bạn lưu webhook, khi Send a test, và lúc bắt đầu mỗi lần quét site
testKiểm traNgay sau một lần ping thành công, mỗi lệnh một lần
article.publishedPhân phốiMột bài viết lên sóng lần đầu; mọi bài viết đang hoạt động khi bạn ngừng lưu trữ
article.updatedPhân phốiMột bản sửa đổi mới lên sóng (làm mới, tối ưu, khôi phục, xuất bản lại sau khi gỡ, thêm ngôn ngữ) hoặc ảnh bìa vừa được thêm
article.unpublishedPhân phốiMột bài viết bị gỡ xuống, hoặc được gộp vào trang khác
path.addPhân phốiMột giá trị mới của loại nội dung có kiểu đã được duyệt
path.removePhân phốiCó người yêu cầu gỡ đường dẫn của một giá trị
path.checkLệnh gọiTrước khi AutoPSEO lên kế hoạch cho một trang tại địa chỉ mà website của bạn chưa có trang, hoặc đề xuất một giá trị mới
site.exportLệnh gọiTrong lúc quét site, trong lần đồng bộ chủ đề hằng ngày, và trước khi AutoPSEO thay đổi một bài viết đang hoạt động
articles.adoptedLệnh gọiSau khi một lần quét nhập các bài viết mà website của bạn đã xuất
site.handoverLệnh gọiKhi có người giao việc viết các loại nội dung cho AutoPSEO, hoặc giao lại

Các lần phân phối đi qua một outbox: chúng được ghi trong cùng một batch cơ sở dữ liệu với thay đổi, gửi ngay lập tức, được thử lại, và có thể chờ website của bạn. Lệnh gọi là yêu cầu và phản hồi thông thường: AutoPSEO đọc phản hồi ngay tại chỗ. Thời điểm các lệnh path được gửi được giải thích trong Typed pages and new paths.

Payload của bài viết

article.published và article.updated mang toàn bộ bài viết, gồm mọi ngôn ngữ:

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

Bản tiếng Đức ở đây được rút gọn; mỗi bản dịch có cùng cấu trúc với copy.

TrườngÝ nghĩa
idID của bài viết trong AutoPSEO. Không bao giờ thay đổi, nên có thể dùng làm khóa an toàn
slugPhần cuối của địa chỉ. Với trang có kiểu, nó chứa các đoạn nối bằng /, chẳng hạn google/software-engineer
pathĐịa chỉ tương đối so với website của bạn: đường dẫn của loại nội dung cộng với slug. Cố định khi trang đã tồn tại
type{ key, name, path }. key ổn định; path là nơi các bài viết của loại đó nằm (để trống nếu ở gốc website)
lang, langsNgôn ngữ chính và mọi ngôn ngữ của bản sửa đổi này
revSố thứ tự bản sửa đổi
urlĐịa chỉ đầy đủ như AutoPSEO biết: địa chỉ trên website của bạn khi việc lưu trữ đã tắt, hoặc url mà bên nhận của bạn trả về lần gần nhất
datePublished, dateModifiedYYYY-MM-DD. Ngày xuất bản được giữ nguyên qua các bản sửa đổi sau
contentHashDấu vân tay của bản sửa đổi
writerAi viết bản sửa đổi: platform (AutoPSEO), mcp (AI của riêng bạn), user, import (văn bản có sẵn trên website của bạn) hoặc aggregate (một trang danh sách)
copyBài viết bằng ngôn ngữ chính
translationsCác ngôn ngữ khác, theo khóa là mã ngôn ngữ
categoryDanh mục của website bạn cho bài viết, khi định dạng của bạn có dùng danh mục
topic{ externalId, keyword } khi bài viết được viết cho một chủ đề mà website của bạn đã giao, để bạn có thể đóng chủ đề đó
partsVới trang có kiểu, giá trị của từng phần: { "company": "google", "role": "software-engineer" }
kind"list" trên trang danh sách. Nội dung của nó là { title, h1, items: [{ from, text }] }, trong đó from là đường dẫn của trang mà mỗi mục lấy từ đó, và parts nêu tên giá trị mà trang gom lại
coverẢnh bìa AutoPSEO tạo cho bài viết. Hãy tải về một lần và giữ bản sao của riêng bạn; địa chỉ không bao giờ đổi vì tên tệp chính là SHA-256 của ảnh
basedOnTrên article.updated: phiên bản trang của bạn mà bản sửa đổi này được viết dựa trên (xem phần xung đột bên dưới)
redirectToTrên article.unpublished sau khi gộp: nơi địa chỉ cũ giờ dẫn tới. Hãy trả lời bằng 301 và đừng đưa gì mới vào đó

article.unpublished mang các trường article giống như trên, trừ copy, translations, category và cover.

Bên trong copy

TrườngÝ nghĩa
title, metaDescription, h1Tiêu đề trang, meta description và tiêu đề chính
directAnswerCâu trả lời ngắn hiển thị đầu tiên
keyTakeawaysDanh sách các chuỗi
sections[{ heading, bodyHtml }]
faq[{ q, a }]
sources[{ title, url, lastVerified? }]
questionTùy chọn: câu hỏi của người tìm kiếm theo lời của họ, khi định dạng của bạn yêu cầu
categoryTùy chọn: một trong các danh mục của website bạn
listsTùy chọn: các danh sách văn bản thuần theo khóa, dành cho những loại nội dung yêu cầu (câu hỏi mẫu của một bài hướng dẫn phỏng vấn)

Trong các bài viết do AutoPSEO viết, bodyHtml chỉ dùng p, ul, li, strong, em, a và code, và các liên kết chỉ giữ lại href. Văn bản lấy từ website của bạn giữ nguyên HTML của bạn. Nếu template của bạn đòi hỏi, hãy cho nó đi qua trình làm sạch HTML của riêng bạn nữa.

Bên nhận trả về gì

Bất kỳ mã 2xx nào cũng hoàn tất một lần phân phối. AutoPSEO cũng đọc một đối tượng JSON tùy chọn từ nội dung phản hồi; hãy giữ dưới 64 KB, vì phản hồi lớn hơn bị tính là thất bại.

TrườngTác dụng
urlĐịa chỉ https:// nơi bài viết hiện nằm. AutoPSEO ghi nhận đó là URL của bài viết
versionPhiên bản trang của bạn, một chuỗi không có cấu trúc dài tối đa 128 ký tự. Nó quay lại dưới dạng basedOn ở lần cập nhật tiếp theo
urlsCác địa chỉ https:// mà lần phân phối này đã thay đổi, dùng cho IndexNow (mọi ngôn ngữ, hoặc các địa chỉ bạn đã ngừng phục vụ). [] nghĩa là không có gì thay đổi
cover"wanted" khi bạn chưa có ảnh bìa cho bài viết và lần phân phối không kèm ảnh nào. AutoPSEO tạo một ảnh và gửi trong một article.updated

Các phản hồi lỗi mang những ý nghĩa khác nhau:

Phản hồi của bạnAutoPSEO hiểu làĐiều xảy ra
Mã 4xx kèm { "error": "unknown_event" }, hoặc mã 501Bạn chưa xử lý lệnh nàyLần phân phối chờ, không thử lại
Mã 409 kèm { "error": "conflict" }Bản cập nhật được viết từ một phiên bản mà trang của bạn không cònLần phân phối thất bại và không thử lại, và bài viết được đọc lại từ website của bạn trước thay đổi tiếp theo
Mã 4xx khác 408 hoặc 429 kèm { "error": "<code>", "details": [] } cho path.add hoặc path.removeMột lời từ chối kèm lý do của bạnĐược ghi nhận; xem Typed pages and new paths
Mọi trường hợp khác: không phản hồi trong 10 giây, lỗi mạng, chuyển hướng, 408, 429, 5xx hoặc một mã 4xx khácLỗi tạm thờiThử lại với backoff

Thử lại và các lần phân phối bị thay thế

Một lần phân phối được gửi ngay khi thay đổi được lưu, và một lịch năm phút một lần sẽ gửi những gì đến hạn, cũ nhất trước. Sau một lỗi tạm thời, AutoPSEO chờ 1 phút, rồi 5 phút, 15 phút, 1 giờ, 3 giờ, 6 giờ và 12 giờ giữa các lần thử. Lần thử thất bại thứ tám đánh dấu lần phân phối là thất bại, và trang Content đếm nó trong một huy hiệu đỏ cạnh webhook.

Khi đã có một lần phân phối mới hơn cho cùng bài viết, lần phân phối cũ hơn còn đang chờ sẽ bị đánh dấu là bị thay thế và không bao giờ được gửi. Mỗi lần phân phối mang toàn bộ bài viết, nên lần mới nhất là tất cả những gì website của bạn cần. Các lệnh path về cùng một giá trị cũng hoạt động theo cách này.

Bên nhận của bạn có thể thấy một lần phân phối hai lần, chẳng hạn khi nó đã lưu bài viết nhưng phản hồi sau khi hết thời gian chờ 10 giây. Hãy loại trùng theo X-AutoPSEO-Delivery, hoặc làm cho mọi thao tác ghi có tính idempotent bằng cách lưu theo article.id. article.updated cũng có thể đến với một bài viết mà bạn đã xóa sau article.unpublished, vì việc xuất bản lại một bài đã gỡ sẽ gửi một bản cập nhật: hãy coi các bản cập nhật là "tạo mới hoặc thay thế".

Các lệnh mà website của bạn chưa xử lý

Một website có thể triển khai giao thức từng lệnh một. AutoPSEO giữ trạng thái của từng lệnh và biết được qua ba cách:

  • Kiểm thử. Sau mỗi lần ping thành công, AutoPSEO gửi một test cho mỗi lệnh. Nội dung là { id, event: "test", occurredAt, site, test: { event: "<command>", ... } }, trong đó test chứa một mẫu có cấu trúc giống lệnh thật và nói về <type path>/autopseo-connection-test, một địa chỉ không website nào có. Hãy kiểm tra nó như với lệnh thật, đừng thay đổi gì, và trả lời bằng mã 2xx: { "test": "<command>", "ok": true } cho Connected, { "test": "<command>", "ok": false, "error": "unknown_event" } cho Not connected. Một phản hồi có ok: false cùng một mã lỗi khác, chẳng hạn việc gỡ một địa chỉ mà bạn không có, vẫn được tính là đã kết nối. Mẫu nằm dưới test, không bao giờ ở cấp cao nhất, nên bên nhận không biết gì về kiểm thử sẽ không thấy bài viết nào trong đó.
  • Lần phân phối và lệnh gọi thật. Mã 2xx đánh dấu lệnh là đã kết nối; unknown_event đánh dấu là chưa kết nối.
  • Phản hồi ping. Các lệnh mà phản hồi của bạn liệt kê trong autopseo.events hiển thị là Declared, not tested cho đến khi một lần kiểm thử hoặc phân phối cho biết thêm.

Một lần phân phối cho lệnh chưa được kết nối sẽ chờ. Trang Content liệt kê những gì đang chờ, ví dụ "Your site does not take articles down yet", kèm nút Done on the site cho bất cứ việc gì bạn đã làm thủ công; khi đó AutoPSEO sẽ không bao giờ gửi mục ấy nữa. Khi một lần kiểm thử hoặc phân phối sau đó thấy lệnh đã kết nối, mọi thứ đang chờ lệnh đó sẽ được gửi đi.

Phản hồi ping

Hãy trả lời ping bằng mã 2xx. Website chỉ nhận bài viết thì không cần gì thêm. Để cho AutoPSEO biết website của bạn còn làm gì khác, hãy thêm một đối tượng 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"
  }
}
TrườngÝ nghĩa
version1. Đối tượng bị bỏ qua nếu không có version từ 1 trở lên
eventsCác lệnh bạn xử lý, hiển thị là đã khai báo cho đến khi được kiểm thử
coverstrue khi các trang của bạn hiển thị ảnh bìa cùng mỗi bài viết. Khi đó trình viết của AutoPSEO tạo ảnh bìa trong lúc viết, nên lần phân phối đầu tiên đã kèm theo, và AI của riêng bạn được yêu cầu cung cấp một ảnh. Xem Covers
indexNow{ key, keyLocation? }: khóa IndexNow mà bạn phục vụ (8 đến 128 chữ cái, chữ số và dấu gạch ngang) và, khi tệp khóa không nằm ở /<key>.txt, vị trí https:// của nó
exportNhững gì site.export bàn giao: articles, topics, profile
adoptBạn chấp nhận articles.adopted, để một lần quét đầy đủ có thể tiếp nhận các bài viết hiện có của bạn
handoverBạn chấp nhận site.handover
typesCác khóa loại nội dung bạn chấp nhận (tối đa 50)
writerAi hiện đang viết các bài mới của bạn: site hoặc autopseo

IndexNow cho các địa chỉ bạn phục vụ

Khi phản hồi ping của bạn khai báo indexNow, AutoPSEO ping IndexNow tại api.indexnow.org sau các lần phân phối mà bạn chấp nhận, và website của bạn không cần tự ping gì.

  • Nó gửi các urls mà phản hồi của bạn liệt kê. Nếu không có urls, nó gửi url trong phản hồi của bạn, nếu không thì dùng URL của bài viết. urls: [] không gửi gì.
  • Chỉ các địa chỉ thuộc host của bạn mới được đưa vào, như giao thức IndexNow yêu cầu: host của keyLocation, nếu không thì host trong địa chỉ website của bạn.
  • Mỗi lượt chạy phân phối gửi một ping cho mỗi website, tối đa 10.000 địa chỉ.
  • 200 và 202 được chấp nhận. Mã 429, 5xx hoặc không có phản hồi sẽ giữ lại các địa chỉ và thử lại sau một giờ, tăng gấp đôi mỗi lần tới tối đa một ngày (lâu hơn nếu Retry-After yêu cầu), kèm những gì các lần phân phối sau thêm vào. Mã 403 (khóa không hợp lệ) hoặc 422 (địa chỉ không thuộc host) được ghi nhận và bỏ đi.
  • Trang Content hiển thị lần ping gần nhất: thời điểm, số địa chỉ, trạng thái HTTP và lần thử tiếp theo.
  • Khi AutoPSEO cũng đang lưu trữ các bài viết, các trang được lưu trữ sẽ được ping bằng khóa riêng của AutoPSEO, và khóa của bạn không được dùng.

Công cụ IndexNow trong mục Indexing là một công cụ gửi thủ công riêng biệt; xem IndexNow.

Xung đột: văn bản của website bạn là bản gốc

version trong phản hồi của bạn (hoặc version của một bài viết trong bản xuất của bạn) là phiên bản mà AutoPSEO tin rằng trang của bạn đang ở. Mọi article.updated đều mang nó dưới dạng basedOn. Nếu trang đã thay đổi trên website của bạn kể từ đó, chẳng hạn sau một chỉnh sửa của biên tập viên, hãy trả lời 409 với { "error": "conflict" } và đừng lưu gì. Lần phân phối thất bại và không thử lại.

Trước khi một thay đổi đang xếp hàng cho bài viết đang hoạt động được ghi, AutoPSEO đọc lại bài viết đó từ website của bạn bằng site.export và paths, khi website của bạn xử lý site.export. Nếu phiên bản của bạn đã đi tiếp, văn bản của bạn trở thành bản sửa đổi tiếp theo của bài viết (writer import), và bản cập nhật theo sau mang phiên bản hiện tại của bạn dưới dạng basedOn.

path.add và path.remove

Hai lần phân phối này liên quan đến một giá trị của một loại nội dung có kiểu, ví dụ một công ty trong /interviews/{company}/{role}. Chúng mang một đối tượng path thay vì article:

{
  "id": "7c1e9d2a-3b4f-4e8a-9c0d-1e2f3a4b5c6d",
  "event": "path.add",
  "occurredAt": "2026-10-12T08:30:00.000Z",
  "site": { "propertyId": "prop_123", "domain": "example.com" },
  "path": {
    "path": "/interviews/jane-street",
    "type": { "key": "interviews", "name": "Interviews", "path": "/interviews" },
    "part": "company",
    "value": {
      "value": "jane-street",
      "name": "Jane Street",
      "aliases": [],
      "url": "https://<its own site>/",
      "sameAs": ["https://<its careers page>/"],
      "description": "<what it is, in a few words>",
      "facts": [
        { "text": "<one sentence, as the source states it>", "source": "https://<the page that says it>", "lastVerified": "2026-10-01", "tier": "official" }
      ]
    },
    "takes": { "role": ["software-engineer"] }
  }
}
TrườngÝ nghĩa
pathĐịa chỉ riêng của giá trị trên website của bạn
typeLoại nội dung, dưới dạng { key, name, path }
partPhần của địa chỉ mà giá trị điền vào
value{ value, name }, và với path.add còn có aliases, url, sameAs, description và facts mà trình viết của AutoPSEO đã thu thập. Mỗi fact có text, source, lastVerified và một tier là official hoặc reported
takesTrên path.add: các giá trị của những phần khác mà người ta tìm kiếm cùng với nó
redirectToTrên path.remove: nơi các địa chỉ của giá trị từ nay phải dẫn tới, bằng 301

Hãy trả lời mã 2xx khi bạn đã tiếp nhận lệnh; với path.add bạn có thể thêm url nơi giá trị hiện nằm, còn với path.remove là các urls bạn đã ngừng phục vụ, các địa chỉ này sẽ được gửi tới IndexNow. Để từ chối, hãy trả lời mã 4xx kèm { "error": "<code>", "details": ["<why>"] }. Với path.add, invalid_value gửi giá trị trở lại cho trình viết cùng lý do của bạn (lần từ chối thứ ba là cuối cùng) và mọi mã khác từ chối nó vĩnh viễn. Với path.remove, not_found được tính là đã xong, và mọi mã khác, chẳng hạn 409 in_use, để mọi thứ nguyên như cũ. Đánh dấu một lệnh path đang chờ là Done on the site được tính là website của bạn đồng ý.

Lệnh gọi: các yêu cầu mà AutoPSEO đọc phản hồi

Lệnh gọi được ký giống như các lần phân phối. Một lệnh gọi chờ tối đa 60 giây theo mặc định và 30 giây cho các lần đọc lại và kiểm tra path, và đọc được phản hồi tới 24 MB. Bất kỳ phản hồi nào khác 2xx đều làm lệnh gọi thất bại.

site.export

Yêu cầu mang export: { what, cursor, limit }, cộng thêm since (một thời điểm ISO) cho chủ đề hoặc paths cho lần đọc lại. Hãy trả lời { "items": [], "next": "<cursor>" }; AutoPSEO hỏi lại với cursor đặt bằng next của bạn cho đến khi nó rỗng hoặc null.

  • what: "articles" chạy trong các lần quét site đầy đủ, 10 mục mỗi trang, khi bạn đã khai báo export có articles và adopt: true. Phản hồi 400 nghĩa là "không được hỗ trợ". Với paths: ["/answers/x"] và limit: 1, nó đọc lại một bài viết; hãy chỉ trả lời bằng bài viết đó.
  • what: "topics" chạy trong mọi lần quét và trong lần đồng bộ hằng ngày, 50 mục mỗi trang; lần đồng bộ hằng ngày gửi since, là thời điểm của lần kéo dữ liệu gần nhất.
  • what: "profile" chạy trong mọi lần quét. Hãy trả lời { "profile": { types, languages, rules, categories, format } }, mỗi trường đều tùy chọn: types dạng [{ key, name, path }], và rules gồm brandTerms, synonyms, noise, onTopic và offTopic.

Một bài viết được xuất cần có externalId, một slug (chữ cái và chữ số của mọi hệ chữ viết cùng dấu gạch ngang, tối đa 200 ký tự, các đoạn nối bằng /), một path kết thúc bằng slug đó, và copy có ít nhất title, h1 và một section. Các trường tùy chọn: lang (mặc định en), type (khóa loại nội dung), url, keyword, category, datePublished, dateModified, version, translations, owner (ID bài viết của AutoPSEO, sau khi được tiếp nhận) và maintenance: { kind: "refresh" or "optimize", reason }, xếp hàng tác vụ đó. Mỗi bài viết được xuất trở thành một bài viết được quản lý với văn bản của bạn làm bản sửa đổi đầu tiên, và không có gì được gửi ngược lại. Xuất lại là an toàn, và một bài viết mà AutoPSEO đã gỡ xuống sẽ không bao giờ quay lại qua một lần xuất.

Một mục chủ đề có dạng { keyword, status?, volume?, source?, externalId?, type?, reason? }, trong đó status là queued, planned hoặc rejected.

articles.adopted

Sau khi nhập, AutoPSEO yêu cầu bạn cho phép nó quản lý các bài viết mới: { "articles": [{ "externalId", "articleId", "slug", "path" }] }. Hãy trả lời { "adopted": ["<externalId>"], "refused": [{ "externalId": "<id>", "reason": "<why>" }] }. Một bài viết mà bạn không xác nhận sẽ bị gỡ khỏi AutoPSEO trở lại, trừ khi AutoPSEO đã viết một bản sửa đổi cho nó.

site.handover

{ "handover": { "types": ["answers"], "writer": "autopseo" } }, được gửi khi có người chọn Let AutoPSEO write them hoặc Hand writing back to the site trên trang Your site của property. Hãy trả lời { "ok": true, "writer": "autopseo" }. Sau khi giao cho AutoPSEO, website của bạn chuyển các chủ đề mà nó duyệt cho AutoPSEO thay vì tự viết.

path.check

{ "paths": [{ "path": "/interviews/google/data-engineer" }, { "path": "/interviews/jane-street", "new": { "part": "company", "value": "jane-street", "name": "Jane Street" } }] }. Hãy trả lời một kết quả cho mỗi path: { "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."] }] }. Một lời từ chối được lưu cùng lý do của bạn và không hỏi lại; path nào bạn bỏ qua sẽ được hỏi lại sau.

Một bên nhận tối giản

Một Cloudflare Worker xác minh chữ ký bằng Web Crypto API, xử lý ba lệnh về bài viết và từ chối phần còn lại bằng unknown_event. Nó lưu bài viết trong một KV namespace gắn với tên ARTICLES, còn khóa ký nằm trong một Worker secret tên 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);
  },
};

Trên Node.js, hãy xác minh tương tự bằng createHmac và timingSafeEqual, trên nội dung thô mà framework của bạn nhận được:

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

Tiếp theo: những gì tới được webhook do writing and review và maintenance quyết định; toàn bộ luồng nằm trong content engine overview.

Tự động hóa nội dung SEO của bạn

Kết nối Search Console và website của bạn. AutoPSEO sẽ quét website và lên kế hoạch cho các chủ đề đầu tiên ngay lập tức. Không cần thẻ tín dụng.