OxiRanker 文档
一个清晰的文档中心,用于将你的网站连接到 OxiRanker 服务。先从 WordPress 集成或 API 集成开始,然后按照指南逐步完成。
API 服务
将 Export API 和 Webhook 用于 custom websites、custom CMS platforms、Next.js、Laravel、Node.js、PHP 或 Python backend。
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 中,并且不应依赖 OxiRanker URLs 进行 public display。
连接培训视频
在连接 WordPress 或实现 API 集成之前,将此视频作为可视化 walkthrough 使用。
逐步实现 output flow
先将 Export Token 和 Webhook Secret 存储在你的 backend 中,然后完成 Webhook receiver、database、UPSERT logic、Image Mirroring 和 daily sync。
在 OxiRanker 注册并创建 domain
首先,在 OxiRanker 注册,sign in 到你的 account,然后在 Domains 部分点击 Add Domain。
正确的 domain 格式
在 Domain name 字段中,只输入 root domain。地址不能包含 http、https 或 www。
example.com错误格式
不要在 domain 字段中输入这些格式。
https://example.com
http://example.com
www.example.com验证 domain 所有权
在此 step 中,你必须证明该 domain 属于你,或者你拥有管理它的 access。
DNS TXT
推荐向 domain DNS 添加 TXT record。对于 Cloudflare,这通常是最简单的 method。
DNS Name:
_yourdomain-verification.example.com
TXT Value:
acb1****29803HTTP File
在你的网站 .well-known path 中放置一个 text file,以便 OxiRanker 可以验证。
File URL:
https://example.com/.well-known/yourdomain-verification.txt
File Content:
220daaa37****42730f27Meta Tag
在你的网站 homepage 的 head section 中放置一个 meta tag。
<meta name="yourdomain-verification" content="190208d****d275b65" />验证第一个 domain 后获得免费 token
验证第一个 domain 后,OxiRanker 会向你的 account 添加 1000 free tokens,用于 initial testing。
配置语言和 Blog URL
在 Blog URLs 部分,定义你的网站是 single-language 还是 multilingual,以及 generated articles 应使用哪些 languages。
选择语言
OxiRanker 支持 24 languages。你可以根据网站结构选择一种或多种 languages。
输入 Blog URL
对于每种 language,输入该 language 的 blog URL。如果你的网站是 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 会告诉 OxiRanker 你的 brand 是什么、为谁写作、应使用什么 tone,以及应避免哪些 topics。
Industry
定义 market 或 business category。示例:Dubai 的 real estate consulting 和 property buying and selling。
Business description
说明 business 销售什么、服务谁、如何运作,以及它的差异点。
Audience
定义 content 应面向谁。每个 audience 都可以单独 save。
Tone
控制 generated content 的 style 和 voice,例如 formal、educational、simple、technical 或 sales-oriented。
Content goal
定义 articles 应达到什么目标,例如吸引 customers、增加 traffic 或教育 users。
禁止主题
任何 brand 不应讨论或不应与之关联的内容,都在此处输入。
Optional CTA
CTA 指你希望 reader 在阅读 article 后执行的 action。如果不确定哪种 text suitable,最好将此 field 留空。
内部链接和外部链接
OxiRanker 可以在 articles 内 intelligent 应用 internal links 和 external links。
Internal links
此 option 默认 enabled,最好保持 enabled。Internal linking 帮助 search engines 更好地理解你网站的 content structure。
在第一篇 article 中,通常不会创建 internal links,因为还没有 previous article。从第二篇 article 开始,system 可以执行 internal linking。
External links
External links 会添加到 official 和 trusted sources,并被 marked 为 nofollow,以免 SEO authority 被 unintentionally 传递。
用 AI 检查 profile quality
完成 Article Profile 后,OxiRanker 会 review profile quality。
连接 Telegram 以发送 article summaries
Profile 成功 save 后,你可以 enable 将 article summaries 发送到 Telegram channel。
添加 bot
将 @Oxiranker_Bot 添加到你的 Telegram channel,并将其设为 Admin。
输入 channel username
输入带 @ 的 channel name。示例:@my_channel
Enable summary delivery
注册 valid channel 后,enable article summary delivery。
配置 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 表示几乎每天一篇 article。15 articles 表示几乎隔天一篇。10 articles 表示大约每两到三天一篇。7 articles 表示大约每三到四天一篇。
Article length 和 image
Article length 可以是 Short、Standard 或 Long。Standard 对大多数网站来说是 balanced choice。
Article images 可以 enabled 或 disabled,image theme 可以选择 Light 或 Dark。
从 Generate 创建第一篇 article
准备好 domain 后,打开 Generate menu,选择 domain,然后点击 New Content 或 New Article。
Language
每个 content request 只选择一种 language。
Topic
写一个清晰的 topic。Source URL 是 optional。
Image
Enable 或 disable image,并选择 Light 或 Dark theme。
Payment 和 generation
Cost 会从 wallet 中 deducted,然后 article generation 开始。
Review Preview、SEO Report 和 JSON Output
Article generated 后,会提供多个 important outputs,用于 quality review 和 publishing preparation。
Preview
显示 article text 和 generated image。此 section 适合在 publishing 前 review final quality。
SEO Report
显示 understandable SEO report,包括 SEO Score、title、meta description、canonical、keywords、schema、headings、links、images 和 OpenGraph。
JSON Output
为 technical users 显示 full article output,包括 slug、html_body、schema、image URLs、tags、category、reading time 和 created/updated timestamps。
Export API 和 Webhook 具体做什么?
当 article 在 OxiRanker 中 generated 后,destination website 可以使用 Export API read 它,或者在 article ready 时通过 Webhook 被 notified。
Method 1: Export API
Destination website 可以在需要时使用 Export Token 从 OxiRanker read articles。此 method 用于 manual sync、daily sync、article list pages 和 fetching full article JSON。
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 会 notify destination website endpoint。Webhook 不发送 full article。它只发送 article identifier,以便 destination website 可以从 Export JSON API fetch full article。
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 store 在自己的 infrastructure 上。
Destination website 必须 store 的内容
正确 image path 示例
Directly 且 permanently 使用 OxiRanker image URLs 是不正确的。Image 必须 download,并且 article URLs 必须 replace 为 destination website 的 internal URLs。
https://oxiranker.com/uploads/articles/3_5/cover.webphttps://example.com/uploads/site-articles/oxiranker/1/5/cover.webp获取 Export 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=0创建 Webhook Secret 和 destination endpoint
Webhook Secret 也从 article page、Outputs tab 和 API Output section 创建。
Webhook Secret 在哪里创建?
Fixed destination endpoint
OxiRanker 只接受以下 fixed path 作为 destination Webhook。Destination website 必须在自己的 backend 中 create 此 endpoint。
POST /api/oxiranker/webhook
https://example.com/api/oxiranker/webhook此 endpoint 必须只 accept POST,preserve raw body,verify timestamp 和 signature,并为 webhook.test return successful response。
Destination website 所需的 .env values
OxiRanker 无法 access destination website files 或 .env。Destination website developer 必须将这些 values store 在自己的 backend 中。
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 receive article list 或每篇 article 的 full output。
获取 domain 的 list of articles
GET https://oxiranker.com/api/v1/users/domains/:domainId/export/articles?limit=20&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonlimit 定义 number of articles,目前 limited 在 1 到 50 之间。offset 用于 pagination。lang 是 optional,用于 fetch 特定 language 的 articles,例如 fa、en 或 tr。
以 JSON 获取 full article
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 store 在自己的 database 中,并从自己的 database read 以进行 public display。
获取 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 的 websites,但更 professional 的 approach 是 store JSON,并使用 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 和 secure Webhook receiving
Destination website 必须使用 original raw body verify Webhook signature。
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 先 parse JSON,然后再次 stringify,order 或 spacing 可能会改变,signature 可能变为 invalid。Signature verification 必须使用 original raw body。
用于 receiving Webhook 的 simple Node.js code sample
此 Express sample 会 handle 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,
});
});Recommended destination website database
Destination website 必须将 articles store 在自己的 database 中,并通过 unique constraint prevent 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 prevent duplicates
Article 不应在每个 Webhook 中重新 inserted。如果它已经 exists,则必须 updated。
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 后 fetch full article 并 sync
当 Webhook 到达时,destination website 必须从 Export API fetch full article,mirror images,并 UPSERT article。
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 必须 download images,将其 store 在自己的 infrastructure 上,并 replace article URLs。
Image Mirroring rules
Recommended 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用于 reliability 的 daily sync
Webhook alone 不够。Destination website 还应有 daily sync,用于 recover missed articles。
为什么需要 daily sync?
Daily sync algorithm
在 destination website 上显示 articles
Articles storing 到 site_articles 后,destination website 不应为了每次 public display 而 connect 到 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 必须从自己的 database read SEO data,并在 article page 上 render。
SEO fields
HTML rendering
<div
className="article-content"
dangerouslySetInnerHTML={{ __html: article.html_body }}
/>如果 destination website 有 strict security policy,可以在 saving 前或 displaying 前 sanitize HTML。
RTL 和 LTR
每篇 article 都有 lang、dir 和 is_rtl fields。如果 dir 是 rtl,article page 必须 rendered right-to-left。
<article dir="rtl">
...
</article>Common errors 及其含义
Destination website errors 必须是 English、clear 且 understandable,这样 troubleshooting 才能快速完成。
Export API errors
Webhook errors
Final API 和 Webhook testing checklist
Implementation 后,这些 tests 会从 beginning 到 end verify connection。
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
如果你用 Postman call destination Webhook 且不带 signature,应该 receive security error。这表示 endpoint active 并 checks security。
{
"error": "missing_webhook_signature",
"message": "Webhook signature header is required."
}然后在 OxiRanker 中点击 Test Webhook。如果 receiver supports webhook.test,response_status 必须为 200。
Correct implementation 的 important rules
在 API integration 中必须遵守这些 rules,以便 system 保持 secure、stable 和 maintainable。
Complete API flow summary
完成这些 steps 后,你的 custom website 可以 securely receive、store、mirror 和 publish OxiRanker 中 generated 的 articles。