OxiRanker ドキュメント
あなたの Web サイトを OxiRanker サービスに接続するための整理されたドキュメントセンターです。WordPress 連携または API 連携から始め、その後ガイドを step by step で進めてください。
API サービス
カスタム Web サイト、カスタム CMS プラットフォーム、Next.js、Laravel、Node.js、PHP、Python backend 向けに Export API と Webhook を使用します。
OxiRanker は destination website の permanent hosting ではありません
OxiRanker は、destination website の content、images、exported files に対して permanent hosting の保証を提供しません。OxiRanker は user-content hosting service ではなく、generated content、cover images、Open Graph images、output files は OxiRanker infrastructure 上に 90 日を超えて保持されない場合があります。
そのため、output を受け取った後、destination website は final article、images、SEO data、Open Graph data、Schema data を自分の database、server、storage、CDN に保存する必要があり、public display のために OxiRanker URLs に依存してはいけません。
接続トレーニング動画
WordPress を接続する前、または API 連携を実装する前に、この動画を視覚的な walkthrough として使用してください。
output flow を step by step で実装する
まず Export Token と Webhook Secret を backend に保存し、その後 Webhook receiver、database、UPSERT logic、Image Mirroring、daily sync を完成させます。
OxiRanker に登録して domain を作成する
まず OxiRanker に登録し、account に sign in して、Domains セクションから Add Domain をクリックします。
正しい domain 形式
Domain name フィールドには root domain のみを入力してください。アドレスに http、https、www を含めてはいけません。
example.com誤った形式
domain フィールドにこれらの形式を入力しないでください。
https://example.com
http://example.com
www.example.comdomain 所有権を確認する
この step では、domain があなたのものであること、または管理する access があることを証明する必要があります。
DNS TXT
推奨domain DNS に TXT record を追加してください。Cloudflare の場合、通常これが最も簡単な方法です。
DNS Name:
_yourdomain-verification.example.com
TXT Value:
acb1****29803HTTP File
OxiRanker が確認できるように、Web サイトの .well-known path に text file を配置してください。
File URL:
https://example.com/.well-known/yourdomain-verification.txt
File Content:
220daaa37****42730f27Meta Tag
Web サイト homepage の head section 内に meta tag を配置してください。
<meta name="yourdomain-verification" content="190208d****d275b65" />最初の domain を verify した後に無料 token を受け取る
最初の domain を verify した後、OxiRanker は initial testing のために account に 1000 free tokens を追加します。
言語と Blog URL を設定する
Blog URLs セクションで、Web サイトが single-language か multilingual か、また generated articles に使用する languages を定義します。
言語を選択する
OxiRanker は 24 languages をサポートしています。Web サイト構造に応じて 1 つまたは複数の languages を選択できます。
Blog URL を入力する
各 language について、その language の blog URL を入力してください。Web サイトが multilingual の場合、各 language path を個別に入力してください。
https://example.com/fa/blog
https://example.com/en/blog各 language に keywords を追加する
Keywords tab で、各 language に最低 3 個、最大 50 個の keywords を追加してください。
主なルール
各 language の keywords は同じ language で入力する必要があります。article language が English の場合、keywords も English である必要があります。
keywords の追加方法
各 keyword を入力した後、Enter を押して追加します。すべての keywords を完了したら、Save をクリックしてください。
domain Article Profile を完成させる
Article Profile は、あなたの brand が何であるか、誰に向けて書くか、どの tone を使うべきか、どの topics を避けるべきかを OxiRanker に伝えます。
Industry
market または business category を定義します。例: Dubai の real estate consulting と property buying and selling。
Business description
business が何を販売し、誰にサービスを提供し、どのように機能し、何が異なるのかを説明します。
Audience
content が誰に語りかけるべきかを定義します。各 audience は個別に保存できます。
Tone
formal、educational、simple、technical、sales-oriented など、generated content の style と voice を制御します。
Content goal
articles が達成すべき内容を定義します。例: customers を attract する、traffic を増やす、users を educate する。
禁止トピック
brand が話すべきでないもの、または関連付けられるべきでないものをここに入力します。
Optional CTA
CTA は、article を読んだ後に reader に取ってほしい action を意味します。適切な text がわからない場合は、この field を empty のままにする方がよいです。
内部リンクと外部リンク
OxiRanker は articles 内に internal links と external links を intelligent に適用できます。
Internal links
この option は default で enabled になっており、enabled のままにしておくことをおすすめします。Internal linking は search engines が Web サイトの content structure をよりよく理解するのに役立ちます。
最初の記事では、まだ previous article がないため、通常 internal links は作成されません。2 本目の記事以降、system は internal linking を実行できます。
External links
External links は official で trusted な sources に追加され、SEO authority が意図せず移転しないように nofollow として marked されます。
AI で profile quality を確認する
Article Profile を完成させた後、OxiRanker は profile quality を review します。
article summaries のために Telegram を接続する
profile を正常に save した後、Telegram channel への article summaries 送信を enable できます。
bot を追加する
@Oxiranker_Bot を Telegram channel に追加し、Admin にしてください。
channel username を入力する
channel name を @ 付きで入力してください。例: @my_channel
summary delivery を enable する
valid channel を登録した後、article summary delivery を enable してください。
automatic article engine を設定する
Automatic article generation tab で、automatic article generation engine を管理できます。
Smart topic selection
system は keywords と Article Profile を使用して、suitable、unique、publishable な topics を選択します。
Brand-aware generation
article generation 中、tone、audience、content goal、brand restrictions が尊重されます。
Scheduled publishing
Monthly capacity は月全体に分散されるため、content publishing がより natural で consistent に見えます。
Monthly capacity
7 articles / month
10 articles / month
15 articles / month
30 articles / month30 articles はほぼ 1 日 1 article を意味します。15 articles はほぼ 2 日に 1 回を意味します。10 articles は約 2〜3 日に 1 回を意味します。7 articles は約 3〜4 日に 1 回を意味します。
Article length と image
Article length は Short、Standard、Long のいずれかです。Standard はほとんどの Web サイトにとって balanced choice です。
Article images は enabled または disabled にでき、image theme は Light または Dark を選択できます。
Generate から最初の記事を作成する
domain を準備した後、Generate menu を開き、domain を選択して New Content または New Article をクリックします。
Language
各 content request では 1 つの language のみが選択されます。
Topic
明確な topic を書いてください。Source URL は optional です。
Image
image を enable または disable にし、Light または Dark theme を選択します。
Payment と generation
cost が wallet から deducted され、article generation が開始されます。
Preview、SEO Report、JSON Output を確認する
article が generated された後、quality review と publishing preparation のために複数の important outputs が利用できます。
Preview
article text と generated image を表示します。この section は publishing 前に final quality を確認するのに役立ちます。
SEO Report
SEO Score、title、meta description、canonical、keywords、schema、headings、links、images、OpenGraph を含む understandable SEO report を表示します。
JSON Output
technical users 向けに full article output を表示します。slug、html_body、schema、image URLs、tags、category、reading time、created/updated timestamps が含まれます。
Export API と Webhook は正確に何をするのか?
OxiRanker で article が generated された後、destination website は Export API でそれを read するか、article が ready になったとき Webhook で通知を受け取れます。
Method 1: Export API
destination website は、必要なときに Export Token を使って OxiRanker から articles を read できます。この method は manual sync、daily sync、article list pages、full article JSON の fetch に使用されます。
GET https://oxiranker.com/api/v1/users/domains/1/export/articles?limit=20&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonGET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonMethod 2: Webhook
new article が created されたとき、または article が updated されたとき、OxiRanker は destination website endpoint に通知します。Webhook は full article を送信しません。destination website が Export JSON API から full article を fetch できるよう、article identifier のみを送信します。
https://example.com/api/oxiranker/webhook{
"event": "article.created",
"domain_id": "1",
"article_id": "5",
"request_id": "5",
"lang": "fa",
"slug": "smart-internal-links",
"occurred_at": "2026-05-20T19:35:33.000Z"
}Content storage responsibility と Image Mirroring
destination website は final article と images を自分の infrastructure に store する必要があります。
destination website が store すべきもの
正しい image path の例
OxiRanker image URLs を直接かつ permanent に使用するのは正しくありません。image は download され、article URLs は destination website の internal URLs に置き換えられる必要があります。
https://oxiranker.com/uploads/articles/3_5/cover.webphttps://example.com/uploads/site-articles/oxiranker/1/5/cover.webpExport Token と ready endpoints を取得する
Export Token と ready-to-use endpoints は article page、Outputs tab、API Output section から利用できます。
Export Token の取得場所
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}Ready-to-use endpoints
同じ article page の API Output section 内で、Ready-to-use endpoints は prepared URLs を表示します。
https://oxiranker.com/api/v1/users/domains/1/export/articles?limit=20&offset=0https://oxiranker.com/api/v1/users/domains/1/articles/1/export/jsonhttps://oxiranker.com/api/v1/users/domains/1/articles/1/export/html?canonical_mode=absolute&include_css=0Webhook Secret と destination endpoint を作成する
Webhook Secret も article page、Outputs tab、API Output section から作成されます。
Webhook Secret はどこで作成されますか?
固定 destination endpoint
OxiRanker は destination Webhook に対して次の固定 path のみを受け付けます。destination website はこの endpoint を backend に作成する必要があります。
POST /api/oxiranker/webhook
https://example.com/api/oxiranker/webhookこの endpoint は POST のみを受け付け、raw body を preserve し、timestamp と signature を verify し、webhook.test に対して successful response を返す必要があります。
destination website に必要な .env values
OxiRanker は destination website files または .env に access できません。destination website developer はこれらの values を自分の backend に store する必要があります。
OXIRANKER_EXPORT_API_BASE_URL=https://oxiranker.com
OXIRANKER_EXPORT_API_TOKEN=YOUR_EXPORT_TOKEN
OXIRANKER_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET
OXIRANKER_WEBHOOK_MAX_SKEW_MS=300000
OXIRANKER_SOURCE_PROVIDER=oxiranker
OXIRANKER_SOURCE_DOMAIN_IDS=1
SITE_ARTICLE_IMAGE_UPLOAD_DIR=/var/www/example.com/uploads/site-articles
SITE_ARTICLE_IMAGE_MAX_BYTES=10485760
PUBLIC_BASE_URL=https://example.com
OXIRANKER_SYNC_ENABLED=true
OXIRANKER_SYNC_HOUR=3
OXIRANKER_SYNC_MINUTE=10
OXIRANKER_SYNC_PAGE_LIMIT=20
OXIRANKER_SYNC_MAX_PAGES=50Security values
Storage values
Export API: List、JSON、HTML、ZIP
destination website は Export API を使用して article list または各 article の full output を受け取れます。
domain の articles list を取得する
GET https://oxiranker.com/api/v1/users/domains/:domainId/export/articles?limit=20&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonlimit は articles の数を定義し、現在 1〜50 に制限されています。offset は pagination に使用されます。lang は optional で、fa、en、tr など specific language の articles を fetch するために使用されます。
full article を JSON として取得する
GET https://oxiranker.com/api/v1/users/domains/:domainId/articles/:articleId/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonこれは destination website にとって最も important な output です。destination website は full article JSON を自分の database に store し、public display のために自分の database から read する必要があります。
ready HTML を取得する
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/html?canonical_mode=absolute&include_css=0
Authorization: Bearer YOUR_EXPORT_TOKENHTML は ready output を必要とする Web サイトに適していますが、より professional な approach は JSON を store し、destination website 独自の template で render することです。
ZIP output を取得する
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/zip
Authorization: Bearer YOUR_EXPORT_TOKENZIP output には article.json、meta.json、assets.json、schema.jsonld、index.html、version.txt、README.txt が含まれます。
Webhook Signature と安全な Webhook 受信
destination website は original raw body を使用して Webhook signature を verify する必要があります。
Webhook headers
content-type: application/json
user-agent: Oxiranker-Webhooks/1.0
x-oxiranker-event: article.created
x-oxiranker-domain-id: 1
x-oxiranker-timestamp: 2026-05-20T19:35:33.000Z
x-oxiranker-signature: sha256=...Signature formula
payload_to_sign = timestamp + "." + rawBody
signature = "sha256=" + HMAC_SHA256(webhook_secret, payload_to_sign)destination website が最初に JSON を parse し、その後再度 stringify すると、order や spacing が変わり、signature が invalid になる可能性があります。signature verification には original raw body を使用する必要があります。
Webhook 受信用の simple Node.js code sample
この Express sample は raw body、timestamp、signature、webhook.test、article events を処理します。
import express from "express";
import crypto from "crypto";
const app = express();
app.use(
express.json({
verify: (req, _res, buf) => {
(req as any).rawBody = Buffer.from(buf);
},
})
);
function getHeader(req: express.Request, name: string): string {
const value = req.headers[name.toLowerCase()];
if (Array.isArray(value)) return String(value[0] || "").trim();
return String(value || "").trim();
}
function getRawBody(req: express.Request): string {
const raw = (req as any).rawBody;
if (Buffer.isBuffer(raw)) return raw.toString("utf8");
return JSON.stringify(req.body || {});
}
function createSignature(secret: string, timestamp: string, rawBody: string): string {
const payload = `${timestamp}.${rawBody}`;
const digest = crypto
.createHmac("sha256", secret)
.update(payload, "utf8")
.digest("hex");
return `sha256=${digest}`;
}
function timingSafeEqualText(a: string, b: string): boolean {
const ab = Buffer.from(a, "utf8");
const bb = Buffer.from(b, "utf8");
if (ab.length !== bb.length) return false;
return crypto.timingSafeEqual(ab, bb);
}
function isTimestampValid(timestamp: string): boolean {
const maxSkewMs = Number(process.env.OXIRANKER_WEBHOOK_MAX_SKEW_MS || 300000);
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) return false;
const diff = Math.abs(Date.now() - date.getTime());
return diff <= maxSkewMs;
}
app.post("/api/oxiranker/webhook", async (req, res) => {
const secret = String(process.env.OXIRANKER_WEBHOOK_SECRET || "").trim();
if (!secret) {
return res.status(500).json({
error: "webhook_secret_missing",
message: "Webhook secret is not configured.",
});
}
const eventHeader = getHeader(req, "x-oxiranker-event");
const domainIdHeader = getHeader(req, "x-oxiranker-domain-id");
const timestamp = getHeader(req, "x-oxiranker-timestamp");
const receivedSignature = getHeader(req, "x-oxiranker-signature");
const rawBody = getRawBody(req);
if (!eventHeader) {
return res.status(400).json({
error: "missing_webhook_event",
message: "Webhook event header is required.",
});
}
if (!domainIdHeader) {
return res.status(400).json({
error: "missing_webhook_domain_id",
message: "Webhook domain id header is required.",
});
}
if (!timestamp) {
return res.status(400).json({
error: "missing_webhook_timestamp",
message: "Webhook timestamp header is required.",
});
}
if (!receivedSignature) {
return res.status(400).json({
error: "missing_webhook_signature",
message: "Webhook signature header is required.",
});
}
if (!isTimestampValid(timestamp)) {
return res.status(401).json({
error: "invalid_webhook_timestamp",
message: "Webhook timestamp is expired or invalid.",
});
}
const expectedSignature = createSignature(secret, timestamp, rawBody);
if (!timingSafeEqualText(expectedSignature, receivedSignature)) {
return res.status(401).json({
error: "invalid_webhook_signature",
message: "Invalid webhook signature.",
});
}
const payload = req.body || {};
const event = String(payload.event || eventHeader || "").trim();
if (event === "webhook.test") {
const sourceDomainId = Number(payload.domain_id || domainIdHeader);
return res.status(200).json({
ok: true,
message: "Webhook test received successfully.",
event,
source_domain_id:
Number.isFinite(sourceDomainId) && sourceDomainId > 0 ? sourceDomainId : null,
test: true,
});
}
if (event !== "article.created" && event !== "article.updated") {
return res.status(400).json({
error: "unsupported_webhook_event",
message: "Webhook event is not supported.",
});
}
const sourceDomainId = Number(payload.domain_id || domainIdHeader);
const sourceArticleId = Number(payload.article_id);
if (!Number.isFinite(sourceDomainId) || sourceDomainId <= 0) {
return res.status(400).json({
error: "invalid_domain_id",
message: "Webhook domain id is invalid.",
});
}
if (!Number.isFinite(sourceArticleId) || sourceArticleId <= 0) {
return res.status(400).json({
error: "invalid_article_id",
message: "Webhook article id is invalid.",
});
}
// Fetch the full article from Export API and store it in the database here.
// await syncArticleFromOxiranker(sourceDomainId, sourceArticleId);
return res.status(200).json({
ok: true,
message: "Webhook processed successfully.",
event,
source_domain_id: sourceDomainId,
source_article_id: sourceArticleId,
});
});推奨 destination website database
destination website は articles を自分の database に store し、unique constraint で duplicates を防ぐ必要があります。
CREATE TABLE public.site_articles (
id BIGSERIAL PRIMARY KEY,
source_provider VARCHAR NOT NULL DEFAULT 'oxiranker',
source_domain_id BIGINT NOT NULL,
source_article_id BIGINT NOT NULL,
source_request_id BIGINT NULL,
lang VARCHAR NOT NULL,
lang_label VARCHAR NULL,
dir VARCHAR NOT NULL,
is_rtl BOOLEAN NULL,
length VARCHAR NULL,
topic_text TEXT NULL,
slug VARCHAR NOT NULL,
canonical_path VARCHAR NULL,
effective_canonical_url TEXT NULL,
title TEXT NOT NULL,
meta_description TEXT NULL,
html_body TEXT NULL,
focus_keywords JSONB NOT NULL DEFAULT '[]'::jsonb,
category VARCHAR NULL,
tags JSONB NOT NULL DEFAULT '[]'::jsonb,
reading_time_minutes INTEGER NULL,
og_title TEXT NULL,
og_description TEXT NULL,
cover_image_url TEXT NULL,
og_image_url TEXT NULL,
article_schema JSONB NOT NULL DEFAULT '{}'::jsonb,
raw_list_item JSONB NOT NULL DEFAULT '{}'::jsonb,
raw_detail_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
source_created_at TIMESTAMPTZ NULL,
source_updated_at TIMESTAMPTZ NULL,
sync_status VARCHAR NOT NULL DEFAULT 'synced',
last_synced_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_sync_error TEXT NULL,
publish_status VARCHAR NOT NULL DEFAULT 'draft',
published_at TIMESTAMPTZ NULL,
deleted_at TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
source_cover_image_url TEXT NULL,
source_og_image_url TEXT NULL,
image_mirror_status VARCHAR NOT NULL DEFAULT 'pending',
image_mirror_error TEXT NULL,
image_mirrored_at TIMESTAMPTZ NULL,
CONSTRAINT site_articles_dir_check
CHECK (dir IN ('ltr', 'rtl')),
CONSTRAINT site_articles_reading_time_check
CHECK (reading_time_minutes IS NULL OR reading_time_minutes >= 0),
CONSTRAINT site_articles_sync_status_check
CHECK (sync_status IN ('pending', 'syncing', 'synced', 'failed')),
CONSTRAINT site_articles_publish_status_check
CHECK (publish_status IN ('draft', 'published', 'archived')),
CONSTRAINT site_articles_image_mirror_status_check
CHECK (image_mirror_status IN ('pending', 'not_needed', 'mirrored', 'failed')),
CONSTRAINT site_articles_source_unique
UNIQUE (source_provider, source_domain_id, source_article_id)
);CREATE INDEX site_articles_source_lookup_idx
ON public.site_articles (source_provider, source_domain_id, source_article_id);
CREATE INDEX site_articles_source_updated_at_idx
ON public.site_articles (source_updated_at DESC);
CREATE INDEX site_articles_slug_idx
ON public.site_articles (slug)
WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX site_articles_lang_slug_active_unique
ON public.site_articles (lang, slug)
WHERE deleted_at IS NULL;
CREATE INDEX site_articles_lang_publish_idx
ON public.site_articles (lang, publish_status)
WHERE deleted_at IS NULL;
CREATE INDEX site_articles_published_at_idx
ON public.site_articles (published_at DESC)
WHERE deleted_at IS NULL;
CREATE INDEX site_articles_focus_keywords_gin_idx
ON public.site_articles USING gin (focus_keywords);
CREATE INDEX site_articles_tags_gin_idx
ON public.site_articles USING gin (tags);
CREATE INDEX site_articles_article_schema_gin_idx
ON public.site_articles USING gin (article_schema);UPSERT で duplicates を防ぐ
article は各 Webhook ごとに再度 insert してはいけません。すでに存在する場合は update する必要があります。
INSERT INTO public.site_articles (
source_provider,
source_domain_id,
source_article_id,
source_request_id,
lang,
dir,
slug,
title,
meta_description,
html_body,
focus_keywords,
category,
tags,
reading_time_minutes,
cover_image_url,
og_image_url,
article_schema,
source_created_at,
source_updated_at,
sync_status,
last_synced_at,
publish_status,
published_at,
created_at,
updated_at
)
VALUES (
'oxiranker',
1,
5,
5,
'fa',
'rtl',
'smart-internal-links',
'Article title',
'Meta description',
'<p>HTML body</p>',
'[]'::jsonb,
'SEO',
'[]'::jsonb,
7,
'https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp',
'https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp',
'{}'::jsonb,
'2026-05-20T19:33:23.618Z',
'2026-05-20T19:34:09.629Z',
'synced',
now(),
'published',
now(),
now(),
now()
)
ON CONFLICT (source_provider, source_domain_id, source_article_id)
DO UPDATE SET
source_request_id = EXCLUDED.source_request_id,
lang = EXCLUDED.lang,
dir = EXCLUDED.dir,
slug = EXCLUDED.slug,
title = EXCLUDED.title,
meta_description = EXCLUDED.meta_description,
html_body = EXCLUDED.html_body,
focus_keywords = EXCLUDED.focus_keywords,
category = EXCLUDED.category,
tags = EXCLUDED.tags,
reading_time_minutes = EXCLUDED.reading_time_minutes,
cover_image_url = EXCLUDED.cover_image_url,
og_image_url = EXCLUDED.og_image_url,
article_schema = EXCLUDED.article_schema,
source_created_at = EXCLUDED.source_created_at,
source_updated_at = EXCLUDED.source_updated_at,
sync_status = 'synced',
last_synced_at = now(),
last_sync_error = NULL,
publish_status = CASE
WHEN public.site_articles.publish_status = 'archived'
THEN public.site_articles.publish_status
ELSE 'published'
END,
published_at = COALESCE(public.site_articles.published_at, EXCLUDED.published_at, now()),
deleted_at = NULL,
updated_at = now();Webhook 後に full article を fetch して sync する
Webhook が到着したら、destination website は Export API から full article を fetch し、images を mirror し、article を UPSERT する必要があります。
async function fetchOxirankerArticleDetail(args: {
baseUrl: string;
token: string;
domainId: number;
articleId: number;
}) {
const url = `${args.baseUrl.replace(/\/+$/, "")}/api/v1/users/domains/${args.domainId}/articles/${args.articleId}/export/json`;
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${args.token}`,
Accept: "application/json",
"User-Agent": "My-Site-Oxiranker-Sync/1.0",
},
});
const bodyText = await response.text();
if (response.status < 200 || response.status > 299) {
throw new Error(
`Export API request failed with HTTP status ${response.status}. Response: ${bodyText.slice(0, 1000)}`
);
}
try {
return JSON.parse(bodyText);
} catch {
throw new Error("Export API returned invalid JSON.");
}
}async function syncArticleFromWebhook(payload: any) {
const sourceDomainId = Number(payload.domain_id);
const sourceArticleId = Number(payload.article_id);
if (!Number.isFinite(sourceDomainId) || sourceDomainId <= 0) {
throw new Error("Webhook domain id is invalid.");
}
if (!Number.isFinite(sourceArticleId) || sourceArticleId <= 0) {
throw new Error("Webhook article id is invalid.");
}
const article = await fetchOxirankerArticleDetail({
baseUrl: process.env.OXIRANKER_EXPORT_API_BASE_URL!,
token: process.env.OXIRANKER_EXPORT_API_TOKEN!,
domainId: sourceDomainId,
articleId: sourceArticleId,
});
const mirroredImages = await mirrorArticleImages(article);
const finalHtmlBody = replaceImageUrls(article.html_body, mirroredImages);
const finalArticleSchema = replaceImageUrlsInJson(article.article_schema, mirroredImages);
await upsertSiteArticle({
source_provider: process.env.OXIRANKER_SOURCE_PROVIDER || "oxiranker",
source_domain_id: sourceDomainId,
source_article_id: sourceArticleId,
source_request_id: article.request_id,
lang: article.lang,
dir: article.dir,
slug: article.slug,
title: article.title,
meta_description: article.meta_description,
html_body: finalHtmlBody,
focus_keywords: article.focus_keywords,
category: article.category,
tags: article.tags,
reading_time_minutes: article.reading_time_minutes,
cover_image_url: mirroredImages.cover_image_url,
og_image_url: mirroredImages.og_image_url,
article_schema: finalArticleSchema,
raw_detail_payload: article,
source_created_at: article.created_at,
source_updated_at: article.updated_at,
});
}Image Mirroring と image storage rules
destination website は images を download し、自分の infrastructure に store し、article URLs を replace する必要があります。
Image Mirroring rules
推奨 storage path
/uploads/site-articles/oxiranker/1/5/cover.webp
/uploads/site-articles/oxiranker/1/5/og.webphttps://example.com/uploads/site-articles/oxiranker/1/5/cover.webp信頼性のための daily sync
Webhook だけでは十分ではありません。destination website は missed articles を recover するために daily sync も持つべきです。
なぜ daily sync が必要ですか?
Daily sync algorithm
destination website に articles を表示する
articles を site_articles に store した後、destination website は各 public display のために OxiRanker に接続してはいけません。自分の database から read する必要があります。
Blog list page
SELECT
id,
lang,
lang_label,
dir,
is_rtl,
slug,
title,
meta_description,
category,
tags,
reading_time_minutes,
cover_image_url,
og_image_url,
source_created_at,
published_at,
created_at
FROM public.site_articles
WHERE source_provider = 'oxiranker'
AND deleted_at IS NULL
AND sync_status = 'synced'
AND publish_status = 'published'
ORDER BY COALESCE(source_created_at, published_at, created_at) DESC, id DESC
LIMIT 20 OFFSET 0;Article detail page
SELECT *
FROM public.site_articles
WHERE source_provider = 'oxiranker'
AND lang = 'fa'
AND slug = 'smart-internal-links'
AND deleted_at IS NULL
AND sync_status = 'synced'
AND publish_status = 'published'
LIMIT 1;SEO、Schema、RTL、HTML rendering
destination website は SEO data を自分の database から read し、article page で render する必要があります。
SEO fields
HTML rendering
<div
className="article-content"
dangerouslySetInnerHTML={{ __html: article.html_body }}
/>destination website に strict security policy がある場合、HTML を save 前または display 前に sanitize できます。
RTL と LTR
各 article には lang、dir、is_rtl fields があります。dir が rtl の場合、article page は right-to-left で render される必要があります。
<article dir="rtl">
...
</article>よくある errors とその意味
destination website errors は English で、clear かつ understandable である必要があります。そうすることで troubleshooting を迅速に行えます。
Export API errors
Webhook errors
最終 API と Webhook testing checklist
implementation 後、これらの tests によって connection を beginning から end まで verify します。
Export Token と JSON test
GET https://oxiranker.com/api/v1/users/domains/1/export/articles?limit=2&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonGET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonWebhook test
destination Webhook を Postman で signature なしに call した場合、security error を受け取る必要があります。これは endpoint が active で security を check していることを意味します。
{
"error": "missing_webhook_signature",
"message": "Webhook signature header is required."
}その後、OxiRanker 内で Test Webhook をクリックしてください。receiver が webhook.test を support している場合、response_status は 200 である必要があります。
正しい implementation のための重要な rules
API integration では、system を secure、stable、maintainable に保つためにこれらの rules に従う必要があります。
Complete API flow summary
これらの steps を完了すると、custom website は OxiRanker で generated された articles を安全に receive、store、mirror、publish できます。