OxiRanker 文档
一个清晰的文档中心,用于将你的网站连接到 OxiRanker 服务。先从 WordPress 集成或 API 集成开始,然后按照指南逐步完成。
从菜单中选择一个指南。此页面是 OxiRanker 的主文档中心。
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 格式 valid,OxiRanker 会创建 domain,并将你带到 domain ownership verification step。
验证 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" />添加 DNS record 后,尤其是在 Cloudflare 中,你可能需要等待 2 到 3 分钟。然后在同一个 OxiRanker page 上点击 Confirm。
Domain verification values 有 time limit。目前,verification values 通常 valid 1440 minutes。
验证第一个 domain 后获得免费 token
验证第一个 domain 后,OxiRanker 会向你的 account 添加 1000 free tokens,用于 initial testing。
此 gift 只会添加给第一个 verified domain,不会为后续 domains 再次添加。
配置语言和 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如果你的网站结构不同,请输入你网站真实的 blog URL。重点是 final URL 必须是 articles 要 publish 的 path。
为每种 language 添加 keywords
在 Keywords tab 中,为每种 language 添加至少 3 个、最多 50 个 keywords。
主要规则
每种 language 的 keywords 必须使用同一种 language 输入。如果 article language 是 English,那么 keywords 也必须是 English。
如何添加 keywords
输入每个 keyword 后,按 Enter 添加。完成所有 keywords 后,点击 Save。
如果你为 English language 输入 Turkish keywords,English content 中可能会出现 Turkish phrases。Keyword language 必须与 article language 匹配。
完成 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 留空。
如果你的网站是 multilingual,只需用一种 language 完成 profile。OxiRanker 可以基于同一个 profile 为其他 languages 准备所需 versions。
内部链接和外部链接
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。
System 不允许 final saving,并会为每个 problematic field 显示 explanations 和 improvement suggestions。你可以 apply suggestions 后再次 save。
连接 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。
如果 enabled,在 content generation 后,article summary 会带着 image 和 link 发送到 Telegram channel。这可以增强指向你网站的 traffic paths。
配置 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。
Automatic engine 会在 OxiRanker 内部 generate articles。要在 generation 后将每篇 article 移动到你的 custom website,你必须 implement Export API、Webhook、database storage 和 Image Mirroring。
从 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 开始。
Article 和 image generation 通常需要约 2 到 3 分钟。完成后,article 会在 OxiRanker 内 visible。
如果输入 Source URL,OxiRanker 会使用它来更好地理解 topic,但不会 generate copied content。System designed 用于创建 unique content。
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。
这些 outputs 表明 article 不只是 plain text。它是为 WordPress、CMS、API 和 publishing systems 准备的 structured、SEO-ready content。
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"
}Webhook 只是 lightweight message。Destination website 不应 expect Webhook 中包含 full article。收到 domain_id 和 article_id 后,它必须从 Export JSON API fetch complete article 并 store 到自己的 database。
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.webpOxiRanker 不负责为 destination website permanently storing output content 和 images。OxiRanker 不是 user-content hosting service,generated content、cover images、Open Graph images 或 output files 可能不会在 OxiRanker infrastructure 上保留超过 90 days。因此,destination website 必须将 final article、images、SEO data、OpenGraph 和 Schema store 在自己的 database、server、storage 或 CDN 中。
获取 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如果 Export Token 在 OxiRanker panel 中 rotated,previous token 会 expires。New value 必须 immediately replace destination website .env 或 backend environment 中的 old value,并且 destination backend 必须 restarted 或 redeployed。
创建 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。
在 current version 中,events 是 fixed:article.created 和 article.updated。为了 testing connection,会发送 event value webhook.test。
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 Token 和 Webhook Secret 不得放在 frontend code、user-visible JavaScript、localStorage 或 window object 中。
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。
不要用 expected === received 比较 signatures。在 Node.js 中最好使用 crypto.timingSafeEqual。
用于 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);在 PostgreSQL 中,Persian 或 Unicode text 使用 simple quotes。不需要也不应使用类似 N'...' 的内容。
使用 UPSERT prevent duplicates
Article 不应在每个 Webhook 中重新 inserted。如果它已经 exists,则必须 updated。
用于 preventing duplicates 的最 important constraint 是:UNIQUE (source_provider, source_domain_id, source_article_id)
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;对于 multilingual websites,你可以使用 /{lang}/blog/{slug} pattern。示例:/fa/blog/smart-internal-links
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。
如果 Webhook Secret 或 Export Token rotated,请立即 copy new value,在 destination website .env 中 replace,并 restart 或 redeploy destination backend。
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。