مستندات OxiRanker
یک مرکز مستندات تمیز برای اتصال وبسایت شما به سرویسهای OxiRanker. با یکپارچهسازی WordPress یا یکپارچهسازی API شروع کنید و سپس راهنما را مرحلهبهمرحله دنبال کنید.
سرویس API
از Export API و Webhook برای وبسایتهای سفارشی، پلتفرمهای CMS سفارشی، Next.js، Laravel، Node.js، PHP یا backendهای Python استفاده کنید.
OxiRanker هاست دائمی برای وبسایت مقصد نیست
OxiRanker هیچ ضمانتی برای میزبانی دائمی محتوای وبسایت مقصد، تصاویر یا فایلهای خروجی ارائه نمیدهد. OxiRanker سرویس میزبانی محتوای کاربران نیست و محتوای تولیدشده، تصاویر کاور، تصاویر Open Graph یا فایلهای خروجی ممکن است بیش از ۹۰ روز روی زیرساخت OxiRanker باقی نمانند.
بنابراین، بعد از دریافت خروجی، وبسایت مقصد باید مقاله نهایی، تصاویر، دادههای SEO، دادههای Open Graph و دادههای Schema را در دیتابیس، سرور، فضای ذخیرهسازی یا CDN خودش ذخیره کند و برای نمایش عمومی به URLهای OxiRanker وابسته نباشد.
ویدیوی آموزش اتصال
قبل از اتصال WordPress یا پیادهسازی یکپارچهسازی API، از این ویدیو بهعنوان راهنمای تصویری استفاده کنید.
جریان خروجی را مرحلهبهمرحله پیادهسازی کنید
ابتدا Export Token و Webhook Secret را در backend خود ذخیره کنید، سپس Webhook receiver، دیتابیس، منطق UPSERT، Image Mirroring و sync روزانه را کامل کنید.
ثبتنام و ایجاد دامنه در OxiRanker
ابتدا در OxiRanker ثبتنام کنید، وارد حساب خود شوید و از بخش Domains روی Add Domain کلیک کنید.
فرمت صحیح دامنه
در فیلد Domain name فقط دامنه اصلی را وارد کنید. آدرس نباید شامل http، https یا www باشد.
example.comفرمتهای اشتباه
این فرمتها را در فیلد دامنه وارد نکنید.
https://example.com
http://example.com
www.example.comتأیید مالکیت دامنه
در این مرحله، باید ثابت کنید دامنه متعلق به شماست یا به مدیریت آن دسترسی دارید.
DNS TXT
پیشنهادییک TXT record به DNS دامنه اضافه کنید. برای Cloudflare، این معمولاً سادهترین روش است.
DNS Name:
_yourdomain-verification.example.com
TXT Value:
acb1****29803HTTP File
یک فایل متنی را در مسیر .well-known وبسایت خود قرار دهید تا OxiRanker بتواند آن را تأیید کند.
File URL:
https://example.com/.well-known/yourdomain-verification.txt
File Content:
220daaa37****42730f27Meta Tag
یک meta tag را داخل بخش head صفحه اصلی وبسایت خود قرار دهید.
<meta name="yourdomain-verification" content="190208d****d275b65" />دریافت توکنهای رایگان بعد از تأیید اولین دامنه
بعد از تأیید اولین دامنه، OxiRanker برای تست اولیه ۱۰۰۰ توکن رایگان به حساب شما اضافه میکند.
تنظیم زبانها و Blog URL
در بخش Blog URLs مشخص کنید که وبسایت شما تکزبانه است یا چندزبانه، و کدام زبانها باید برای مقالههای تولیدشده استفاده شوند.
انتخاب زبانها
OxiRanker از ۲۴ زبان پشتیبانی میکند. بر اساس ساختار وبسایت خود میتوانید یک یا چند زبان را انتخاب کنید.
وارد کردن Blog URL
برای هر زبان، Blog URL همان زبان را وارد کنید. اگر وبسایت شما چندزبانه است، مسیر هر زبان را جداگانه وارد کنید.
https://example.com/fa/blog
https://example.com/en/blogافزودن کلمات کلیدی برای هر زبان
در تب Keywords، برای هر زبان حداقل ۳ و حداکثر ۵۰ کلمه کلیدی اضافه کنید.
قانون اصلی
کلمات کلیدی هر زبان باید به همان زبان وارد شوند. اگر زبان مقاله انگلیسی است، کلمات کلیدی هم باید انگلیسی باشند.
نحوه افزودن کلمات کلیدی
بعد از تایپ هر کلمه کلیدی، برای افزودن آن Enter را فشار دهید. بعد از تکمیل همه کلمات کلیدی، روی Save کلیک کنید.
تکمیل Article Profile دامنه
Article Profile به OxiRanker میگوید برند شما چیست، برای چه کسی مینویسد، چه لحنی باید استفاده کند و از چه موضوعاتی باید دوری کند.
صنعت
بازار یا دستهبندی کسبوکار را تعریف میکند. مثال: مشاوره املاک و خرید و فروش ملک در دبی.
توضیح کسبوکار
توضیح میدهد کسبوکار چه چیزی میفروشد، به چه کسانی خدمت میکند، چگونه کار میکند و چه چیزی آن را متفاوت میکند.
مخاطب
تعریف میکند محتوا باید با چه کسانی صحبت کند. هر مخاطب میتواند جداگانه ذخیره شود.
لحن
سبک و صدای محتوای تولیدشده را کنترل میکند، مانند رسمی، آموزشی، ساده، فنی یا فروشمحور.
هدف محتوا
تعریف میکند مقالهها باید چه چیزی را محقق کنند، مانند جذب مشتری، افزایش ترافیک یا آموزش کاربران.
موضوعات ممنوع
هر چیزی که برند نباید درباره آن صحبت کند یا نباید با آن مرتبط شود، در اینجا وارد میشود.
CTA اختیاری
CTA یعنی اقدامی که میخواهید خواننده بعد از خواندن مقاله انجام دهد. اگر مطمئن نیستید چه متنی مناسب است، بهتر است این فیلد را خالی بگذارید.
لینکسازی داخلی و خارجی
OxiRanker میتواند لینکهای داخلی و خارجی را بهصورت هوشمند داخل مقالهها اعمال کند.
لینکهای داخلی
این گزینه بهصورت پیشفرض فعال است و بهتر است فعال بماند. لینکسازی داخلی به موتورهای جستجو کمک میکند ساختار محتوای وبسایت شما را بهتر درک کنند.
در مقاله اول، معمولاً لینک داخلی ایجاد نمیشود چون هنوز مقاله قبلی وجود ندارد. از مقاله دوم به بعد، سیستم میتواند لینکسازی داخلی انجام دهد.
لینکهای خارجی
لینکهای خارجی به منابع رسمی و معتبر اضافه میشوند و بهصورت nofollow علامتگذاری میشوند تا اعتبار سئو بهصورت ناخواسته منتقل نشود.
بررسی کیفیت پروفایل با AI
بعد از تکمیل Article Profile، OxiRanker کیفیت پروفایل را بررسی میکند.
اتصال Telegram برای خلاصه مقالهها
بعد از ذخیره موفق پروفایل، میتوانید ارسال خلاصه مقالهها به کانال Telegram را فعال کنید.
افزودن ربات
@Oxiranker_Bot را به کانال Telegram خود اضافه کنید و آن را Admin کنید.
وارد کردن نام کاربری کانال
نام کانال را با @ وارد کنید. مثال: @my_channel
فعالسازی ارسال خلاصه
بعد از ثبت یک کانال معتبر، ارسال خلاصه مقاله را فعال کنید.
تنظیم موتور خودکار مقاله
در تب Automatic article generation، میتوانید موتور تولید خودکار مقاله را مدیریت کنید.
انتخاب هوشمند موضوع
سیستم از کلمات کلیدی و Article Profile برای انتخاب موضوعات مناسب، منحصربهفرد و قابل انتشار استفاده میکند.
تولید آگاه از برند
لحن، مخاطب، هدف محتوا و محدودیتهای برند هنگام تولید مقاله رعایت میشوند.
انتشار زمانبندیشده
ظرفیت ماهانه در طول ماه توزیع میشود تا انتشار محتوا طبیعیتر و منظمتر به نظر برسد.
ظرفیت ماهانه
7 articles / month
10 articles / month
15 articles / month
30 articles / month۳۰ مقاله یعنی تقریباً روزی یک مقاله. ۱۵ مقاله یعنی تقریباً یک روز در میان. ۱۰ مقاله یعنی حدود هر دو تا سه روز. ۷ مقاله یعنی حدود هر سه تا چهار روز.
طول مقاله و تصویر
طول مقاله میتواند Short، Standard یا Long باشد. Standard برای بیشتر وبسایتها انتخابی متعادل است.
تصاویر مقاله میتوانند فعال یا غیرفعال شوند و تم تصویر میتواند Light یا Dark باشد.
ایجاد اولین مقاله از Generate
بعد از آمادهسازی دامنه، منوی Generate را باز کنید، دامنه را انتخاب کنید و روی New Content یا New Article کلیک کنید.
زبان
برای هر درخواست محتوا فقط یک زبان انتخاب میشود.
موضوع
یک موضوع واضح بنویسید. Source URL اختیاری است.
تصویر
تصویر را فعال یا غیرفعال کنید و تم Light یا Dark را انتخاب کنید.
پرداخت و تولید
هزینه از wallet کسر میشود و تولید مقاله شروع میشود.
بررسی Preview، SEO Report و JSON Output
بعد از تولید مقاله، چند خروجی مهم برای بررسی کیفیت و آمادهسازی انتشار در دسترس است.
Preview
متن مقاله و تصویر تولیدشده را نمایش میدهد. این بخش برای بررسی کیفیت نهایی قبل از انتشار مفید است.
SEO Report
یک گزارش قابلفهم سئو شامل SEO Score، title، meta description، canonical، keywords، schema، headings، links، images و OpenGraph را نمایش میدهد.
JSON Output
خروجی کامل مقاله را برای کاربران فنی نمایش میدهد، شامل slug، html_body، schema، URLهای تصویر، tags، category، زمان مطالعه و زمانهای ایجاد/بهروزرسانی.
Export API و Webhook دقیقاً چه کاری انجام میدهند؟
بعد از تولید یک مقاله در OxiRanker، وبسایت مقصد میتواند آن را با Export API بخواند یا وقتی مقاله آماده شد از طریق Webhook مطلع شود.
روش ۱: Export API
وبسایت مقصد میتواند هر زمان لازم بود مقالهها را با Export Token از OxiRanker بخواند. این روش برای sync دستی، sync روزانه، صفحات لیست مقالهها و دریافت 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روش ۲: Webhook
وقتی یک مقاله جدید ایجاد میشود یا یک مقاله بهروزرسانی میشود، OxiRanker endpoint وبسایت مقصد را مطلع میکند. Webhook مقاله کامل را ارسال نمیکند. فقط شناسه مقاله را ارسال میکند تا وبسایت مقصد بتواند مقاله کامل را از 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"
}مسئولیت ذخیرهسازی محتوا و Image Mirroring
وبسایت مقصد باید مقاله نهایی و تصاویر را روی زیرساخت خودش ذخیره کند.
وبسایت مقصد چه چیزهایی را باید ذخیره کند
مثال صحیح مسیر تصویر
استفاده مستقیم و دائمی از URLهای تصاویر OxiRanker درست نیست. تصویر باید دانلود شود و URLهای مقاله باید با URLهای داخلی وبسایت مقصد جایگزین شوند.
https://oxiranker.com/uploads/articles/3_5/cover.webphttps://example.com/uploads/site-articles/oxiranker/1/5/cover.webpدریافت Export Token و endpointهای آماده
Export Token و endpointهای آماده استفاده از صفحه مقاله، تب Outputs و بخش API Output در دسترس هستند.
Export Token را از کجا بگیریم
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}Endpointهای آماده استفاده
در همان صفحه مقاله، داخل بخش API Output، قسمت Ready-to-use endpoints آدرسهای آماده را نمایش میدهد.
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 و endpoint مقصد
Webhook Secret نیز از صفحه مقاله، تب Outputs و بخش API Output ایجاد میشود.
Webhook Secret کجا ایجاد میشود؟
Endpoint ثابت مقصد
OxiRanker فقط مسیر ثابت زیر را برای Webhook مقصد میپذیرد. وبسایت مقصد باید این endpoint را در backend خودش ایجاد کند.
POST /api/oxiranker/webhook
https://example.com/api/oxiranker/webhookاین endpoint باید فقط POST را بپذیرد، raw body را حفظ کند، timestamp و signature را بررسی کند و برای webhook.test پاسخ موفق برگرداند.
مقادیر .env موردنیاز برای وبسایت مقصد
OxiRanker به فایلها یا .env وبسایت مقصد دسترسی ندارد. توسعهدهنده وبسایت مقصد باید این مقادیر را در 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مقادیر امنیتی
مقادیر ذخیرهسازی
Export API: List، JSON، HTML و ZIP
وبسایت مقصد میتواند از Export API برای دریافت لیست مقالهها یا خروجی کامل هر مقاله استفاده کند.
دریافت لیست مقالهها برای یک دامنه
GET https://oxiranker.com/api/v1/users/domains/:domainId/export/articles?limit=20&offset=0
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonlimit تعداد مقالهها را مشخص میکند و در حال حاضر بین ۱ تا ۵۰ محدود است. offset برای صفحهبندی استفاده میشود. lang اختیاری است و برای دریافت مقالههای یک زبان خاص مانند fa، en یا tr استفاده میشود.
دریافت مقاله کامل بهصورت JSON
GET https://oxiranker.com/api/v1/users/domains/:domainId/articles/:articleId/export/json
Authorization: Bearer YOUR_EXPORT_TOKEN
Accept: application/jsonاین مهمترین خروجی برای وبسایت مقصد است. وبسایت مقصد باید JSON کامل مقاله را در دیتابیس خودش ذخیره کند و برای نمایش عمومی از دیتابیس خودش بخواند.
دریافت 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 برای وبسایتهایی مناسب است که خروجی آماده میخواهند، اما روش حرفهایتر این است که JSON ذخیره شود و با قالب خود وبسایت مقصد render شود.
دریافت خروجی ZIP
GET https://oxiranker.com/api/v1/users/domains/1/articles/5/export/zip
Authorization: Bearer YOUR_EXPORT_TOKENخروجی ZIP شامل article.json، meta.json، assets.json، schema.jsonld، index.html، version.txt و README.txt است.
امضای Webhook و دریافت امن Webhook
وبسایت مقصد باید امضای Webhook را با استفاده از raw body اصلی بررسی کند.
Headerهای Webhook
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=...فرمول امضا
payload_to_sign = timestamp + "." + rawBody
signature = "sha256=" + HMAC_SHA256(webhook_secret, payload_to_sign)اگر وبسایت مقصد ابتدا JSON را parse کند و سپس دوباره آن را stringify کند، ترتیب یا فاصلهها ممکن است تغییر کنند و امضا نامعتبر شود. برای بررسی امضا باید از raw body اصلی استفاده شود.
نمونه کد ساده Node.js برای دریافت Webhook
این نمونه Express با raw body، timestamp، signature، webhook.test و رویدادهای مقاله کار میکند.
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,
});
});دیتابیس پیشنهادی وبسایت مقصد
وبسایت مقصد باید مقالهها را در دیتابیس خودش ذخیره کند و با 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);جلوگیری از تکرار با UPSERT
مقاله نباید برای هر Webhook دوباره insert شود. اگر از قبل وجود دارد، باید بهروزرسانی شود.
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();دریافت مقاله کامل و sync بعد از Webhook
وقتی یک Webhook میرسد، وبسایت مقصد باید مقاله کامل را از Export API دریافت کند، تصاویر را mirror کند و مقاله را UPSERT کند.
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 و قوانین ذخیرهسازی تصویر
وبسایت مقصد باید تصاویر را دانلود کند، روی زیرساخت خودش ذخیره کند و URLهای مقاله را جایگزین کند.
قوانین Image Mirroring
مسیر ذخیرهسازی پیشنهادی
/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.webpSync روزانه برای اطمینان
Webhook بهتنهایی کافی نیست. وبسایت مقصد باید sync روزانه هم داشته باشد تا مقالههای ازدسترفته را بازیابی کند.
چرا sync روزانه لازم است؟
الگوریتم sync روزانه
نمایش مقالهها در وبسایت مقصد
بعد از ذخیره مقالهها در site_articles، وبسایت مقصد نباید برای هر نمایش عمومی به OxiRanker متصل شود. باید از دیتابیس خودش بخواند.
صفحه لیست بلاگ
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;صفحه جزئیات مقاله
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
وبسایت مقصد باید دادههای SEO را از دیتابیس خودش بخواند و آن را در صفحه مقاله render کند.
فیلدهای SEO
HTML rendering
<div
className="article-content"
dangerouslySetInnerHTML={{ __html: article.html_body }}
/>اگر وبسایت مقصد سیاست امنیتی سختگیرانه دارد، میتواند HTML را قبل از ذخیره یا قبل از نمایش sanitize کند.
RTL و LTR
هر مقاله فیلدهای lang، dir و is_rtl دارد. اگر dir برابر rtl باشد، صفحه مقاله باید راستبهچپ render شود.
<article dir="rtl">
...
</article>خطاهای رایج و معنی آنها
خطاهای وبسایت مقصد باید انگلیسی، واضح و قابلفهم باشند تا عیبیابی سریع انجام شود.
خطاهای Export API
خطاهای Webhook
چکلیست نهایی تست API و Webhook
بعد از پیادهسازی، این تستها اتصال را از ابتدا تا انتها تأیید میکنند.
تست Export Token و JSON
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/jsonتست Webhook
اگر Webhook مقصد را با Postman بدون امضا صدا بزنید، باید خطای امنیتی دریافت کنید. این یعنی endpoint فعال است و امنیت را بررسی میکند.
{
"error": "missing_webhook_signature",
"message": "Webhook signature header is required."
}سپس داخل OxiRanker روی Test Webhook کلیک کنید. اگر receiver از webhook.test پشتیبانی کند، response_status باید 200 باشد.
قوانین مهم برای پیادهسازی صحیح
این قوانین باید در یکپارچهسازی API رعایت شوند تا سیستم امن، پایدار و قابل نگهداری بماند.
خلاصه جریان کامل API
بعد از تکمیل این مراحل، وبسایت سفارشی شما میتواند مقالههای تولیدشده در OxiRanker را بهصورت امن دریافت، ذخیره، mirror و منتشر کند.