توثيق مطوري OXIRANKER

توثيق OxiRanker

مركز توثيق واضح لربط موقعك بخدمات OxiRanker. ابدأ بتكامل WordPress أو تكامل API، ثم اتبع الدليل خطوة بخطوة.

توثيق رسميالإصدار الكامل V1لمالكي المواقع والمطورين وفرق التكامل
قائمة التوثيق
اختر دليلاً من القائمة. هذه الصفحة هي مركز التوثيق الرئيسي لـ OxiRanker.
خدمة API

خدمة API

استخدم Export API و Webhook للمواقع المخصصة ومنصات CMS المخصصة و Next.js و Laravel و Node.js و PHP أو Python backends.

النطاق
تم التحقق
المحتوى
جاهز
API
Webhook
تنبيه مهم حول التخزين

OxiRanker ليس استضافة دائمة للموقع الوجهة

لا يقدّم OxiRanker ضمان استضافة دائمة لمحتوى الموقع الوجهة أو صوره أو ملفاته المصدّرة. OxiRanker ليس خدمة استضافة لمحتوى المستخدمين، وقد لا تبقى المحتويات المُنشأة أو صور الغلاف أو صور Open Graph أو ملفات الإخراج على بنية OxiRanker لأكثر من 90 يوماً.

لذلك، بعد استلام الإخراج، يجب على الموقع الوجهة تخزين المقال النهائي والصور وبيانات السيو وبيانات Open Graph وبيانات Schema داخل قاعدة بياناته أو خادمه أو التخزين أو CDN الخاص به، ويجب ألا يعتمد على روابط OxiRanker للعرض العام.

فيديو تدريبي

فيديو تدريب الاتصال

استخدم هذا الفيديو كشرح بصري قبل ربط WordPress أو تنفيذ تكامل API.

مخرج API

نفّذ مسار الإخراج خطوة بخطوة

خزّن Export Token و Webhook Secret في backend أولاً، ثم أكمل Webhook receiver وقاعدة البيانات ومنطق UPSERT و Image Mirroring والمزامنة اليومية.

الخطوة 01

التسجيل وإنشاء نطاق في OxiRanker

أولاً، سجّل في OxiRanker، ثم ادخل إلى حسابك واضغط Add Domain من قسم Domains.

صيغة النطاق الصحيحة

في حقل Domain name، أدخل النطاق الجذر فقط. يجب ألا يحتوي العنوان على http أو https أو www.

example.com

صيغ خاطئة

لا تُدخل هذه الصيغ في حقل النطاق.

https://example.com
http://example.com
www.example.com
بعد إنشاء النطاق
إذا كانت صيغة النطاق صحيحة، ينشئ OxiRanker النطاق وينقلك إلى خطوة تأكيد ملكية النطاق.
الخطوة 02

تأكيد ملكية النطاق

في هذه الخطوة، يجب أن تثبت أن النطاق يخصك أو أن لديك صلاحية إدارته.

DNS TXT

موصى به

أضف TXT record إلى DNS الخاص بالنطاق. في Cloudflare، تكون هذه غالباً أسهل طريقة.

DNS Name:
_yourdomain-verification.example.com

TXT Value:
acb1****29803

HTTP File

ضع ملفاً نصياً داخل مسار .well-known في موقعك حتى يتمكن OxiRanker من التحقق منه.

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

File Content:
220daaa37****42730f27

Meta Tag

ضع meta tag داخل قسم head في الصفحة الرئيسية لموقعك.

<meta name="yourdomain-verification" content="190208d****d275b65" />
ملاحظة مهمة حول DNS
بعد إضافة DNS record، خصوصاً في Cloudflare، قد تحتاج إلى الانتظار من دقيقتين إلى 3 دقائق. ثم اضغط Confirm في نفس صفحة OxiRanker.
مدة صلاحية رمز التحقق
قيم تأكيد ملكية النطاق لها حد زمني. حالياً، تكون قيم التحقق صالحة عادة لمدة 1440 دقيقة.
الخطوة 03

استلام توكنات مجانية بعد تأكيد أول نطاق

بعد تأكيد أول نطاق، يضيف OxiRanker إلى حسابك 1000 توكن مجاني للاختبار الأولي.

فقط للنطاق الأول
تُضاف هذه الهدية فقط لأول نطاق يتم التحقق منه، ولا تُضاف مرة أخرى للنطاقات اللاحقة.
الخطوة 04

إعداد اللغات ورابط المدونة

في قسم Blog URLs، حدّد ما إذا كان موقعك بلغة واحدة أو متعدد اللغات، وما اللغات التي يجب استخدامها للمقالات المُنشأة.

اختيار اللغات

يدعم OxiRanker 24 لغة. يمكنك اختيار لغة واحدة أو عدة لغات بناءً على بنية موقعك.

إدخال رابط المدونة

لكل لغة، أدخل رابط المدونة الخاص بتلك اللغة. إذا كان موقعك متعدد اللغات، فأدخل مسار كل لغة بشكل منفصل.

https://example.com/fa/blog
https://example.com/en/blog
بنية الرابط
إذا كان موقعك يستخدم بنية مختلفة، فأدخل رابط المدونة الحقيقي في موقعك. النقطة المهمة أن يكون الرابط النهائي هو المسار الذي يجب نشر المقالات فيه.
الخطوة 05

إضافة الكلمات المفتاحية لكل لغة

في تبويب Keywords، أضف 3 كلمات مفتاحية على الأقل وحتى 50 كلمة لكل لغة.

القاعدة الأساسية

يجب إدخال الكلمات المفتاحية لكل لغة بنفس اللغة. إذا كانت لغة المقال الإنجليزية، فيجب أن تكون الكلمات المفتاحية أيضاً بالإنجليزية.

كيفية إضافة الكلمات المفتاحية

بعد كتابة كل كلمة مفتاحية، اضغط Enter لإضافتها. بعد إكمال جميع الكلمات، اضغط Save.

خطأ شائع
إذا أدخلت كلمات مفتاحية تركية للغة الإنجليزية، فقد تظهر عبارات تركية داخل المحتوى الإنجليزي. يجب أن تتطابق لغة الكلمات المفتاحية مع لغة المقال.
الخطوة 06

إكمال ملف Article Profile للنطاق

يوضح Article Profile لـ OxiRanker ما هي علامتك التجارية، ولمن يكتب، وما الأسلوب الذي يجب استخدامه، وما الموضوعات التي يجب تجنبها.

المجال

يحدد السوق أو فئة العمل. مثال: استشارات عقارية وشراء وبيع العقارات في دبي.

وصف النشاط التجاري

يوضح ما الذي يبيعه النشاط التجاري، ومن يخدم، وكيف يعمل، وما الذي يميزه.

الجمهور

يحدد من يجب أن يخاطبه المحتوى. يمكن حفظ كل جمهور بشكل منفصل.

النبرة

تتحكم في أسلوب وصوت المحتوى المُنشأ، مثل الرسمي أو التعليمي أو البسيط أو التقني أو الموجه للبيع.

هدف المحتوى

يحدد ما يجب أن تحققه المقالات، مثل جذب العملاء أو زيادة الزيارات أو تثقيف المستخدمين.

الموضوعات المحظورة

أي شيء لا يجب أن تتحدث عنه العلامة التجارية أو لا يجب أن ترتبط به يتم إدخاله هنا.

CTA اختياري

CTA يعني الإجراء الذي تريد من القارئ اتخاذه بعد قراءة المقال. إذا لم تكن متأكداً من النص المناسب، فمن الأفضل ترك هذا الحقل فارغاً.

ملف متعدد اللغات
إذا كان موقعك متعدد اللغات، فأنت تحتاج فقط إلى إكمال الملف بلغة واحدة. يستطيع OxiRanker إعداد النسخ المطلوبة للغات الأخرى بناءً على نفس الملف.
الخطوة 07

الربط الداخلي والخارجي

يمكن لـ OxiRanker تطبيق الروابط الداخلية والخارجية داخل المقالات بذكاء.

الروابط الداخلية

هذا الخيار مفعّل افتراضياً ومن الأفضل إبقاؤه مفعّلاً. يساعد الربط الداخلي محركات البحث على فهم بنية محتوى موقعك بشكل أفضل.

في المقال الأول، لا تُنشأ الروابط الداخلية عادة لأنه لا يوجد مقال سابق بعد. من المقال الثاني فصاعداً، يستطيع النظام تنفيذ الربط الداخلي.

الروابط الخارجية

تُضاف الروابط الخارجية إلى مصادر رسمية وموثوقة، ويتم وضعها كـ nofollow حتى لا تنتقل قوة السيو بشكل غير مقصود.

الخطوة 08

فحص جودة الملف باستخدام AI

بعد إكمال Article Profile، يراجع OxiRanker جودة الملف.

85+
الحد الأدنى المطلوب للحفظ
95
درجة ممتازة يتم الوصول إليها غالباً بعد التحسينات
AI
اقتراحات احترافية لكل حقل
إذا كانت الدرجة أقل من 85
لا يسمح النظام بالحفظ النهائي ويعرض توضيحات واقتراحات تحسين لكل حقل فيه مشكلة. يمكنك تطبيق الاقتراحات ثم الحفظ مرة أخرى.
الخطوة 09

ربط Telegram لملخصات المقالات

بعد حفظ الملف بنجاح، يمكنك تفعيل إرسال ملخصات المقالات إلى قناة Telegram.

1

إضافة البوت

أضف @Oxiranker_Bot إلى قناة Telegram الخاصة بك واجعله Admin.

2

إدخال اسم مستخدم القناة

أدخل اسم القناة مع @. مثال: @my_channel

3

تفعيل إرسال الملخص

بعد تسجيل قناة صالحة، فعّل إرسال ملخص المقالات.

فائدة Telegram
إذا تم تفعيله، فبعد إنشاء المحتوى، يُرسل ملخص المقال إلى قناة Telegram مع الصورة والرابط. يمكن أن يساعد ذلك في تقوية مسارات الزيارات إلى موقعك.
الخطوة 10

إعداد محرك المقالات التلقائي

في تبويب Automatic article generation، يمكنك إدارة محرك إنشاء المقالات التلقائي.

اختيار ذكي للموضوع

يستخدم النظام الكلمات المفتاحية و Article Profile لاختيار موضوعات مناسبة وفريدة وقابلة للنشر.

إنشاء واعٍ بالعلامة التجارية

تتم مراعاة النبرة والجمهور وهدف المحتوى وقيود العلامة التجارية أثناء إنشاء المقال.

النشر المجدول

يتم توزيع السعة الشهرية على مدار الشهر حتى يبدو نشر المحتوى طبيعياً ومتناسقاً.

السعة الشهرية

7 articles / month
10 articles / month
15 articles / month
30 articles / month

30 مقالاً تعني تقريباً مقالاً واحداً يومياً. 15 مقالاً تعني تقريباً يوماً بعد يوم. 10 مقالات تعني تقريباً كل يومين إلى ثلاثة أيام. 7 مقالات تعني تقريباً كل ثلاثة إلى أربعة أيام.

طول المقال والصورة

يمكن أن يكون طول المقال Short أو Standard أو Long. خيار Standard مناسب ومتوازن لمعظم المواقع.

يمكن تفعيل صور المقال أو تعطيلها، ويمكن اختيار ثيم الصورة Light أو Dark.

ملاحظة لمواقع API
ينشئ المحرك التلقائي المقالات داخل OxiRanker. لنقل كل مقال إلى موقعك المخصص بعد الإنشاء، يجب تنفيذ Export API و Webhook وتخزين قاعدة البيانات و Image Mirroring.
الخطوة 11

إنشاء أول مقال من Generate

بعد تجهيز النطاق، افتح قائمة Generate، واختر النطاق، ثم اضغط New Content أو New Article.

1

اللغة

يتم اختيار لغة واحدة فقط لكل طلب محتوى.

2

الموضوع

اكتب موضوعاً واضحاً. Source URL اختياري.

3

الصورة

فعّل الصورة أو عطّلها واختر ثيم Light أو Dark.

4

الدفع والإنشاء

يتم خصم التكلفة من المحفظة ويبدأ إنشاء المقال.

وقت الإنشاء
عادةً يستغرق إنشاء المقال والصورة حوالي 2 إلى 3 دقائق. بعد الاكتمال، يظهر المقال داخل OxiRanker.
Source URL
إذا تم إدخال Source URL، يستخدمه OxiRanker لفهم الموضوع بشكل أفضل، لكنه لا ينشئ محتوى منسوخاً. النظام مصمم لإنشاء محتوى فريد.
الخطوة 12

مراجعة Preview و SEO Report و JSON Output

بعد إنشاء المقال، تتوفر عدة مخرجات مهمة لمراجعة الجودة والتحضير للنشر.

Preview

يعرض نص المقال والصورة المُنشأة. هذا القسم مفيد لمراجعة الجودة النهائية قبل النشر.

SEO Report

يعرض تقرير سيو واضحاً يشمل SEO Score والعنوان و meta description و canonical والكلمات المفتاحية و schema والعناوين والروابط والصور و OpenGraph.

JSON Output

يعرض الإخراج الكامل للمقال للمستخدمين التقنيين، بما في ذلك slug و html_body و schema وروابط الصور والوسوم والتصنيف ووقت القراءة وتواريخ الإنشاء/التحديث.

معنى المخرجات
توضح هذه المخرجات أن المقال ليس مجرد نص عادي. إنه محتوى منظم وجاهز للسيو ومعدّ لـ WordPress و CMS و API وأنظمة النشر.
الخطوة 13

ماذا يفعل 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/json
GET 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"
}
ملاحظة مهمة جداً
Webhook هو رسالة خفيفة فقط. يجب ألا يتوقع الموقع الوجهة وجود المقال الكامل داخل Webhook. بعد استلام domain_id و article_id، يجب أن يجلب المقال الكامل من Export JSON API ويخزنه في قاعدة بياناته الخاصة.
الخطوة 14

مسؤولية تخزين المحتوى و Image Mirroring

يجب على الموقع الوجهة تخزين المقال النهائي والصور على بنيته التحتية الخاصة.

ما الذي يجب على الموقع الوجهة تخزينه

نص المقال الكامل في قاعدة بيانات الموقع الوجهة
صورة المقال الرئيسية على خادم الموقع الوجهة أو التخزين أو CDN
صورة Open Graph على خادم الموقع الوجهة أو التخزين أو CDN
بيانات SEO و OpenGraph و Article Schema
raw_detail_payload و raw_list_item للحفاظ على الإخراج الأصلي

مثال صحيح لمسار الصورة

استخدام روابط صور OxiRanker مباشرة وبشكل دائم ليس صحيحاً. يجب تنزيل الصورة واستبدال روابط المقال بروابط داخلية خاصة بالموقع الوجهة.

https://oxiranker.com/uploads/articles/3_5/cover.webp
https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp
OxiRanker ليس استضافة دائمة للموقع الوجهة
OxiRanker غير مسؤول عن تخزين مخرجات المحتوى والصور الخاصة بالموقع الوجهة بشكل دائم. OxiRanker ليس خدمة استضافة لمحتوى المستخدمين، وقد لا تبقى المحتويات المُنشأة أو صور الغلاف أو صور Open Graph أو ملفات الإخراج على بنية OxiRanker لأكثر من 90 يوماً. لذلك، يجب على الموقع الوجهة تخزين المقال النهائي والصور وبيانات السيو و OpenGraph و Schema داخل قاعدة بياناته أو خادمه أو التخزين أو CDN الخاص به.
الخطوة 15

الحصول على Export Token و endpoints جاهزة

يتوفر Export Token و endpoints الجاهزة للاستخدام من صفحة المقال، تبويب Outputs وقسم API Output.

أين تحصل على Export Token

أنشئ أول مقال في OxiRanker.
افتح صفحة ذلك المقال.
افتح تبويب Outputs.
افتح قسم API Output.
أنشئ توكن من Token management.
يتم عرض التوكن الكامل مرة واحدة فقط.
خزّن التوكن في backend الخاص بالموقع الوجهة أو في .env.
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=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
تدوير Token
إذا تم تدوير Export Token في لوحة OxiRanker، تنتهي صلاحية التوكن السابق. يجب استبدال القيمة القديمة بالقيمة الجديدة فوراً في ملف .env أو بيئة backend للموقع الوجهة، ويجب إعادة تشغيل أو إعادة نشر backend الوجهة.
الخطوة 16

إنشاء Webhook Secret و endpoint للوجهة

يتم إنشاء Webhook Secret أيضاً من صفحة المقال، تبويب Outputs وقسم API Output.

أين يتم إنشاء Webhook Secret؟

افتح صفحة المقال.
افتح تبويب Outputs.
افتح قسم API Output.
أنشئ secret في قسم Webhook Secret.
يتم عرض secret الكامل مرة واحدة فقط.
خزّن secret في ملف .env الخاص بالموقع الوجهة.

Endpoint ثابت للوجهة

يقبل OxiRanker المسار الثابت التالي فقط للـ Webhook الخاص بالوجهة. يجب على الموقع الوجهة إنشاء هذا endpoint في backend الخاص به.

POST /api/oxiranker/webhook

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

يجب أن يقبل هذا endpoint طلبات POST فقط، ويحافظ على raw body، ويتحقق من timestamp و signature، ويُرجع استجابة ناجحة لـ webhook.test.

الأحداث النشطة
في الإصدار الحالي، الأحداث ثابتة: article.created و article.updated. لاختبار الاتصال، تُرسل قيمة الحدث webhook.test.
الخطوة 17

قيم .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

قيم الأمان

OXIRANKER_EXPORT_API_TOKEN
يجب تخزين Export API token في backend فقط.
OXIRANKER_WEBHOOK_SECRET
يجب تخزين Webhook Secret في backend فقط.
OXIRANKER_WEBHOOK_MAX_SKEW_MS
الحد الأقصى المسموح لفارق الوقت في Webhook. القيمة 300000 تعني 5 دقائق.

قيم التخزين

SITE_ARTICLE_IMAGE_UPLOAD_DIR
المسار الفعلي لتخزين الصور المنسوخة على خادم الوجهة.
SITE_ARTICLE_IMAGE_MAX_BYTES
الحد الأقصى المسموح لحجم الصورة عند التنزيل والتخزين.
PUBLIC_BASE_URL
الرابط العام للموقع الوجهة المستخدم لإنشاء روابط الصور والمقالات النهائية.
أمان Frontend
يجب عدم وضع Export Token و Webhook Secret داخل كود frontend أو JavaScript المرئي للمستخدم أو localStorage أو window object.
الخطوة 18

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_TOKEN

HTML مناسب للمواقع التي تريد مخرجاً جاهزاً، لكن الأسلوب الأكثر احترافية هو تخزين 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.

الخطوة 19

توقيع 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 الأصلي للتحقق من التوقيع.

مقارنة آمنة زمنياً
لا تقارن التوقيعات باستخدام expected === received. في Node.js، من الأفضل استخدام crypto.timingSafeEqual.
الخطوة 20

نموذج كود 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,
  });
});
الخطوة 21

قاعدة البيانات الموصى بها للموقع الوجهة

يجب على الموقع الوجهة تخزين المقالات في قاعدة بياناته الخاصة ومنع التكرار باستخدام 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
في PostgreSQL، تُستخدم علامات الاقتباس البسيطة للنصوص الفارسية أو Unicode. شيء مثل N'...' غير مطلوب ولا يجب استخدامه.
الخطوة 22

منع التكرار باستخدام UPSERT

يجب ألا يتم إدراج المقال مرة أخرى عند كل Webhook. إذا كان موجوداً بالفعل، فيجب تحديثه.

المفتاح الأساسي لمنع التكرار
أهم constraint لمنع التكرار هو: UNIQUE (source_provider, source_domain_id, source_article_id)
INSERT INTO public.site_articles (
  source_provider,
  source_domain_id,
  source_article_id,
  source_request_id,
  lang,
  dir,
  slug,
  title,
  meta_description,
  html_body,
  focus_keywords,
  category,
  tags,
  reading_time_minutes,
  cover_image_url,
  og_image_url,
  article_schema,
  source_created_at,
  source_updated_at,
  sync_status,
  last_synced_at,
  publish_status,
  published_at,
  created_at,
  updated_at
)
VALUES (
  'oxiranker',
  1,
  5,
  5,
  'fa',
  'rtl',
  'smart-internal-links',
  'Article title',
  'Meta description',
  '<p>HTML body</p>',
  '[]'::jsonb,
  'SEO',
  '[]'::jsonb,
  7,
  'https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp',
  'https://example.com/uploads/site-articles/oxiranker/1/5/cover.webp',
  '{}'::jsonb,
  '2026-05-20T19:33:23.618Z',
  '2026-05-20T19:34:09.629Z',
  'synced',
  now(),
  'published',
  now(),
  now(),
  now()
)
ON CONFLICT (source_provider, source_domain_id, source_article_id)
DO UPDATE SET
  source_request_id = EXCLUDED.source_request_id,
  lang = EXCLUDED.lang,
  dir = EXCLUDED.dir,
  slug = EXCLUDED.slug,
  title = EXCLUDED.title,
  meta_description = EXCLUDED.meta_description,
  html_body = EXCLUDED.html_body,
  focus_keywords = EXCLUDED.focus_keywords,
  category = EXCLUDED.category,
  tags = EXCLUDED.tags,
  reading_time_minutes = EXCLUDED.reading_time_minutes,
  cover_image_url = EXCLUDED.cover_image_url,
  og_image_url = EXCLUDED.og_image_url,
  article_schema = EXCLUDED.article_schema,
  source_created_at = EXCLUDED.source_created_at,
  source_updated_at = EXCLUDED.source_updated_at,
  sync_status = 'synced',
  last_synced_at = now(),
  last_sync_error = NULL,
  publish_status = CASE
    WHEN public.site_articles.publish_status = 'archived'
    THEN public.site_articles.publish_status
    ELSE 'published'
  END,
  published_at = COALESCE(public.site_articles.published_at, EXCLUDED.published_at, now()),
  deleted_at = NULL,
  updated_at = now();
الخطوة 23

جلب المقال الكامل والمزامنة بعد Webhook

عند وصول 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,
  });
}
الخطوة 24

قواعد Image Mirroring وتخزين الصور

يجب على الموقع الوجهة تنزيل الصور وتخزينها على بنيته التحتية الخاصة واستبدال روابط المقال.

قواعد Image Mirroring

يجب قبول روابط HTTPS فقط.
يجب أن يكون الملف صورة حقيقية.
يجب أن يبدأ content-type بـ image/.
يجب عدم تخزين الملفات الفارغة.
يجب ألا يتجاوز حجم الملف الحد المسموح.
إذا كانت cover و og متطابقتين، فقم بالتنزيل مرة واحدة فقط.
بعد النسخ المحلي، استبدل الروابط داخل html_body و article_schema.

مسار التخزين الموصى به

/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

المزامنة اليومية للموثوقية

Webhook وحده ليس كافياً. يجب على الموقع الوجهة أيضاً تنفيذ مزامنة يومية لاستعادة المقالات الفائتة.

لماذا نحتاج إلى مزامنة يومية؟

قد يكون الموقع الوجهة متوقفاً مؤقتاً.
قد يحدث timeout للـ Webhook.
قد تحدث مشاكل نشر أو شبكة.
قد يتم إنشاء مقال ولكن يتم فقد Webhook الخاص به.

خوارزمية المزامنة اليومية

ابدأ offset من 0.
استخدم limit 20 أو 50.
اجلب قائمة المقالات من Export List API.
لكل عنصر، اجلب Export JSON الخاص بذلك المقال.
انسخ الصور محلياً.
خزّن المقال باستخدام UPSERT.
زد offset واستمر حتى يكتمل total.
الخطوة 26

عرض المقالات على الموقع الوجهة

بعد تخزين المقالات في 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;
رابط المقال الموصى به
للمواقع متعددة اللغات، يمكنك استخدام النمط /{lang}/blog/{slug}. مثال: /fa/blog/smart-internal-links
الخطوة 27

السيو و Schema و RTL وعرض HTML

يجب على الموقع الوجهة قراءة بيانات السيو من قاعدة بياناته الخاصة وعرضها في صفحة المقال.

حقول السيو

title لعنوان الصفحة
meta_description لوصف meta
effective_canonical_url لـ canonical
og_title و og_description لـ Open Graph
og_image_url أو cover_image_url لصورة المعاينة الاجتماعية
article_schema لـ JSON-LD

عرض HTML

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

إذا كان الموقع الوجهة لديه سياسة أمان صارمة، فيمكنه تنظيف HTML قبل الحفظ أو قبل العرض.

RTL و LTR

كل مقال يحتوي على حقول lang و dir و is_rtl. إذا كان dir يساوي rtl، فيجب عرض صفحة المقال من اليمين إلى اليسار.

<article dir="rtl">
  ...
</article>
الخطوة 28

الأخطاء الشائعة ومعانيها

يجب أن تكون أخطاء الموقع الوجهة باللغة الإنجليزية وواضحة ومفهومة حتى تتم معالجة المشاكل بسرعة.

أخطاء Export API

توكن تصدير النطاق مفقود.
لم يتم إرسال توكن Export API. يجب إرسال Authorization: Bearer YOUR_EXPORT_TOKEN.
توكن تصدير النطاق غير صالح.
التوكن غير صحيح، أو تم تدويره، أو لا يرتبط بهذا النطاق، أو أن domainId غير صحيح.
التوكن لا يحتوي على الصلاحية المطلوبة.
التوكن لا يحتوي على الصلاحية المطلوبة. يجب أن تحتوي التوكنات الجديدة على articles:read و exports:read.
قيمة lang غير صالحة.
تم إرسال قيمة lang في query parameter بشكل غير صحيح.

أخطاء Webhook

يجب أن يستخدم رابط Webhook بروتوكول HTTPS.
يجب أن يبدأ رابط Webhook بـ https://.
توقيع Webhook غير صالح.
Webhook Secret غير صحيح، أو تم تدوير secret، أو تم تغيير raw body، أو تم حساب التوقيع بشكل غير صحيح.
طابع Webhook الزمني منتهي الصلاحية أو غير صالح.
الطابع الزمني قديم، أو في المستقبل، أو غير صالح. تحقق من ساعة الخادم و NTP.
فشل إرسال اختبار Webhook.
الموقع الوجهة لم يُرجع حالة نجاح من 200 إلى 299، أو انتهت المهلة، أو لم يتعامل مع webhook.test بشكل صحيح.
الخطوة 29

قائمة اختبار نهائية لـ 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/json
GET 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.

بعد التدوير
إذا تم تدوير Webhook Secret أو Export Token، انسخ القيمة الجديدة فوراً، واستبدلها في ملف .env الخاص بالموقع الوجهة، ثم أعد تشغيل أو أعد نشر backend الوجهة.
الخطوة 30

قواعد مهمة للتنفيذ الصحيح

يجب اتباع هذه القواعد في تكامل API حتى يبقى النظام آمناً ومستقراً وقابلاً للصيانة.

يجب تخزين Export Token في backend فقط.
يجب تخزين Webhook Secret في backend فقط.
يجب أن يكون endpoint الخاص بـ Webhook في الوجهة بالضبط /api/oxiranker/webhook.
يجب أن يستخدم رابط Webhook بروتوكول HTTPS.
يجب دائماً التحقق من Webhook signature و timestamp.
يجب تخزين المقالات باستخدام UPSERT وليس insert بسيط.
يلزم وجود unique constraint لمنع التكرار.
يجب تنزيل الصور من OxiRanker وتخزينها على الموقع الوجهة.
يجب ألا يعتمد الموقع الوجهة بشكل دائم على روابط صور OxiRanker.
يجب استبدال روابط الصور داخل html_body و article_schema.
إذا فشل Webhook، يجب أن تستعيد المزامنة اليومية المقال الفائت.
يجب أن تكون الأخطاء المعروضة للمستخدمين أو في السجلات باللغة الإنجليزية ومفهومة.
إذا كان المقال مؤرشفاً، فيجب ألا تقوم المزامنة التلقائية بنشره مرة أخرى.
يجب الاحتفاظ بـ raw_detail_payload و raw_list_item حتى لا تضيع أي بيانات من الإخراج الأصلي.
إذا تم تدوير Export Token أو Webhook Secret، فيجب استبدال القيمة الجديدة في .env وإعادة تشغيل أو إعادة نشر backend.
الخطوة 31

ملخص مسار API الكامل

بعد إكمال هذه الخطوات، يستطيع موقعك المخصص استقبال المقالات التي ينشئها OxiRanker وتخزينها ونسخ صورها ونشرها بأمان.

التسجيل في OxiRanker
إضافة نطاق
تأكيد ملكية النطاق
اختيار اللغات وإضافة Blog URL
إضافة الكلمات المفتاحية لكل لغة
إكمال Article Profile
إعداد Telegram عند الحاجة
إعداد محرك المقالات التلقائي
إنشاء أول مقال من Generate
مراجعة Preview و SEO Report
الحصول على Export Token من API Output
الحصول على endpoints جاهزة لـ Export API
الحصول على Webhook Secret
إنشاء endpoint الثابت /api/oxiranker/webhook في backend الوجهة
تخزين Token و Secret في .env الخاص بالموقع الوجهة
إنشاء جدول site_articles و unique constraint
تنفيذ جلب المقال الكامل من Export JSON
تنفيذ Image Mirroring
تنفيذ UPSERT
تنفيذ المزامنة اليومية
عرض المقال و SEO و OG و Schema من قاعدة بيانات الوجهة
تشغيل اختبارات Export API و Webhook النهائية