Документация OxiRanker
Понятный центр документации для подключения вашего сайта к сервисам OxiRanker. Начните с интеграции WordPress или API-интеграции, затем следуйте руководству шаг за шагом.
API-сервис
Используйте Export API и Webhook для пользовательских сайтов, custom CMS платформ, Next.js, Laravel, Node.js, PHP или Python backend.
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-интеграции.
Реализуйте output flow шаг за шагом
Сначала сохраните Export Token и Webhook Secret в своем backend, затем завершите Webhook receiver, database, UPSERT logic, Image Mirroring и daily sync.
Зарегистрироваться и создать 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 принадлежит вам или что у вас есть access для его управления.
DNS TXT
РекомендуетсяДобавьте TXT record в DNS domain. Для Cloudflare это обычно самый простой метод.
DNS Name:
_yourdomain-verification.example.com
TXT Value:
acb1****29803HTTP File
Разместите text file в path .well-known вашего сайта, чтобы OxiRanker мог его проверить.
File URL:
https://example.com/.well-known/yourdomain-verification.txt
File Content:
220daaa37****42730f27Meta Tag
Разместите meta tag внутри head section homepage вашего сайта.
<meta name="yourdomain-verification" content="190208d****d275b65" />Получить бесплатные token после подтверждения первого domain
После подтверждения первого domain OxiRanker добавляет 1000 free tokens в ваш account для initial testing.
Настроить языки и 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Добавить keywords для каждого языка
Во вкладке Keywords добавьте минимум 3 и максимум 50 keywords для каждого языка.
Главное правило
Keywords для каждого языка должны быть введены на том же языке. Если article language — English, keywords также должны быть English.
Как добавить keywords
После ввода каждого keyword нажмите Enter, чтобы добавить его. После завершения всех keywords нажмите Save.
Заполнить 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.
Внутренние и внешние ссылки
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.
Проверить profile quality с помощью AI
После заполнения Article Profile OxiRanker проверяет profile quality.
Подключить Telegram для article summaries
После успешного save profile можно enable отправку article summaries в Telegram channel.
Добавить bot
Добавьте @Oxiranker_Bot в свой Telegram channel и сделайте его Admin.
Введите channel username
Введите channel name с @. Пример: @my_channel
Enable summary delivery
После регистрации valid channel enable article summary delivery.
Настроить automatic article engine
Во вкладке Automatic article generation вы можете управлять 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 / month30 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.
Создать первую article из Generate
После подготовки domain откройте Generate menu, выберите domain и нажмите New Content или New Article.
Language
Для каждого content request выбирается только один language.
Topic
Напишите понятный topic. Source URL optional.
Image
Enable или disable image и выберите theme Light или Dark.
Payment и generation
Cost списывается из wallet, и article generation начинается.
Проверить 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.
Что именно делают 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/jsonGET 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"
}Content storage responsibility и Image Mirroring
Destination website должен store final article и images на собственной infrastructure.
Что destination website должен store
Правильный пример image path
Использовать OxiRanker image URLs напрямую и permanently неправильно. Image нужно download, а article URLs нужно replace внутренними URLs destination website.
https://oxiranker.com/uploads/articles/3_5/cover.webphttps://example.com/uploads/site-articles/oxiranker/1/5/cover.webpПолучить Export Token и ready endpoints
Export Token и ready-to-use endpoints доступны со страницы article, вкладки Outputs и раздела API Output.
Где получить Export Token
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}Ready-to-use endpoints
На той же article page, внутри API Output section, Ready-to-use endpoints показывает prepared URLs.
https://oxiranker.com/api/v1/users/domains/1/export/articles?limit=20&offset=0https://oxiranker.com/api/v1/users/domains/1/articles/1/export/jsonhttps://oxiranker.com/api/v1/users/domains/1/articles/1/export/html?canonical_mode=absolute&include_css=0Создать Webhook Secret и destination endpoint
Webhook Secret также создается со страницы article, вкладки Outputs и раздела API Output.
Где создается Webhook Secret?
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.
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=50Security values
Storage values
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/jsonlimit определяет 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_TOKENHTML подходит для 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_TOKENZIP output включает article.json, meta.json, assets.json, schema.jsonld, index.html, version.txt и README.txt.
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.
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,
});
});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);Предотвращать duplicates с UPSERT
Article не должна inserted заново для каждого Webhook. Если она уже exists, ее нужно updated.
INSERT INTO public.site_articles (
source_provider,
source_domain_id,
source_article_id,
source_request_id,
lang,
dir,
slug,
title,
meta_description,
html_body,
focus_keywords,
category,
tags,
reading_time_minutes,
cover_image_url,
og_image_url,
article_schema,
source_created_at,
source_updated_at,
sync_status,
last_synced_at,
publish_status,
published_at,
created_at,
updated_at
)
VALUES (
'oxiranker',
1,
5,
5,
'fa',
'rtl',
'smart-internal-links',
'Article title',
'Meta description',
'<p>HTML body</p>',
'[]'::jsonb,
'SEO',
'[]'::jsonb,
7,
'https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp',
'https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp',
'{}'::jsonb,
'2026-05-20T19:33:23.618Z',
'2026-05-20T19:34:09.629Z',
'synced',
now(),
'published',
now(),
now(),
now()
)
ON CONFLICT (source_provider, source_domain_id, source_article_id)
DO UPDATE SET
source_request_id = EXCLUDED.source_request_id,
lang = EXCLUDED.lang,
dir = EXCLUDED.dir,
slug = EXCLUDED.slug,
title = EXCLUDED.title,
meta_description = EXCLUDED.meta_description,
html_body = EXCLUDED.html_body,
focus_keywords = EXCLUDED.focus_keywords,
category = EXCLUDED.category,
tags = EXCLUDED.tags,
reading_time_minutes = EXCLUDED.reading_time_minutes,
cover_image_url = EXCLUDED.cover_image_url,
og_image_url = EXCLUDED.og_image_url,
article_schema = EXCLUDED.article_schema,
source_created_at = EXCLUDED.source_created_at,
source_updated_at = EXCLUDED.source_updated_at,
sync_status = 'synced',
last_synced_at = now(),
last_sync_error = NULL,
publish_status = CASE
WHEN public.site_articles.publish_status = 'archived'
THEN public.site_articles.publish_status
ELSE 'published'
END,
published_at = COALESCE(public.site_articles.published_at, EXCLUDED.published_at, now()),
deleted_at = NULL,
updated_at = now();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,
});
}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.webpDaily sync для reliability
Webhook alone недостаточно. Destination website также должен иметь daily sync для recover missed articles.
Почему daily sync нужен?
Daily sync algorithm
Отображать 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;SEO, Schema, RTL и HTML rendering
Destination website должен read SEO data из своей database и render их на article page.
SEO fields
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>Common errors и их значения
Destination website errors должны быть English, clear и understandable, чтобы troubleshooting выполнялся быстро.
Export API errors
Webhook errors
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/jsonGET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonWebhook 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.
Important rules для correct implementation
Эти rules нужно соблюдать в API integration, чтобы system оставалась secure, stable и maintainable.
Complete API flow summary
После завершения этих steps ваш custom website может securely receive, store, mirror и publish articles, generated в OxiRanker.