/ 开发者指南
通过 API 发布到任意网站。
如何搭建你自己的接口,让 Orova 把写好的文章发布到非 WordPress 网站。
Orova 开箱即可发布到 WordPress。如果你的网站不是 WordPress — 自研 CMS、headless 架构、静态站点或你自己的后端 — 依然可以让 Orova 自动发布,只要用一个小型 API 接收文章即可。你写一个 HTTP 接口;Orova 每写完一篇文章就调用一次。本页面面向开发者,完整说明请求约定。
/ 概览
工作方式
当 Orova 为某个关键词写完一篇文章,它会向你登记的接口地址发送一个 HTTP POST 请求。请求体是 JSON,包含整篇文章。你的接口在自己这边创建文章,然后返回文章最终的公开网址。Orova 会把这个网址存为已发布链接,和 WordPress 文章一样。如果你想在非 WordPress 网站上实现全自动发布,或者希望完全掌控文章的存储方式,就用这种方式。
/ 配置
配置——六个步骤
- 1
在项目中开启“通过 API 连接”. 打开你的项目(项目 → 你的项目 → 连接)。WordPress 卡片旁边有一张通过 API 连接的卡片。把你的接口地址填进去。密钥一栏是可选的:留空则由 Orova 自动生成,或者粘贴网站已在用的密钥。保存后,密钥会显示在卡片上,旁边有复制按钮。一个项目只发布到一个目标——通过 API 连接后,WordPress 的连接按钮会被锁定,反之亦然。
- 2
搭建一个接受 POST 的接口. 在你自己的网站或服务器上建一个路由,监听带 JSON 请求体的 HTTP POST。这就是你要粘回项目「连接」页面的网址。Orova 每写完一篇文章就调用它一次。
- 3
每个请求都要校验 Bearer token. Orova 的每个请求都带有 Authorization 请求头,格式为“Bearer <secret>”,用的正是项目里显示的那把密钥。把它和你保存的副本比对,不一致就一律拒绝——这是唯一能阻止别人往你网站发文章的东西。
- 4
根据 payload 创建文章. 读取 JSON 请求体,在你的 CMS 或数据库里创建文章:用 title、slug 和 content_html 组成正文,用 excerpt 作为 meta description,用 featured_image_url 作为封面图,keyword / lang / published_at 作为附带信息。
- 5
返回文章的真实网址. 返回 HTTP 200 或 201,请求体为 JSON { "url": "https://yoursite.com/the-new-article" }。Orova 会把该网址存为这篇文章的已发布链接。如果你返回非 2xx 状态,或缺少 url 字段,Orova 会判定此次发布失败。
- 6
处理来自「优化」引擎的更新请求. 当请求体里带有 action = "update" 时,按 target_url(或 slug)找到已有文章,就地覆盖它的 title、content_html 和 excerpt——网址保持不变。返回 200,并在 { "url": ... } 中指向同一篇文章。还没实现这一步的接口,只会让 Orova 把该次优化标记为失败;新文章的发布照常运作。
/ 约定
Orova 发来的请求
每篇写完的文章都会以这样一个请求送达:
POST <your endpoint URL>
Headers:
Content-Type: application/json
Authorization: Bearer <secret> # the secret key shown in Project -> Connections
User-Agent: Orova-SEO
Body (JSON):
{
"title": "...", # article headline
"slug": "...", # URL-friendly identifier
"content_html": "...", # full article HTML
"excerpt": "...", # meta description
"featured_image_url": "..." | null,
"keyword": "...", # target keyword
"lang": "en", # ISO language code
"published_at": "2026-05-17T09:00:00.482913", # ISO 8601, UTC, no offset suffix
"status": "draft" # optional: "draft" keeps it unpublished
}
# Optional fields are left out of the body when empty — never sent as null./ 参考
payload 字段
这是发布(新文章)请求的字段。删除和更新请求复用同一个接口,请求体更短,见下方各自的章节。
| 字段 | 类型 | 说明 |
|---|---|---|
| title | string | 文章标题。 |
| slug | string | 为该文章建议的、适合放进网址的标识符。 |
| content_html | string | 整篇文章正文,已是可直接发布的 HTML。 |
| excerpt | string | 简短摘要,用作 meta description。 |
| featured_image_url | string | null | 封面图网址;没有封面图时为 null。 |
| keyword | string | 这篇文章所针对的目标 SEO 关键词。 |
| lang | string | ISO 语言代码,例如 "en" 或 "vi"。 |
| published_at | string | Orova 发送这篇文章的时刻,ISO 8601 格式的 UTC 时间,不带时区后缀。 |
| status | string | undefined | 可选。"draft" 让文章保持未发布;"publish" 或不填则直接发布。 |
| action | string | undefined | 新文章没有这个字段。"delete" 表示请你删除一篇文章;"update"(由「优化」引擎发出)表示请你就地覆盖一篇已有文章。 |
| target_url | string | 只在 action = "update" 时出现:要覆盖的那篇文章的公开网址。请保持该网址不变。 |
| id | number | undefined | 只有当 Orova 知道你站内的文章 id(来自 `list` 或 `get`)时才会发送:请先按它匹配,再按 `target_url`。 |
可选字段没有内容时,Orova 会整个省掉这个键,绝不发送 null,所以读取可选字段时请做好防护。status 同样是可选的:"draft" 把文章存为草稿,"publish" 或不带 status 则立即发布。
/ 约定
你必须返回的响应
创建好文章之后,返回 HTTP 200 或 201 状态,以及这样的 JSON 响应体:
HTTP 200 (or 201)
Content-Type: application/json
{
"url": "https://yoursite.com/published-article"
}Orova 会把这个 url 存为该文章的已发布链接。如果你的接口返回任何非 2xx 状态,或响应体不是带 url 字段的 JSON 对象,Orova 会判定发布失败并给文章做标记,方便你重试。请在 60 秒内回复——超时 Orova 会中断请求(发布、更新、删除都一样)。
/ 示例
代码示例
一个用 Node.js 加 Express 写的最简接收接口。步骤在任何语言或框架里都一样:校验 token,按 action 分支(update / delete),创建或修改文章,最后返回网址。
// Node.js / Express — a minimal receiving endpoint
import express from "express";
const app = express();
app.use(express.json({ limit: "5mb" }));
// The secret Orova generated for this project.
const OROVA_SECRET = process.env.OROVA_SECRET;
app.post("/orova/publish", async (req, res) => {
// 1. Verify the Bearer token.
const auth = req.get("authorization") || "";
if (auth !== "Bearer " + OROVA_SECRET) {
return res.status(401).json({ error: "unauthorized" });
}
// 2. Visual Editor: list your articles. action = "list".
if (req.body.action === "list") {
const posts = await listPosts(req.body.limit || 500); // newest first, 500 max
return res.status(200).json({ ok: true, posts });
}
// 3. Visual Editor: read one article in full. action = "get".
if (req.body.action === "get") {
const post = await findPost(req.body.id, req.body.target_url, req.body.slug);
if (!post) return res.status(404).json({ ok: false, err: "not_found" });
return res.status(200).json({ ok: true, post });
}
// 4. Visual Editor: store an image, answer with its absolute URL.
if (req.body.action === "upload_image") {
const { filename, mime, data_base64 } = req.body;
if (!String(mime || "").startsWith("image/")) {
return res.status(422).json({ error: "image files only" });
}
const bytes = Buffer.from(data_base64, "base64");
if (bytes.length > 15 * 1024 * 1024) {
return res.status(413).json({ error: "image too large" });
}
return res.status(200).json({ ok: true, url: await saveImage(filename, bytes) });
}
// 5. Delete requests: body carries action = "delete".
if (req.body.action === "delete") {
const removed = await deletePostByUrl(req.body.url, req.body.slug);
if (!removed) return res.status(404).json({ deleted: true }); // already gone
return res.status(200).json({ deleted: true });
}
// 6. Update requests: action = "update". Optional fields may be absent.
if (req.body.action === "update") {
const updated = await updatePost(req.body.id, req.body.target_url, {
title: req.body.title,
html: req.body.content_html,
metaDescription: req.body.excerpt,
coverImage: req.body.featured_image_url, // optional
status: req.body.status, // optional: "publish" | "draft"
});
if (!updated) return res.status(404).json({ error: "post not found" });
return res.status(200).json({ url: updated.url });
}
// 7. New article: read the payload Orova sent.
const {
title, slug, content_html, excerpt,
featured_image_url, keyword, lang, published_at, status,
} = req.body;
// 8. Create the article in your own CMS or database.
const post = await createPost({
title,
slug,
html: content_html,
metaDescription: excerpt,
coverImage: featured_image_url,
keyword,
lang,
publishedAt: published_at,
draft: status === "draft", // status is optional; absent means publish now
});
// 9. Return 200/201 with the live URL.
return res.status(201).json({
url: "https://yoursite.com/" + post.slug,
});
});
app.listen(3000);/ 约定 — 更新
更新文章(优化)
Orova 的「优化」引擎会重写已经上线的文章——更新事实、改进标题、合并内容——然后就地覆盖已有文章,网址保持不变。请求发往同一个接口地址,同样是 POST,用同一把 Bearer token:
POST <your endpoint URL>
Headers:
Content-Type: application/json
Authorization: Bearer <secret>
User-Agent: Orova-SEO
Body (JSON):
{
"action": "update", # always the literal string "update"
"id": 123, # optional: post id, match on this first
"target_url": "...", # public URL of the post to overwrite (match next)
"title": "...", # new headline
"slug": "...", # slug fallback for matching, may be empty
"content_html": "...", # full new article body as HTML
"excerpt": "...", # new meta description
"featured_image_url": "...", # optional: new cover image
"status": "publish" # optional: "publish" or "draft"
}
# Optional fields are left out of the body when empty — never sent as null.按 target_url 找到文章(找不到再用 slug),替换它的标题、正文和 meta description,网址原样保留,然后返回 200 和 { "url": "<同一篇文章的网址>" }。任何非 2xx 状态,或响应体缺少 url,都会让 Orova 把这次优化标记为失败(它绝不会创建重复文章)。还没实现这一步的接口,照常发布新文章不受影响——只有当你用到「优化」页面时才需要补上。
从 8 月 29 日起,更新请求的 body 还可能带上 id(先按它匹配,再按 target_url)、用于更换封面的 featured_image_url,以及取值 "publish" 或 "draft" 的 status。三者都是可选的,为空时直接省略,不会发成 null。
/ 约定 — Visual Editor
读取文章(Visual Editor)
Visual Editor 直接打开你站点上已有的文章,所以它得先读到这些文章。这是同一个 endpoint URL、同一个 Bearer token 上的另外两个 action:list 返回文章列表,get 返回其中一篇的完整内容。
POST <your endpoint URL> # same endpoint, same Bearer secret
Body (JSON):
{
"action": "list", # always the literal string "list"
"limit": 500 # newest article first, 500 maximum
}
Response — HTTP 200:
{
"ok": true,
"posts": [
{
"id": 123, # your own post id, reused by "get" and "update"
"title": "...",
"slug": "...",
"link": "https://yoursite.com/the-article",
"date": "2026-08-29", # published date, YYYY-MM-DD
"modified": "2026-08-29",
"status": "publish", # "publish" or "draft"
"author": "..."
}
]
}POST <your endpoint URL>
Body (JSON):
{
"action": "get", # always the literal string "get"
"id": 123, # match on this first
"target_url": "...", # then the public URL
"slug": "..." # then the slug
}
Response — HTTP 200:
{
"ok": true,
"post": {
"id": 123,
"title": "...",
"slug": "...",
"link": "https://yoursite.com/the-article",
"content_html": "...", # full article body as HTML
"excerpt": "...",
"featured_image_url": "...",
"status": "publish" # "publish" or "draft"
}
}
No such post — HTTP 404:
{ "ok": false, "err": "not_found" }limit 上限为 500,最新的文章排在最前面。get 时 Orova 会同时发送 id、target_url 和 slug——按这个顺序,用你认得的那个去查。查不到就返回 404,内容为 { "ok": false, "err": "not_found" }。只有你想用 Visual Editor 修改站点文章时才需要这两个 action,自动发布和自动优化不需要它们。
/ 约定 — Visual Editor
上传图片(Visual Editor)
你在 Visual Editor 里放入一张图片时,Orova 会把它以 base64 放进 JSON body,发到同一个 endpoint URL——不用 multipart,也不用再保护第二个地址。
POST <your endpoint URL>
Body (JSON):
{
"action": "upload_image", # always the literal string "upload_image"
"filename": "cover.png", # original file name, extension included
"mime": "image/png", # image/* only
"data_base64": "iVBORw0KGgo..." # file bytes, base64, 15 MB maximum
}
Response — HTTP 200 (or 201):
{
"ok": true,
"url": "https://yoursite.com/uploads/cover.png" # absolute URL
}只接受 image/*,超过 15 MB 一律拒绝。存好文件后,返回图片的绝对 URL:相对路径会让文章在别处显示时坏掉。这个 action 只有 Visual Editor 才用得到。
/ 约定 — 删除
删除文章
当有人在 Orova 里删除一篇已发布的文章(在「写文章」或「报告」页面),Orova 会请求你的网站一并删除。请求发往同一个接口地址,同样是 POST,用同一把 Bearer token——唯一的区别在请求体:
POST <your endpoint URL>
Headers:
Content-Type: application/json
Authorization: Bearer <secret>
User-Agent: Orova-SEO
Body (JSON):
{
"action": "delete", # always the literal string "delete"
"url": "...", # public URL of the article to remove (match on this first)
"slug": "...", # slug fallback, may be empty
"keyword": "..." # the article's target keyword, for your logs
}按 url 查找文章(找不到再用 slug),删除或取消发布,然后返回 200 和 { "deleted": true }——只返回 204 也可以。如果文章已经不存在,返回 404,Orova 视为已删除。返回 { "deleted": false } 则告诉 Orova 文章没有被删除。普通的发布请求绝不会带 action 字段,所以现有接口照常运作——删除功能可做可不做,但建议实现。
/ 安全
安全提示
- 务必校验 Bearer token. 你的接口是一个公开网址——谁都能调用。Authorization 请求头是唯一能证明请求确实来自 Orova 的东西,所以凡是 token 与你保存的密钥不完全一致的请求,一律拒绝。
- 接口要走 HTTPS. 密钥是放在请求头里传输的。HTTPS 能防止它在传输途中被读取。
- 不要把密钥写进代码. 把它存到环境变量或密钥管理服务里,绝不要硬编码在会提交的文件中。一旦泄露,到「项目 → 连接」重新生成一把。
- 把 content_html 当作内容,而不是可信标记. 存储和渲染时,请像对待 CMS 里任何一篇文章正文那样谨慎处理。
/ 小结
整个约定就这些
接口上线并在项目里登记好之后,Orova 就会自动发布到你的网站——和面向 WordPress 用户时完全一样。它写完的每篇文章都会落到你的服务器上、变成一篇文章,公开网址再回流到「报告」和「分析」,方便你追踪表现。
