/ 開発者向けガイド
API 経由で あらゆるサイトへ公開。
WordPress ではないサイトへ Orova が完成記事を公開できるよう、自前のエンドポイントを作る方法。
Orova は最初から WordPress への公開に対応しています。サイトが WordPress ではない場合 — 自作 CMS、ヘッドレス構成、静的サイト、独自バックエンド — でも、小さな API で記事を受け取れば自動公開できます。用意するのは HTTP エンドポイント 1 本。記事が 1 本仕上がるたびに Orova がそこを呼びます。このページは開発者向けで、リクエストの取り決めを最後まで説明します。
/ 概要
仕組み
キーワードに対する記事を書き終えると、Orova は登録済みのエンドポイント URL へ HTTP POST リクエストを 1 回だけ送ります。ボディは JSON で、記事の全文が入っています。あなたのエンドポイントは自分側で記事を作成し、その記事の最終的な公開 URL を返します。Orova はその URL を公開リンクとして保存します。WordPress 記事のときと同じです。WordPress 以外のサイトで手離れよく公開したいとき、あるいは記事の保存方法を自分で完全に決めたいときに使ってください。
/ 設定
設定 — 6 ステップ
- 1
プロジェクトで「API で接続」をオンにする. プロジェクトを開きます(プロジェクト → 対象のプロジェクト → 接続)。WordPress のカードの隣に API で接続のカードがあります。そこに自分のエンドポイント URL を入力します。シークレットキー欄は任意です。空欄なら Orova が生成し、サイトで既に使っているキーを貼ってもかまいません。保存するとキーがカードに表示され、コピーボタンが付きます。1 つのプロジェクトの公開先は 1 つだけです。API で接続すると WordPress の接続ボタンはロックされ、逆も同じです。
- 2
POST を受けるエンドポイントを作る. 自分のサイトかサーバーに、JSON ボディの HTTP POST を待ち受けるルートを作ります。これがプロジェクトの「接続」画面に貼り戻す URL です。記事が 1 本仕上がるごとに Orova が 1 回呼び出します。
- 3
毎回 Bearer トークンを検証する. Orova からのリクエストには必ず「Bearer <secret>」形式の Authorization ヘッダーが付きます。値はプロジェクトに表示されているシークレットキーそのものです。保存してある値と突き合わせ、一致しないものはすべて拒否してください。他人が勝手に投稿するのを防げるのはこれだけです。
- 4
payload から記事を作る. JSON ボディを読み、CMS かデータベースに記事を作成します。記事本体には title、slug、content_html を使い、excerpt は meta description に、featured_image_url はアイキャッチ画像に、keyword / lang / published_at はメタ情報として扱います。
- 5
記事の公開 URL を返す. HTTP 200 か 201 と、JSON ボディ { "url": "https://yoursite.com/the-new-article" } を返します。Orova はその URL を記事の公開リンクとして保存します。2xx 以外のステータスを返した場合、または url 項目が無い場合、Orova は公開失敗として扱います。
- 6
最適化エンジンからの更新リクエストに対応する. ボディに action = "update" が入っている場合は、target_url(または slug)で既存記事を探し、その場で title、content_html、excerpt を上書きします。URL は変えません。同じ記事を指す { "url": ... } を付けて 200 を返してください。未対応のエンドポイントでは、その最適化が失敗と記録されるだけです。新規記事の公開はこれまでどおり動きます。
/ 取り決め
Orova が送るリクエスト
仕上がった記事は、次の形のリクエスト 1 本として届きます。
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 | この記事に提案される、URL 向けの識別子。 |
| content_html | string | そのまま公開できる HTML 形式の記事本文一式。 |
| excerpt | string | 短い要約。meta description 用です。 |
| featured_image_url | string | null | アイキャッチ画像の URL。無い場合は null。 |
| keyword | string | この記事が狙っている SEO キーワード。 |
| lang | string | ISO コードによる言語。例:"en"、"vi"。 |
| published_at | string | Orova が記事を送った時刻。UTC の ISO 8601 形式で、タイムゾーンの接尾辞は付きません。 |
| status | string | undefined | 任意。"draft" なら未公開のまま、"publish" または値なしなら公開します。 |
| action | string | undefined | 新規記事にはありません。"delete" は記事の削除依頼、"update"(最適化エンジンが送信)は既存記事をその場で上書きする依頼です。 |
| target_url | string | action = "update" のときだけ入ります。上書き対象記事の公開 URL です。この URL は変更しないでください。 |
| id | number | undefined | `list` や `get` で取得した記事 ID を Orova が知っている場合だけ届きます。`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 による最小構成の受け取りエンドポイントです。トークンを検証し、action(update / delete)で分岐し、記事を作成または変更し、URL を返す — この流れはどの言語・どのフレームワークでも同じです。
// 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 の最適化エンジンは、すでに公開されている記事を書き直します(情報の更新、見出しの改善、内容の統合)。そのうえで既存記事をその場で上書きし、URL は変えません。リクエストは同じエンドポイント URL へ、同じ Bearer トークンを付けた POST として届きます。
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 を差し替え、URL はそのままにして、200 と { "url": "<同じ記事の URL>" } を返します。2xx 以外のステータス、または url の無いボディを返すと、Orova はその最適化を失敗として記録します(記事が重複して作られることはありません)。未実装のエンドポイントでも新規記事の公開はそのまま続きます。更新への対応が要るのは、最適化画面を使うようになってからです。
8 月 29 日から、更新のボディには id(target_url より先に照合)、アイキャッチを差し替える featured_image_url、"publish" か "draft" を取る status も入ります。3 つとも任意で、中身がなければキーごと省かれ、null として送られることはありません。
/ 取り決め — Visual Editor
記事の読み取り(Visual Editor)
Visual Editor はサイトに公開済みの記事をそのまま開くので、まず記事を読み取る必要があります。同じ endpoint URL・同じ Bearer トークンで動く action が 2 つ増えます。list は記事一覧を、get は 1 記事の全文を返します。
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 では id・target_url・slug がまとめて届くので、この順で分かるものを使って照合してください。該当なしなら 404 と { "ok": false, "err": "not_found" } を返します。この 2 つの action が要るのは、Visual Editor でサイトの記事を編集したい場合だけです。自動投稿と自動最適化には不要です。
/ 取り決め — Visual Editor
画像のアップロード(Visual Editor)
Visual Editor に画像を置くと、Orova はその画像を base64 にして JSON ボディに入れ、同じ endpoint URL へ送ります。multipart は使わず、守るべき URL も増えません。
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 はあなたのサイトにも削除を依頼します。リクエストは同じエンドポイント URL へ、同じ Bearer トークンを付けた POST として届きます。違うのはボディだけです。
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 トークンは必ず検証する. あなたのエンドポイントは公開 URL です。誰でも呼べます。そのリクエストが本当に Orova から来たと証明できるのは Authorization ヘッダーだけなので、保存してあるシークレットと完全に一致しないリクエストはすべて拒否してください。
- エンドポイントは HTTPS で公開する. シークレットはヘッダーに乗って流れます。HTTPS なら通信途中で読まれずに済みます。
- シークレットをコードに書かない. 環境変数かシークレット管理サービスに保管し、コミットするファイルに直書きしないでください。万一漏れたら、プロジェクト → 接続 で新しいキーを生成します。
- content_html は信頼できるマークアップではなく、コンテンツとして扱う. CMS 内のほかの記事本文と同じように、慎重に保存・表示してください。
/ まとめ
取り決めはこれで全部です
エンドポイントが動き、プロジェクトに登録できていれば、Orova はあなたのサイトへ自動で公開します。WordPress 利用者の場合とまったく同じです。仕上がった記事はあなたのサーバーに届いて記事になり、その公開 URL がレポートと分析に戻ってくるので、成果を追いかけられます。
/ お困りですか?
エンドポイントの接続でつまずいていますか。ワークスペース内のサポートを開くか、 メッセージをお送りください。
