OXIRANKER DEVELOPER DOCS

OxiRanker 文档

一个清晰的文档中心,用于将你的网站连接到 OxiRanker 服务。先从 WordPress 集成或 API 集成开始,然后按照指南逐步完成。

官方文档Full-feature V1适用于网站所有者、开发者和集成团队
文档菜单
从菜单中选择一个指南。此页面是 OxiRanker 的主文档中心。
API 服务

API 服务

将 Export API 和 Webhook 用于 custom websites、custom CMS platforms、Next.js、Laravel、Node.js、PHP 或 Python backend。

Domain
已验证
内容
就绪
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 中,并且不应依赖 OxiRanker URLs 进行 public display。

培训视频

连接培训视频

在连接 WordPress 或实现 API 集成之前,将此视频作为可视化 walkthrough 使用。

API Output

逐步实现 output flow

先将 Export Token 和 Webhook Secret 存储在你的 backend 中,然后完成 Webhook receiver、database、UPSERT logic、Image Mirroring 和 daily sync。

步骤 01

在 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 后
如果 domain 格式 valid,OxiRanker 会创建 domain,并将你带到 domain ownership verification step。
步骤 02

验证 domain 所有权

在此 step 中,你必须证明该 domain 属于你,或者你拥有管理它的 access。

DNS TXT

推荐

向 domain DNS 添加 TXT record。对于 Cloudflare,这通常是最简单的 method。

DNS Name:
_yourdomain-verification.example.com

TXT Value:
acb1****29803

HTTP File

在你的网站 .well-known path 中放置一个 text file,以便 OxiRanker 可以验证。

File URL:
https://example.com/.well-known/yourdomain-verification.txt

File Content:
220daaa37****42730f27

Meta Tag

在你的网站 homepage 的 head section 中放置一个 meta tag。

<meta name="yourdomain-verification" content="190208d****d275b65" />
重要 DNS 说明
添加 DNS record 后,尤其是在 Cloudflare 中,你可能需要等待 2 到 3 分钟。然后在同一个 OxiRanker page 上点击 Confirm。
Verification token 有效期
Domain verification values 有 time limit。目前,verification values 通常 valid 1440 minutes。
步骤 03

验证第一个 domain 后获得免费 token

验证第一个 domain 后,OxiRanker 会向你的 account 添加 1000 free tokens,用于 initial testing。

仅适用于第一个 domain
此 gift 只会添加给第一个 verified domain,不会为后续 domains 再次添加。
步骤 04

配置语言和 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
URL structure
如果你的网站结构不同,请输入你网站真实的 blog URL。重点是 final URL 必须是 articles 要 publish 的 path。
步骤 05

为每种 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 匹配。
步骤 06

完成 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 profile
如果你的网站是 multilingual,只需用一种 language 完成 profile。OxiRanker 可以基于同一个 profile 为其他 languages 准备所需 versions。
步骤 07

内部链接和外部链接

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 传递。

步骤 08

用 AI 检查 profile quality

完成 Article Profile 后,OxiRanker 会 review profile quality。

85+
save 所需的 minimum score
95
改进后通常达到的 excellent score
AI
每个 field 的 professional suggestions
如果 score 低于 85
System 不允许 final saving,并会为每个 problematic field 显示 explanations 和 improvement suggestions。你可以 apply suggestions 后再次 save。
步骤 09

连接 Telegram 以发送 article summaries

Profile 成功 save 后,你可以 enable 将 article summaries 发送到 Telegram channel。

1

添加 bot

将 @Oxiranker_Bot 添加到你的 Telegram channel,并将其设为 Admin。

2

输入 channel username

输入带 @ 的 channel name。示例:@my_channel

3

Enable summary delivery

注册 valid channel 后,enable article summary delivery。

Telegram 优势
如果 enabled,在 content generation 后,article summary 会带着 image 和 link 发送到 Telegram channel。这可以增强指向你网站的 traffic paths。
步骤 10

配置 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 / month

30 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。

API websites 说明
Automatic engine 会在 OxiRanker 内部 generate articles。要在 generation 后将每篇 article 移动到你的 custom website,你必须 implement Export API、Webhook、database storage 和 Image Mirroring。
步骤 11

从 Generate 创建第一篇 article

准备好 domain 后,打开 Generate menu,选择 domain,然后点击 New Content 或 New Article。

1

Language

每个 content request 只选择一种 language。

2

Topic

写一个清晰的 topic。Source URL 是 optional。

3

Image

Enable 或 disable image,并选择 Light 或 Dark theme。

4

Payment 和 generation

Cost 会从 wallet 中 deducted,然后 article generation 开始。

Generation time
Article 和 image generation 通常需要约 2 到 3 分钟。完成后,article 会在 OxiRanker 内 visible。
Source URL
如果输入 Source URL,OxiRanker 会使用它来更好地理解 topic,但不会 generate copied content。System designed 用于创建 unique content。
步骤 12

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 的含义
这些 outputs 表明 article 不只是 plain text。它是为 WordPress、CMS、API 和 publishing systems 准备的 structured、SEO-ready content。
步骤 13

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/json
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json

Method 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"
}
非常重要的 note
Webhook 只是 lightweight message。Destination website 不应 expect Webhook 中包含 full article。收到 domain_id 和 article_id 后,它必须从 Export JSON API fetch complete article 并 store 到自己的 database。
步骤 14

Content storage responsibility 和 Image Mirroring

Destination website 必须将 final article 和 images store 在自己的 infrastructure 上。

Destination website 必须 store 的内容

Destination website database 中的 full article text
Destination server、storage 或 CDN 上的 main article image
Destination server、storage 或 CDN 上的 Open Graph image
SEO、OpenGraph 和 Article Schema data
用于 preserve original output 的 raw_detail_payload 和 raw_list_item

正确 image path 示例

Directly 且 permanently 使用 OxiRanker image URLs 是不正确的。Image 必须 download,并且 article URLs 必须 replace 为 destination website 的 internal URLs。

https://oxiranker.com/uploads/articles/3_5/cover.webp
https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp
OxiRanker 不是 destination website 的 permanent hosting
OxiRanker 不负责为 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 中。
步骤 15

获取 Export Token 和 ready endpoints

Export Token 和 ready-to-use endpoints 可从 article page、Outputs tab 和 API Output section 获取。

从哪里获取 Export Token

在 OxiRanker 中 create 第一篇 article。
Open 那篇 article page。
Open Outputs tab。
Open API Output section。
从 Token management create token。
Full token 只会 shown 一次。
将 token store 到 destination website backend 或 .env 中。
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=0
https://oxiranker.com/api/v1/users/domains/1/articles/1/export/json
https://oxiranker.com/api/v1/users/domains/1/articles/1/export/html?canonical_mode=absolute&include_css=0
Rotating Token
如果 Export Token 在 OxiRanker panel 中 rotated,previous token 会 expires。New value 必须 immediately replace destination website .env 或 backend environment 中的 old value,并且 destination backend 必须 restarted 或 redeployed。
步骤 16

创建 Webhook Secret 和 destination endpoint

Webhook Secret 也从 article page、Outputs tab 和 API Output section 创建。

Webhook Secret 在哪里创建?

Open article page。
Open Outputs tab。
Open API Output section。
在 Webhook Secret section 中 create secret。
Full secret 只会 shown 一次。
将 secret store 到 destination website .env 中。

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。

Active events
在 current version 中,events 是 fixed:article.created 和 article.updated。为了 testing connection,会发送 event value webhook.test。
步骤 17

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=50

Security values

OXIRANKER_EXPORT_API_TOKEN
Export API token 只能 stored 在 backend 中。
OXIRANKER_WEBHOOK_SECRET
Webhook Secret 只能 stored 在 backend 中。
OXIRANKER_WEBHOOK_MAX_SKEW_MS
Webhook 允许的 maximum time difference。300000 表示 5 minutes。

Storage values

SITE_ARTICLE_IMAGE_UPLOAD_DIR
用于在 destination server 上 storing mirrored images 的 physical path。
SITE_ARTICLE_IMAGE_MAX_BYTES
Download 和 storage 允许的 maximum image size。
PUBLIC_BASE_URL
用于 create final image 和 article URLs 的 public destination website URL。
Frontend security
Export Token 和 Webhook Secret 不得放在 frontend code、user-visible JavaScript、localStorage 或 window object 中。
步骤 18

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/json

limit 定义 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_TOKEN

HTML 适合需要 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_TOKEN

ZIP output 包含 article.json、meta.json、assets.json、schema.jsonld、index.html、version.txt 和 README.txt。

步骤 19

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。

Timing-safe comparison
不要用 expected === received 比较 signatures。在 Node.js 中最好使用 crypto.timingSafeEqual。
步骤 20

用于 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,
  });
});
步骤 21

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 note
在 PostgreSQL 中,Persian 或 Unicode text 使用 simple quotes。不需要也不应使用类似 N'...' 的内容。
步骤 22

使用 UPSERT prevent duplicates

Article 不应在每个 Webhook 中重新 inserted。如果它已经 exists,则必须 updated。

Preventing duplicates 的 main key
用于 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();
步骤 23

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,
  });
}
步骤 24

Image Mirroring 和 image storage rules

Destination website 必须 download images,将其 store 在自己的 infrastructure 上,并 replace article URLs。

Image Mirroring rules

只能 accepted HTTPS URLs。
File 必须真实是 image。
content-type 必须以 image/ 开头。
Empty files 不应 stored。
File size 不得超过 limit。
如果 cover 和 og 相同,只 download 一次。
Mirroring 后,replace html_body 和 article_schema 中的 URLs。

Recommended storage path

/uploads/site-articles/oxiranker/1/5/cover.webp
/uploads/site-articles/oxiranker/1/5/og.webp
https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp
步骤 25

用于 reliability 的 daily sync

Webhook alone 不够。Destination website 还应有 daily sync,用于 recover missed articles。

为什么需要 daily sync?

Destination website 可能 temporarily down。
Webhook 可能 timeout。
可能发生 deployment 或 network issues。
Article 可能 generated,但它的 Webhook 可能 missed。

Daily sync algorithm

从 0 start offset。
使用 limit 20 或 50。
从 Export List API fetch article list。
对于每个 item,fetch 该 article 的 Export JSON。
Mirror images。
使用 UPSERT store article。
Increase offset 并 continue,直到 total completed。
步骤 26

在 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;
Recommended article URL
对于 multilingual websites,你可以使用 /{lang}/blog/{slug} pattern。示例:/fa/blog/smart-internal-links
步骤 27

SEO、Schema、RTL 和 HTML rendering

Destination website 必须从自己的 database read SEO data,并在 article page 上 render。

SEO fields

用于 page title 的 title
用于 meta description 的 meta_description
用于 canonical 的 effective_canonical_url
用于 Open Graph 的 og_title 和 og_description
用于 social preview image 的 og_image_url 或 cover_image_url
用于 JSON-LD 的 article_schema

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>
步骤 28

Common errors 及其含义

Destination website errors 必须是 English、clear 且 understandable,这样 troubleshooting 才能快速完成。

Export API errors

缺少 Domain export token。
未发送 Export API token。必须发送 Authorization: Bearer YOUR_EXPORT_TOKEN。
Domain export token invalid。
Token wrong、rotated、未关联到此 domain,或 domainId wrong。
Token 没有 required scope。
Token 没有 required scope。New tokens 必须包含 articles:read 和 exports:read。
lang invalid。
Query parameter 中的 lang value 被 incorrectly 发送。

Webhook errors

Webhook URL 必须使用 HTTPS。
Webhook URL 必须以 https:// 开头。
Webhook signature invalid。
Webhook Secret wrong、secret rotated、raw body changed,或 signature incorrectly calculated。
Webhook timestamp expired 或 invalid。
Timestamp old、在 future 中,或 invalid。请检查 server clock 和 NTP。
Webhook test delivery 失败。
Destination website 没有 return successful status 200 到 299、timed out,或没有 correctly handle webhook.test。
步骤 29

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/json
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json

Webhook 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。

Rotation 后
如果 Webhook Secret 或 Export Token rotated,请立即 copy new value,在 destination website .env 中 replace,并 restart 或 redeploy destination backend。
步骤 30

Correct implementation 的 important rules

在 API integration 中必须遵守这些 rules,以便 system 保持 secure、stable 和 maintainable。

Export Token 只能 stored 在 backend 中。
Webhook Secret 只能 stored 在 backend 中。
Destination Webhook endpoint 必须完全是 /api/oxiranker/webhook。
Webhook URL 必须使用 HTTPS。
Webhook signature 和 timestamp 必须始终 verified。
Articles 必须使用 UPSERT stored,而不是 simple insert。
Prevent duplicates 需要 unique constraint。
Images 必须从 OxiRanker downloaded,并 stored 在 destination website 上。
Destination website 不应 permanently depend 于 OxiRanker image URLs。
Image URLs 必须在 html_body 和 article_schema 中 replaced。
如果 Webhook fails,daily sync 必须 recover missed article。
Shown 给 users 或 logs 的 errors 必须是 English 且 understandable。
如果 article archived,automatic sync 不应再次 publish 它。
raw_detail_payload 和 raw_list_item 必须 preserved,以免 original output data 丢失。
如果 Export Token 或 Webhook Secret rotated,new value 必须在 .env 中 replaced,并且 backend 必须 restarted 或 redeployed。
步骤 31

Complete API flow summary

完成这些 steps 后,你的 custom website 可以 securely receive、store、mirror 和 publish OxiRanker 中 generated 的 articles。

在 OxiRanker 注册
添加 domain
Verify domain ownership
选择 languages 并添加 Blog URL
为每种 language 添加 keywords
完成 Article Profile
如有需要,配置 Telegram
配置 automatic article engine
从 Generate create 第一篇 article
Review Preview 和 SEO Report
从 API Output 获取 Export Token
获取 ready Export API endpoints
获取 Webhook Secret
在 destination backend 中 create fixed /api/oxiranker/webhook endpoint
将 Token 和 Secret store 到 destination website .env 中
Create site_articles table 和 unique constraint
Implement 从 Export JSON full article fetching
Implement Image Mirroring
Implement UPSERT
Implement daily sync
从 destination database render article、SEO、OG 和 Schema
Run final Export API 和 Webhook tests