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 使用者時完全一樣。它寫完的每篇文章都會落到你的伺服器上、變成一篇文章,公開網址再回流到「報表」和「分析」,方便你追蹤成效。

← 返回說明文件庫

/ 需要幫忙?

端點接不上?打開工作區裡的「支援」區塊,或者 傳個訊息給我們。