OROVA.VN — BIZ AI AGENT

/ 开发者指南

通过 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 字段

这是发布(新文章)请求的字段。删除和更新请求复用同一个接口,请求体更短,见下方各自的章节。

字段类型说明
titlestring文章标题。
slugstring为该文章建议的、适合放进网址的标识符。
content_htmlstring整篇文章正文,已是可直接发布的 HTML。
excerptstring简短摘要,用作 meta description。
featured_image_urlstring | null封面图网址;没有封面图时为 null。
keywordstring这篇文章所针对的目标 SEO 关键词。
langstringISO 语言代码,例如 "en" 或 "vi"。
published_atstringOrova 发送这篇文章的时刻,ISO 8601 格式的 UTC 时间,不带时区后缀。
statusstring | undefined可选。"draft" 让文章保持未发布;"publish" 或不填则直接发布。
actionstring | undefined新文章没有这个字段。"delete" 表示请你删除一篇文章;"update"(由「优化」引擎发出)表示请你就地覆盖一篇已有文章。
target_urlstring只在 action = "update" 时出现:要覆盖的那篇文章的公开网址。请保持该网址不变。
idnumber | undefined只有当 Orova 知道你站内的文章 id(来自 `list` 或 `get`)时才会发送:请先按它匹配,再按 `target_url`。

可选字段没有内容时,Orova 会整个省掉这个键,绝不发送 null,所以读取可选字段时请做好防护。status 同样是可选的:"draft" 把文章存为草稿,"publish" 或不带 status 则立即发布。

/ 约定

你必须返回的响应

创建好文章之后,返回 HTTP 200201 状态,以及这样的 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 会同时发送 idtarget_urlslug——按这个顺序,用你认得的那个去查。查不到就返回 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 用户时完全一样。它写完的每篇文章都会落到你的服务器上、变成一篇文章,公开网址再回流到「报告」和「分析」,方便你追踪表现。

← 返回指南库

/ 需要帮忙?

接口接不上?打开工作区里的「支持」板块,或者 给我们发条消息。