توثيق OxiRanker
مركز توثيق واضح لربط موقعك بخدمات OxiRanker. ابدأ بتكامل WordPress أو تكامل API، ثم اتبع الدليل خطوة بخطوة.
خدمة API
استخدم Export API و Webhook للمواقع المخصصة ومنصات CMS المخصصة و Next.js و Laravel و Node.js و PHP أو Python backends.
OxiRanker ليس استضافة دائمة للموقع الوجهة
لا يقدّم OxiRanker ضمان استضافة دائمة لمحتوى الموقع الوجهة أو صوره أو ملفاته المصدّرة. OxiRanker ليس خدمة استضافة لمحتوى المستخدمين، وقد لا تبقى المحتويات المُنشأة أو صور الغلاف أو صور Open Graph أو ملفات الإخراج على بنية OxiRanker لأكثر من 90 يوماً.
لذلك، بعد استلام الإخراج، يجب على الموقع الوجهة تخزين المقال النهائي والصور وبيانات السيو وبيانات Open Graph وبيانات Schema داخل قاعدة بياناته أو خادمه أو التخزين أو CDN الخاص به، ويجب ألا يعتمد على روابط OxiRanker للعرض العام.
فيديو تدريب الاتصال
استخدم هذا الفيديو كشرح بصري قبل ربط WordPress أو تنفيذ تكامل API.
نفّذ مسار الإخراج خطوة بخطوة
خزّن Export Token و Webhook Secret في backend أولاً، ثم أكمل Webhook receiver وقاعدة البيانات ومنطق UPSERT و Image Mirroring والمزامنة اليومية.
التسجيل وإنشاء نطاق في OxiRanker
أولاً، سجّل في OxiRanker، ثم ادخل إلى حسابك واضغط Add Domain من قسم Domains.
صيغة النطاق الصحيحة
في حقل 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 إلى حسابك 1000 توكن مجاني للاختبار الأولي.
إعداد اللغات ورابط المدونة
في قسم Blog URLs، حدّد ما إذا كان موقعك بلغة واحدة أو متعدد اللغات، وما اللغات التي يجب استخدامها للمقالات المُنشأة.
اختيار اللغات
يدعم OxiRanker 24 لغة. يمكنك اختيار لغة واحدة أو عدة لغات بناءً على بنية موقعك.
إدخال رابط المدونة
لكل لغة، أدخل رابط المدونة الخاص بتلك اللغة. إذا كان موقعك متعدد اللغات، فأدخل مسار كل لغة بشكل منفصل.
https://example.com/fa/blog
https://example.com/en/blogإضافة الكلمات المفتاحية لكل لغة
في تبويب Keywords، أضف 3 كلمات مفتاحية على الأقل وحتى 50 كلمة لكل لغة.
القاعدة الأساسية
يجب إدخال الكلمات المفتاحية لكل لغة بنفس اللغة. إذا كانت لغة المقال الإنجليزية، فيجب أن تكون الكلمات المفتاحية أيضاً بالإنجليزية.
كيفية إضافة الكلمات المفتاحية
بعد كتابة كل كلمة مفتاحية، اضغط 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 / month30 مقالاً تعني تقريباً مقالاً واحداً يومياً. 15 مقالاً تعني تقريباً يوماً بعد يوم. 10 مقالات تعني تقريباً كل يومين إلى ثلاثة أيام. 7 مقالات تعني تقريباً كل ثلاثة إلى أربعة أيام.
طول المقال والصورة
يمكن أن يكون طول المقال Short أو Standard أو Long. خيار Standard مناسب ومتوازن لمعظم المواقع.
يمكن تفعيل صور المقال أو تعطيلها، ويمكن اختيار ثيم الصورة Light أو Dark.
إنشاء أول مقال من Generate
بعد تجهيز النطاق، افتح قائمة Generate، واختر النطاق، ثم اضغط New Content أو New Article.
اللغة
يتم اختيار لغة واحدة فقط لكل طلب محتوى.
الموضوع
اكتب موضوعاً واضحاً. Source URL اختياري.
الصورة
فعّل الصورة أو عطّلها واختر ثيم Light أو Dark.
الدفع والإنشاء
يتم خصم التكلفة من المحفظة ويبدأ إنشاء المقال.
مراجعة Preview و SEO Report و JSON Output
بعد إنشاء المقال، تتوفر عدة مخرجات مهمة لمراجعة الجودة والتحضير للنشر.
Preview
يعرض نص المقال والصورة المُنشأة. هذا القسم مفيد لمراجعة الجودة النهائية قبل النشر.
SEO Report
يعرض تقرير سيو واضحاً يشمل SEO Score والعنوان و meta description و canonical والكلمات المفتاحية و schema والعناوين والروابط والصور و OpenGraph.
JSON Output
يعرض الإخراج الكامل للمقال للمستخدمين التقنيين، بما في ذلك slug و html_body و schema وروابط الصور والوسوم والتصنيف ووقت القراءة وتواريخ الإنشاء/التحديث.
ماذا يفعل Export API و Webhook بالضبط؟
بعد إنشاء مقال في OxiRanker، يمكن للموقع الوجهة قراءته باستخدام Export API أو تلقي إشعار عبر Webhook عندما يكون المقال جاهزاً.
الطريقة 1: Export API
يمكن للموقع الوجهة قراءة المقالات من OxiRanker باستخدام Export Token عند الحاجة. تُستخدم هذه الطريقة للمزامنة اليدوية والمزامنة اليومية وصفحات قائمة المقالات وجلب 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
عند إنشاء مقال جديد أو تحديث مقال، يُرسل 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
يجب على الموقع الوجهة تخزين المقال النهائي والصور على بنيته التحتية الخاصة.
ما الذي يجب على الموقع الوجهة تخزينه
مثال صحيح لمسار الصورة
استخدام روابط صور OxiRanker مباشرة وبشكل دائم ليس صحيحاً. يجب تنزيل الصورة واستبدال روابط المقال بروابط داخلية خاصة بالموقع الوجهة.
https://oxiranker.com/uploads/articles/3_5/cover.webphttps://example.com/uploads/site-articles/oxiranker/1/5/cover.webpالحصول على Export Token و endpoints جاهزة
يتوفر Export Token و endpoints الجاهزة للاستخدام من صفحة المقال، تبويب Outputs وقسم API Output.
أين تحصل على Export Token
https://oxiranker.com/en/pannel/generate/{domainId}/articles/{articleId}Endpoints جاهزة للاستخدام
في نفس صفحة المقال، داخل قسم 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/jsonيحدد limit عدد المقالات وهو حالياً محدود بين 1 و 50. يُستخدم 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 وعرضه باستخدام قالب الموقع الوجهة الخاص.
الحصول على مخرج 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 الأصلي.
Headers الخاصة بالـ 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 أولاً ثم أعاد تحويله إلى string، فقد يتغير الترتيب أو المسافات ويصبح التوقيع غير صالح. يجب استخدام 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 INTO public.site_articles (
source_provider,
source_domain_id,
source_article_id,
source_request_id,
lang,
dir,
slug,
title,
meta_description,
html_body,
focus_keywords,
category,
tags,
reading_time_minutes,
cover_image_url,
og_image_url,
article_schema,
source_created_at,
source_updated_at,
sync_status,
last_synced_at,
publish_status,
published_at,
created_at,
updated_at
)
VALUES (
'oxiranker',
1,
5,
5,
'fa',
'rtl',
'smart-internal-links',
'Article title',
'Meta description',
'<p>HTML body</p>',
'[]'::jsonb,
'SEO',
'[]'::jsonb,
7,
'https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp',
'https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp',
'{}'::jsonb,
'2026-05-20T19:33:23.618Z',
'2026-05-20T19:34:09.629Z',
'synced',
now(),
'published',
now(),
now(),
now()
)
ON CONFLICT (source_provider, source_domain_id, source_article_id)
DO UPDATE SET
source_request_id = EXCLUDED.source_request_id,
lang = EXCLUDED.lang,
dir = EXCLUDED.dir,
slug = EXCLUDED.slug,
title = EXCLUDED.title,
meta_description = EXCLUDED.meta_description,
html_body = EXCLUDED.html_body,
focus_keywords = EXCLUDED.focus_keywords,
category = EXCLUDED.category,
tags = EXCLUDED.tags,
reading_time_minutes = EXCLUDED.reading_time_minutes,
cover_image_url = EXCLUDED.cover_image_url,
og_image_url = EXCLUDED.og_image_url,
article_schema = EXCLUDED.article_schema,
source_created_at = EXCLUDED.source_created_at,
source_updated_at = EXCLUDED.source_updated_at,
sync_status = 'synced',
last_synced_at = now(),
last_sync_error = NULL,
publish_status = CASE
WHEN public.site_articles.publish_status = 'archived'
THEN public.site_articles.publish_status
ELSE 'published'
END,
published_at = COALESCE(public.site_articles.published_at, EXCLUDED.published_at, now()),
deleted_at = NULL,
updated_at = now();جلب المقال الكامل والمزامنة بعد Webhook
عند وصول Webhook، يجب على الموقع الوجهة جلب المقال الكامل من Export API ونسخ الصور محلياً ثم تنفيذ 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 وتخزين الصور
يجب على الموقع الوجهة تنزيل الصور وتخزينها على بنيته التحتية الخاصة واستبدال روابط المقال.
قواعد 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.webpالمزامنة اليومية للموثوقية
Webhook وحده ليس كافياً. يجب على الموقع الوجهة أيضاً تنفيذ مزامنة يومية لاستعادة المقالات الفائتة.
لماذا نحتاج إلى مزامنة يومية؟
خوارزمية المزامنة اليومية
عرض المقالات على الموقع الوجهة
بعد تخزين المقالات في 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;السيو و Schema و RTL وعرض HTML
يجب على الموقع الوجهة قراءة بيانات السيو من قاعدة بياناته الخاصة وعرضها في صفحة المقال.
حقول السيو
عرض HTML
<div
className="article-content"
dangerouslySetInnerHTML={{ __html: article.html_body }}
/>إذا كان الموقع الوجهة لديه سياسة أمان صارمة، فيمكنه تنظيف HTML قبل الحفظ أو قبل العرض.
RTL و LTR
كل مقال يحتوي على حقول lang و dir و is_rtl. إذا كان dir يساوي rtl، فيجب عرض صفحة المقال من اليمين إلى اليسار.
<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."
}ثم اضغط Test Webhook داخل OxiRanker. إذا كان المستقبل يدعم webhook.test، فيجب أن تكون response_status تساوي 200.
قواعد مهمة للتنفيذ الصحيح
يجب اتباع هذه القواعد في تكامل API حتى يبقى النظام آمناً ومستقراً وقابلاً للصيانة.
ملخص مسار API الكامل
بعد إكمال هذه الخطوات، يستطيع موقعك المخصص استقبال المقالات التي ينشئها OxiRanker وتخزينها ونسخ صورها ونشرها بأمان.