OXIRANKER DEVELOPER DOCS

Документация OxiRanker

Понятный центр документации для подключения вашего сайта к сервисам OxiRanker. Начните с интеграции WordPress или API-интеграции, затем следуйте руководству шаг за шагом.

Официальная документацияFull-feature V1Для владельцев сайтов, разработчиков и интеграционных команд
Меню документации
Выберите руководство из меню. Эта страница является главным центром документации OxiRanker.
API-сервис

API-сервис

Используйте Export API и Webhook для пользовательских сайтов, custom CMS платформ, Next.js, Laravel, Node.js, PHP или Python backend.

Domain
Подтверждено
Контент
Готово
API
Webhook
Важное уведомление о хранении

OxiRanker не является permanent hosting для сайта назначения

OxiRanker не предоставляет гарантию permanent hosting для контента, изображений или exported files сайта назначения. OxiRanker не является user-content hosting service, а generated content, cover images, Open Graph images или output files могут не храниться в инфраструктуре OxiRanker более 90 дней.

Поэтому после получения output сайт назначения должен сохранить final article, images, SEO data, Open Graph data и Schema data в своей database, на своем server, storage или CDN и не должен зависеть от OxiRanker URLs для публичного отображения.

Обучающее видео

Обучающее видео по подключению

Используйте это видео как визуальное руководство перед подключением WordPress или реализацией API-интеграции.

API Output

Реализуйте output flow шаг за шагом

Сначала сохраните Export Token и Webhook Secret в своем backend, затем завершите Webhook receiver, database, UPSERT logic, Image Mirroring и daily sync.

ШАГ 01

Зарегистрироваться и создать domain в OxiRanker

Сначала зарегистрируйтесь в OxiRanker, sign in в свой account и нажмите Add Domain в разделе Domains.

Правильный формат 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.
ШАГ 02

Подтвердить владение domain

На этом шаге нужно доказать, что domain принадлежит вам или что у вас есть access для его управления.

DNS TXT

Рекомендуется

Добавьте TXT record в DNS domain. Для Cloudflare это обычно самый простой метод.

DNS Name:
_yourdomain-verification.example.com

TXT Value:
acb1****29803

HTTP File

Разместите text file в path .well-known вашего сайта, чтобы OxiRanker мог его проверить.

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

File Content:
220daaa37****42730f27

Meta Tag

Разместите meta tag внутри head section homepage вашего сайта.

<meta name="yourdomain-verification" content="190208d****d275b65" />
Важное DNS-примечание
После добавления DNS record, особенно в Cloudflare, может потребоваться подождать 2–3 минуты. Затем нажмите Confirm на той же странице OxiRanker.
Срок действия verification token
Domain verification values имеют time limit. Сейчас verification values обычно valid в течение 1440 minutes.
ШАГ 03

Получить бесплатные token после подтверждения первого domain

После подтверждения первого domain OxiRanker добавляет 1000 free tokens в ваш account для initial testing.

Только для первого domain
Этот подарок добавляется только для первого verified domain и не добавляется повторно для следующих domains.
ШАГ 04

Настроить языки и Blog URL

В разделе Blog URLs определите, является ли ваш сайт single-language или multilingual, и какие languages должны использоваться для generated articles.

Выберите языки

OxiRanker поддерживает 24 languages. Вы можете выбрать один или несколько languages в зависимости от структуры вашего сайта.

Введите Blog URL

Для каждого language введите blog URL этого языка. Если ваш сайт multilingual, введите каждый language path отдельно.

https://example.com/fa/blog
https://example.com/en/blog
Структура URL
Если у вашего сайта другая структура, введите реальный blog URL вашего сайта. Важно, чтобы final URL был тем path, где articles должны публиковаться.
ШАГ 05

Добавить keywords для каждого языка

Во вкладке Keywords добавьте минимум 3 и максимум 50 keywords для каждого языка.

Главное правило

Keywords для каждого языка должны быть введены на том же языке. Если article language — English, keywords также должны быть English.

Как добавить keywords

После ввода каждого keyword нажмите Enter, чтобы добавить его. После завершения всех keywords нажмите Save.

Распространенная ошибка
Если вы введете Turkish keywords для English language, в English content могут появиться Turkish phrases. Keyword language должен совпадать с article language.
ШАГ 06

Заполнить domain Article Profile

Article Profile сообщает OxiRanker, что представляет собой ваш brand, для кого он пишет, какой tone использовать и каких topics избегать.

Industry

Определяет market или business category. Пример: real estate consulting и property buying and selling в Dubai.

Business description

Объясняет, что продает business, кого обслуживает, как работает и чем отличается.

Audience

Определяет, к кому должен обращаться content. Каждую audience можно сохранить отдельно.

Tone

Управляет style и voice generated content, например formal, educational, simple, technical или sales-oriented.

Content goal

Определяет, чего должны достигать articles, например привлечение customers, увеличение traffic или обучение users.

Запрещенные темы

Все, о чем brand не должен говорить или с чем он не должен ассоциироваться, вводится здесь.

Optional CTA

CTA означает action, которое вы хотите, чтобы reader выполнил после прочтения article. Если вы не уверены, какой text подходит, лучше оставить это field empty.

Multilingual profile
Если ваш сайт multilingual, нужно заполнить profile только на одном language. OxiRanker может подготовить необходимые versions для других languages на основе того же profile.
ШАГ 07

Внутренние и внешние ссылки

OxiRanker может intelligent применять internal links и external links внутри articles.

Internal links

Эта option включена default, и лучше оставить ее enabled. Internal linking помогает search engines лучше понимать content structure вашего сайта.

В первой article internal links обычно не создаются, потому что еще нет previous article. Начиная со второй article, system может выполнять internal linking.

External links

External links добавляются на official и trusted sources и помечаются как nofollow, чтобы SEO authority не передавался unintentionally.

ШАГ 08

Проверить profile quality с помощью AI

После заполнения Article Profile OxiRanker проверяет profile quality.

85+
Minimum score, необходимый для save
95
Excellent score, обычно достигаемый после improvements
AI
Professional suggestions для каждого field
Если score ниже 85
System не разрешает final saving и показывает explanations и improvement suggestions для каждого problematic field. Вы можете применить suggestions и снова save.
ШАГ 09

Подключить Telegram для article summaries

После успешного save profile можно 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 отправляется в Telegram channel с image и link. Это может усилить traffic paths к вашему сайту.
ШАГ 10

Настроить automatic article engine

Во вкладке Automatic article generation вы можете управлять automatic article generation engine.

Smart topic selection

System использует keywords и Article Profile для выбора suitable, unique и publishable topics.

Brand-aware generation

Tone, audience, content goal и brand restrictions соблюдаются во время article generation.

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 можно enable или disable, а image theme может быть Light или Dark.

Примечание для API websites
Automatic engine генерирует articles внутри OxiRanker. Чтобы после generation перенести каждую article на ваш custom website, нужно implement Export API, Webhook, database storage и Image Mirroring.
ШАГ 11

Создать первую article из Generate

После подготовки domain откройте Generate menu, выберите domain и нажмите New Content или New Article.

1

Language

Для каждого content request выбирается только один language.

2

Topic

Напишите понятный topic. Source URL optional.

3

Image

Enable или disable image и выберите theme Light или Dark.

4

Payment и generation

Cost списывается из wallet, и article generation начинается.

Generation time
Article и image generation обычно занимает около 2–3 минут. После завершения article visible внутри OxiRanker.
Source URL
Если Source URL введен, OxiRanker использует его, чтобы лучше понять topic, но не generate copied content. System designed для создания unique content.
ШАГ 12

Проверить Preview, SEO Report и JSON Output

После article generation доступны несколько important outputs для quality review и publishing preparation.

Preview

Показывает article text и generated image. Этот section полезен для review final quality перед publishing.

SEO Report

Показывает понятный SEO report, включая SEO Score, title, meta description, canonical, keywords, schema, headings, links, images и OpenGraph.

JSON Output

Показывает full article output для technical users, включая slug, html_body, schema, image URLs, tags, category, reading time и created/updated timestamps.

Значение outputs
Эти outputs показывают, что article — это не просто plain text. Это structured, SEO-ready content, подготовленный для WordPress, CMS, API и publishing systems.
ШАГ 13

Что именно делают Export API и Webhook?

После того как article generated в OxiRanker, destination website может read ее через Export API или получить уведомление через Webhook, когда article ready.

Метод 1: Export API

Destination website может read articles из OxiRanker с Export Token при необходимости. Этот 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

Метод 2: Webhook

Когда new article created или article updated, OxiRanker notify destination website endpoint. Webhook не отправляет full article. Он отправляет только article identifier, чтобы destination website мог fetch full article из Export JSON API.

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 full article внутри Webhook. После получения domain_id и article_id он должен fetch complete article из Export JSON API и store его в своей database.
ШАГ 14

Content storage responsibility и Image Mirroring

Destination website должен store final article и images на собственной infrastructure.

Что destination website должен store

Full article text в destination website database
Main article image на destination server, storage или CDN
Open Graph image на destination server, storage или CDN
SEO, OpenGraph и Article Schema data
raw_detail_payload и raw_list_item для preserve original output

Правильный пример image path

Использовать OxiRanker image URLs напрямую и permanently неправильно. Image нужно download, а article URLs нужно replace внутренними URLs destination website.

https://oxiranker.com/uploads/articles/3_5/cover.webp
https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp
OxiRanker не является permanent hosting для destination website
OxiRanker не несет ответственности за permanently storing output content и images для destination website. OxiRanker не является user-content hosting service, и generated content, cover images, Open Graph images или output files могут не храниться в OxiRanker infrastructure более 90 days. Поэтому destination website должен store final article, images, SEO data, OpenGraph и Schema в собственной database, server, storage или CDN.
ШАГ 15

Получить Export Token и ready endpoints

Export Token и ready-to-use endpoints доступны со страницы article, вкладки Outputs и раздела API Output.

Где получить Export Token

Create первую article в OxiRanker.
Open эту article page.
Open Outputs tab.
Open API Output section.
Create token из Token management.
Full token shown только один раз.
Store token в 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 rotated в панели OxiRanker, previous token expires. New value должен immediately replace old value в destination website .env или backend environment, а destination backend должен быть restarted или redeployed.
ШАГ 16

Создать Webhook Secret и destination endpoint

Webhook Secret также создается со страницы article, вкладки Outputs и раздела API Output.

Где создается Webhook Secret?

Open article page.
Open Outputs tab.
Open API Output section.
Create secret в Webhook Secret section.
Full secret shown только один раз.
Store secret в destination website .env.

Fixed destination endpoint

OxiRanker принимает только следующий fixed path для destination Webhook. Destination website должен create этот endpoint в своем backend.

POST /api/oxiranker/webhook

https://example.com/api/oxiranker/webhook

Этот endpoint должен accept только POST, preserve raw body, verify timestamp и signature, и return successful response для webhook.test.

Active events
В current version events fixed: article.created и article.updated. Для testing connection отправляется event value webhook.test.
ШАГ 17

Required .env values для destination website

OxiRanker не имеет access к destination website files или .env. Destination website developer должен store эти values в своем 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
Maximum allowed time difference для Webhook. 300000 означает 5 minutes.

Storage values

SITE_ARTICLE_IMAGE_UPLOAD_DIR
Physical path для storing mirrored images на destination server.
SITE_ARTICLE_IMAGE_MAX_BYTES
Maximum allowed image size для download и storage.
PUBLIC_BASE_URL
Public destination website URL, используемый для create final image и article URLs.
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 или full output каждой article.

Получить list of articles для domain

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 articles определенного language, например fa, en или tr.

Получить full article как JSON

GET https://oxiranker.com/api/v1/users/domains/:domainId/articles/:articleId/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/json

Это самый important output для destination website. Destination website должен store full article JSON в своей database и read из своей database для 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 подходит для websites, которым нужен ready output, но более professional approach — store JSON и render его собственным template destination website.

Получить 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 должен verify Webhook signature с использованием original raw body.

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
Не сравнивайте signatures через expected === received. В Node.js лучше использовать crypto.timingSafeEqual.
ШАГ 20

Simple Node.js code sample для receiving Webhook

Этот 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 должен store articles в своей database и prevent duplicates с помощью unique constraint.

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

Предотвращать duplicates с UPSERT

Article не должна inserted заново для каждого Webhook. Если она уже exists, ее нужно updated.

Main key для preventing duplicates
Самый important constraint для preventing duplicates: 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

Fetch full article и sync после Webhook

Когда Webhook приходит, destination website должен fetch full article из Export API, 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 URLs внутри html_body и article_schema.

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

Daily sync для reliability

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

Start offset с 0.
Используйте limit 20 или 50.
Fetch article list из Export List API.
Для каждого item fetch Export JSON этой article.
Mirror images.
Store article через UPSERT.
Increase offset и continue, пока total не будет completed.
ШАГ 26

Отображать articles на destination website

После storing articles в site_articles, destination website не должен connect к OxiRanker для каждого public display. Он должен read из своей database.

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 можно использовать pattern /{lang}/blog/{slug}. Пример: /fa/blog/smart-internal-links
ШАГ 27

SEO, Schema, RTL и HTML rendering

Destination website должен read SEO data из своей database и render их на article page.

SEO fields

title для page title
meta_description для meta description
effective_canonical_url для canonical
og_title и og_description для Open Graph
og_image_url или cover_image_url для social preview image
article_schema для JSON-LD

HTML rendering

<div
  className="article-content"
  dangerouslySetInnerHTML={{ __html: article.html_body }}
/>

Если destination website имеет strict security policy, он может sanitize HTML перед saving или перед displaying.

RTL и LTR

Каждая article имеет fields lang, dir и is_rtl. Если 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.
Значение lang в query parameter было отправлено incorrectly.

Webhook errors

Webhook URL должен использовать HTTPS.
Webhook URL должен начинаться с https://.
Webhook signature invalid.
Webhook Secret wrong, secret был rotated, raw body changed или signature рассчитана incorrectly.
Webhook timestamp expired или invalid.
Timestamp old, находится в future или invalid. Проверьте server clock и NTP.
Webhook test delivery failed.
Destination website не вернул successful status 200–299, timed out или не handle webhook.test correctly.
ШАГ 29

Final API и Webhook testing checklist

После implementation эти tests verify connection от beginning до end.

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

Если вы call destination Webhook через Postman без signature, вы должны receive security error. Это означает, что endpoint active и checks security.

{
  "error": "missing_webhook_signature",
  "message": "Webhook signature header is required."
}

Затем нажмите Test Webhook внутри OxiRanker. Если receiver supports webhook.test, response_status должен быть 200.

После rotation
Если Webhook Secret или Export Token rotated, немедленно copy new value, replace его в destination website .env и restart или redeploy destination backend.
ШАГ 30

Important rules для correct implementation

Эти rules нужно соблюдать в API integration, чтобы 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 должны stored через UPSERT, не через simple insert.
Для prevent duplicates требуется unique constraint.
Images должны downloaded из OxiRanker и stored на destination website.
Destination website не должен permanently depend на OxiRanker image URLs.
Image URLs должны replaced внутри html_body и article_schema.
Если Webhook fails, daily sync должен recover missed article.
Errors, shown users или logs, должны быть English и understandable.
Если article archived, automatic sync не должен publish ее снова.
raw_detail_payload и raw_list_item должны preserved, чтобы original output data не потерялись.
Если Export Token или Webhook Secret rotated, new value должен replaced в .env, а backend restarted или redeployed.
ШАГ 31

Complete API flow summary

После завершения этих steps ваш custom website может securely receive, store, mirror и publish articles, generated в OxiRanker.

Зарегистрироваться в OxiRanker
Добавить domain
Verify domain ownership
Выбрать languages и добавить Blog URL
Добавить keywords для каждого language
Заполнить Article Profile
Настроить Telegram при необходимости
Настроить automatic article engine
Create первую article из Generate
Review Preview и SEO Report
Получить Export Token из API Output
Получить ready Export API endpoints
Получить Webhook Secret
Create fixed /api/oxiranker/webhook endpoint в destination backend
Store Token и Secret в destination website .env
Create site_articles table и unique constraint
Implement full article fetching из Export JSON
Implement Image Mirroring
Implement UPSERT
Implement daily sync
Render article, SEO, OG и Schema из destination database
Run final Export API и Webhook tests