تخطي إلى المحتوى
التوثيق

Webhook التسليم: استقبال المقالات على موقعك الخاص

الـ webhook الموقَّع الذي يرسل مقالات AutoPSEO إلى موقعك: الترويسات، وكل حدث مع حمولته، والردود المطلوبة، وإعادة المحاولة، والأوامر المعلّقة، وIndexNow.

الخاصية ← المحتوى ← أماكن نشر المقالاتمدة القراءة 17 دقيقة

يرسل AutoPSEO كل مقال ينشره أو يحدّثه أو يسحبه إلى موقعك الخاص عبر طلب HTTPS من نوع POST موقَّع. يخزّن موقعك المقال ويعرضه بقوالبه الخاصة، فيبقى المحتوى في نظامك ويستمر عمله حتى لو قررت يومًا مغادرة AutoPSEO. هذه الصفحة هي المرجع للمطوّر الذي يبني المستقبِل: التوقيع، وكل حدث مع حمولته، والردود التي يقرؤها AutoPSEO، وإعادة المحاولة، والأوامر المعلّقة، وIndexNow.

إعداد الـ webhook

  1. افتح صفحة المحتوى الخاصة بالموقع وانتقل إلى أماكن نشر المقالات.
  2. املأ حقل عنوان الـ webhook على موقعك. يجب أن يكون عنوان URL يبدأ بـ https:// ويستخدم اسم مضيف عامًا، إذ تُرفض عناوين IP وlocalhost. لا يتبع AutoPSEO عمليات إعادة التوجيه، لذا أدخل العنوان النهائي.
  3. املأ حقل عنوان موقعك، مثل https://example.com. يُلحق مسار كل نوع محتوى بهذا العنوان، فالمقال من نوع مساره /answers يكون في https://example.com/answers/<slug>.
  4. اختر حفظ وإرسال اختبار. ينشئ AutoPSEO سرّ توقيع (whsec_ يليه 48 حرفًا سداسيًا عشريًا) ويرسل ping إلى عنوانك. المستقبِل الذي يتحقق من التوقيعات سيفشل في أول ping لأنه لا يملك السرّ بعد.
  5. أظهِر سرّ التوقيع بالضغط على إظهار أو نسخ، واحفظه كسرّ في موقعك، ثم اختر إرسال اختبار.
  6. اقرأ قسم ما يتعامل معه موقعك: يظهر لكل أمر إحدى الحالات متصل أو غير متصل أو مُعلَن، لم يُختبر أو لم يُختبر.

تبدأ المواقع الجديدة بأن يستضيف AutoPSEO المقالات على عنوانه الخاص (راجع الاستضافة)، ويمكن للاستضافة والـ webhook أن يعملا جنبًا إلى جنب. بعد أن يعرض موقعك المقالات، اختر إيقاف الاستضافة على AutoPSEO: تنتقل كل مقالة منشورة إلى عنوان موقعك وتُرسل إلى الـ webhook بالحدث article.published. يتطلب هذا الزر webhook أجاب عن آخر اختبار وعنوانًا لموقعك. وما دامت الاستضافة متوقفة، لا يمكن حذف الـ webhook.

سرّ جديد يستبدل سرّ التوقيع فورًا ويتوقف السرّ القديم عن اجتياز التحقق في الحال. ضع السرّ الجديد في موقعك مباشرة بعد إنشائه.

لا يضع AutoPSEO عمليات التسليم في الطابور إلا لـ webhook أُجيب عن آخر ping له برمز 2xx. يؤدي فشل ping (عند الحفظ، أو عند إرسال اختبار، أو في بداية فحص الموقع) إلى وضع الـ webhook في حالة خطأ، ولا تُوضع في طابوره المقالات المنشورة أثناء ذلك. أبقِ ping يستجيب، واختر إرسال اختبار بعد إصلاح أي عطل.

الترويسات والتوقيع

كل طلب هو POST بجسم JSON وبالترويسات التالية:

الترويسةالقيمة
Content-Typeapplication/json
User-AgentAutoPSEO-Webhook/1
X-AutoPSEO-Eventاسم الحدث، ويرد أيضًا في الجسم باسم event
X-AutoPSEO-Deliveryمعرّف التسليم، ويرد أيضًا في الجسم باسم id. وهو نفسه في كل إعادة محاولة للتسليم الواحد
X-AutoPSEO-Timestampوقت Unix بالثواني الذي وُقّعت فيه هذه المحاولة
X-AutoPSEO-Signaturesha256= يليه HMAC-SHA256 بصيغة سداسية عشرية للسلسلة <timestamp>.<raw body>

للتحقق من طلب:

  1. خذ ترويسة الطابع الزمني، ثم نقطة، ثم جسم الطلب الخام تمامًا كما وصل، قبل أي تحليل JSON.
  2. احسب HMAC-SHA256 على هذه السلسلة. المفتاح هو سرّ التوقيع كاملًا كما يعرضه التطبيق، بما في ذلك البادئة whsec_، بصيغة بايتات UTF-8.
  3. حوّل الناتج إلى صيغة سداسية عشرية وقارنه بالجزء الذي يلي sha256= في X-AutoPSEO-Signature، بمقارنة ثابتة الزمن. ارفض الطلب إذا اختلفا.

تُوقَّع كل محاولة من جديد بطابعها الزمني الخاص، فيمكنك رفض الطلبات التي يبتعد طابعها الزمني كثيرًا عن ساعتك (خمس دقائق نافذة شائعة) دون أن تتعطل إعادة المحاولة. أما HMAC نفسه فمعرَّف في RFC 2104.

يحمل كل جسم الحقول id وevent وoccurredAt (بصيغة ISO 8601 وبتوقيت UTC) وsite: { propertyId, domain }.

الأحداث

الحدثالنوعمتى يرسله AutoPSEO
pingفحصعند حفظ الـ webhook، وعند إرسال اختبار، وفي بداية كل فحص للموقع
testفحصمباشرة بعد ping ناجح، واحد لكل أمر
article.publishedتسليمعندما يصبح المقال منشورًا لأول مرة؛ وكل مقال منشور عند إيقاف الاستضافة
article.updatedتسليمعندما تصبح مراجعة جديدة منشورة (تحديث، أو تحسين، أو استرجاع، أو نشر ثانٍ بعد السحب، أو إضافة لغات) أو عند إضافة غلاف
article.unpublishedتسليمعند سحب مقال، أو دمجه في صفحة أخرى
path.addتسليمعند اعتماد قيمة جديدة لنوع محتوى مُنمَّط
path.removeتسليمعندما يُطلب سحب مسار قيمة ما
path.checkاستدعاءقبل أن يخطط AutoPSEO لصفحة على عنوان ليس لموقعك صفحة عليه، أو يقترح قيمة جديدة
site.exportاستدعاءأثناء فحص الموقع، وفي المزامنة اليومية للمواضيع، وقبل أن يغيّر AutoPSEO مقالًا منشورًا
articles.adoptedاستدعاءبعد أن يستورد الفحص مقالات صدّرها موقعك
site.handoverاستدعاءعندما يسلّم أحدهم كتابة أنواع المحتوى إلى AutoPSEO، أو يستعيدها

تمر عمليات التسليم عبر outbox: تُسجَّل في دفعة قاعدة البيانات نفسها التي سُجّل فيها التغيير، وتُرسل فورًا، وتُعاد محاولتها، ويمكن أن تنتظر موقعك. أما الاستدعاءات فهي طلب واستجابة عاديان: يقرأ AutoPSEO الرد في الحال. يُشرح توقيت إرسال أوامر المسارات في الصفحات المُنمَّطة والمسارات الجديدة.

حمولة المقال

يحمل article.published وarticle.updated المقال كاملًا بكل لغاته:

{
  "id": "0f6d2c1e-7a51-4c1b-9d0e-5b8f3a2e9c41",
  "event": "article.published",
  "occurredAt": "2026-10-12T08:30:00.000Z",
  "site": { "propertyId": "prop_123", "domain": "example.com" },
  "article": {
    "id": "a4c09e2b-6f1d-4e57-8b3a-1d2e3f4a5b6c",
    "slug": "plan-a-content-calendar",
    "path": "/answers/plan-a-content-calendar",
    "type": { "key": "answers", "name": "Answers", "path": "/answers" },
    "lang": "en",
    "langs": ["en", "de"],
    "rev": 1,
    "url": "https://example.com/answers/plan-a-content-calendar",
    "datePublished": "2026-10-12",
    "dateModified": "2026-10-12",
    "contentHash": "9f2c4e1a7b3d5f6081a2c3d4",
    "writer": "platform",
    "category": "Planning",
    "copy": {
      "title": "How to plan a content calendar",
      "metaDescription": "A content calendar in five steps, with a template.",
      "h1": "How to plan a content calendar",
      "directAnswer": "List your topics, give each a date and an owner, then review the plan weekly.",
      "keyTakeaways": ["Plan a quarter at a time.", "Give every piece one owner."],
      "sections": [{ "heading": "Start from your topics", "bodyHtml": "<p>Begin with the searches you want to win.</p>" }],
      "faq": [{ "q": "How far ahead should I plan?", "a": "One quarter is enough for most teams." }],
      "sources": [{ "title": "Creating helpful content", "url": "https://developers.google.com/search/docs/fundamentals/creating-helpful-content" }]
    },
    "translations": { "de": { "title": "Einen Redaktionsplan erstellen", "h1": "Einen Redaktionsplan erstellen" } },
    "cover": {
      "url": "https://app.autopseo.com/media/covers/prop_123/<sha256>.jpg",
      "sha256": "<sha256>",
      "contentType": "image/jpeg",
      "width": 1536,
      "height": 1024
    }
  }
}

اختُصر الإدخال الألماني هنا؛ لكل ترجمة الشكل نفسه الذي لـ copy.

الحقلالمعنى
idمعرّف المقال لدى AutoPSEO. لا يتغيّر أبدًا، لذا هو مفتاح آمن
slugالجزء الأخير من العنوان. في الصفحات المُنمَّطة يحمل مقاطع مفصولة بـ /، مثل google/software-engineer
pathالعنوان نسبةً إلى موقعك: مسار النوع مع الـ slug. يثبت بمجرد وجود الصفحة
type{ key, name, path }. قيمة key ثابتة؛ وpath هو المكان الذي تقع فيه مقالات النوع (فارغ إذا كانت في جذر الموقع)
lang, langsاللغة الأساسية وكل لغات هذه المراجعة
revرقم المراجعة
urlالعنوان الكامل كما يعرفه AutoPSEO: عنوان موقعك بعد إيقاف الاستضافة، أو آخر url أجاب به مستقبِلك
datePublished, dateModifiedبصيغة YYYY-MM-DD. يبقى تاريخ النشر ثابتًا عبر المراجعات اللاحقة
contentHashبصمة للمراجعة
writerمن كتب المراجعة: platform (AutoPSEO)، أو mcp (الذكاء الاصطناعي الخاص بك)، أو user، أو import (نص موقعك نفسه)، أو aggregate (صفحة قائمة)
copyالمقال بلغته الأساسية
translationsاللغات الأخرى، مفهرسة برمز اللغة
categoryتصنيف موقعك للمقال، إذا كان تنسيقك يحتفظ بالتصنيفات
topic{ externalId, keyword } عندما كُتب المقال لموضوع سلّمه موقعك، لتتمكن من إغلاق ذلك الموضوع
partsفي الصفحات المُنمَّطة، قيمة كل جزء: { "company": "google", "role": "software-engineer" }
kindالقيمة "list" في صفحة القائمة. نسختها { title, h1, items: [{ from, text }] }، حيث from هو مسار الصفحة التي جاء منها كل عنصر، وتسمّي parts القيمة التي تجمعها
coverغلاف أنشأه AutoPSEO للمقال. نزّله مرة واحدة واحتفظ بنسختك؛ فالعنوان لا يتغيّر لأن اسم الملف هو SHA-256 للصورة
basedOnفي article.updated: إصدار صفحتك الذي كُتبت هذه المراجعة انطلاقًا منه (راجع التعارضات أدناه)
redirectToفي article.unpublished بعد الدمج: الوجهة الجديدة للعنوان القديم. أجب عنه بـ 301 ولا تنشر شيئًا جديدًا هناك

يحمل article.unpublished حقول article نفسها عدا copy وtranslations وcategory وcover.

داخل copy

الحقلالمعنى
title, metaDescription, h1عنوان الصفحة ووصفها التعريفي وعنوانها الرئيسي
directAnswerالإجابة المختصرة التي تُعرض أولًا
keyTakeawaysقائمة من السلاسل النصية
sections[{ heading, bodyHtml }]
faq[{ q, a }]
sources[{ title, url, lastVerified? }]
questionاختياري: سؤال الباحث بكلماته، عندما يطلبه تنسيقك
categoryاختياري: أحد تصنيفات موقعك
listsاختياري: قوائم نصية عادية مفهرسة بمفتاح، لأنواع المحتوى التي تطلبها (أسئلة نموذجية في دليل مقابلات)

في المقالات التي يكتبها AutoPSEO لا يستخدم bodyHtml سوى p وul وli وstrong وem وa وcode، وتحتفظ الروابط بالسمة href فقط. أما النص المأخوذ من موقعك فيحتفظ بـ HTML الخاص بك. مرّره عبر منظِّفك الخاص أيضًا إذا كانت قوالبك تتطلب ذلك.

ما يجيب به مستقبِلك

أي رمز 2xx يُكمل التسليم. ويقرأ AutoPSEO أيضًا كائن JSON اختياريًا من جسم الرد؛ أبقِه أقل من 64 KB، لأن الرد الأكبر يُعدّ فشلًا.

الحقلالتأثير
urlعنوان https:// الذي يوجد عليه المقال الآن. يسجّله AutoPSEO بوصفه URL المقال
versionإصدار صفحتك، سلسلة معتمة لا تتجاوز 128 حرفًا. تعود بوصفها basedOn مع التحديث التالي
urlsعناوين https:// التي غيّرها هذا التسليم، من أجل IndexNow (كل لغة، أو العناوين التي توقفت عن خدمتها). القيمة [] تعني أن شيئًا لم يتغيّر
coverالقيمة "wanted" عندما لا يكون لديك غلاف للمقال ولم يحمل التسليم غلافًا. ينشئ AutoPSEO واحدًا ويرسله في article.updated

لردود الخطأ معانٍ مختلفة:

ردّككيف يقرؤه AutoPSEOما الذي يحدث
4xx مع { "error": "unknown_event" }، أو 501لا تتعامل مع هذا الأمر بعدينتظر التسليم، دون إعادة محاولة
409 مع { "error": "conflict" }كُتب التحديث انطلاقًا من إصدار لم تعد صفحتك تملكهيفشل التسليم دون إعادة محاولة، ويُعاد قراءة المقال من موقعك قبل تغييره التالي
4xx غير 408 أو 429 مع { "error": "<code>", "details": [] } على path.add أو path.removeرفض مع سببكيُسجَّل؛ راجع الصفحات المُنمَّطة والمسارات الجديدة
أي شيء آخر: لا رد خلال 10 ثوانٍ، أو خطأ شبكة، أو إعادة توجيه، أو 408، أو 429، أو 5xx، أو 4xx آخرفشل مؤقتتُعاد المحاولة مع تأخير تصاعدي

إعادة المحاولة والتسليمات المتجاوَزة

يُرسل التسليم فور حفظ التغيير، وجدول مدته خمس دقائق يرسل كل ما حان موعده، الأقدم أولًا. بعد فشل مؤقت ينتظر AutoPSEO دقيقة واحدة، ثم 5 دقائق، ثم 15 دقيقة، ثم ساعة، ثم 3 ساعات، ثم 6 ساعات، ثم 12 ساعة بين المحاولات. تُعلِّم المحاولة الثامنة الفاشلة التسليم بأنه فشل، وتحسبه صفحة المحتوى في شارة حمراء بجوار الـ webhook.

عندما يوجد تسليم أحدث للمقال نفسه، يُعلَّم التسليم الأقدم المعلّق بأنه متجاوَز ولا يُرسل أبدًا. يحمل كل تسليم المقال كاملًا، لذا فالأحدث هو كل ما يحتاجه موقعك. وتعمل أوامر المسارات المتعلقة بالقيمة نفسها بالطريقة ذاتها.

قد يرى مستقبِلك التسليم نفسه مرتين، مثلًا عندما خزّن المقال لكنه أجاب بعد انتهاء مهلة الـ 10 ثوانٍ. أزِل التكرار بالاعتماد على X-AutoPSEO-Delivery، أو اجعل كل كتابة متكافئة (idempotent) بالتخزين على article.id. ويمكن أن يصل article.updated أيضًا إلى مقال حذفته بعد article.unpublished، لأن إعادة نشر مقال سُحب ترسل تحديثًا: تعامل مع التحديثات بوصفها «أنشئ أو استبدل».

أوامر لا يتعامل معها موقعك بعد

يمكن للموقع أن ينفّذ البروتوكول أمرًا أمرًا. يحتفظ AutoPSEO بحالة كل أمر ويتعرف عليها بثلاث طرق:

  • الاختبارات. بعد كل ping ناجح يرسل AutoPSEO اختبارًا test واحدًا لكل أمر. الجسم هو { id, event: "test", occurredAt, site, test: { event: "<command>", ... } }، حيث يحوي test عيّنة بشكل الأمر الحقيقي وتتعلق بـ <type path>/autopseo-connection-test، وهو عنوان لا يوجد في أي موقع. افحصها كما تفحص الأمر الحقيقي، ولا تغيّر شيئًا، وأجب برمز 2xx: { "test": "<command>", "ok": true } ليصبح متصل، أو { "test": "<command>", "ok": false, "error": "unknown_event" } ليصبح غير متصل. والرد بـ ok: false مع رمز خطأ آخر، مثل سحب عنوان لا تملكه، يُحتسب أيضًا متصلًا. تقع العيّنة تحت test ولا تقع أبدًا في المستوى الأعلى، فلا يجد فيها مستقبِل لا يعرف شيئًا عن الاختبارات أي مقال.
  • التسليمات والاستدعاءات الحقيقية. يعلّم رمز 2xx الأمر بأنه متصل؛ ويعلّمه unknown_event بأنه غير متصل.
  • رد ping. الأوامر التي يسردها ردّك في autopseo.events تظهر بحالة مُعلَن، لم يُختبر إلى أن يقول اختبار أو تسليم شيئًا آخر.

التسليم الخاص بأمر غير متصل ينتظر. تعرض صفحة المحتوى ما هو منتظر، مثل «موقعك لا يسحب المقالات بعد»، مع زر تم على الموقع لأي أمر نفّذته يدويًا؛ وعندها لا يرسل AutoPSEO ذلك التسليم أبدًا. وعندما يجد اختبار أو تسليم لاحق أن الأمر متصل، يُرسل كل ما كان ينتظره.

رد ping

أجب عن ping برمز 2xx. الموقع الذي يستقبل المقالات فقط لا يحتاج إلى أكثر من ذلك. وإذا أردت إخبار AutoPSEO بما يفعله موقعك أيضًا، فأضف كائن autopseo:

{
  "ok": true,
  "autopseo": {
    "version": 1,
    "events": ["article.published", "article.updated", "article.unpublished"],
    "covers": true,
    "indexNow": { "key": "3f1c9a7e5b2d4f60", "keyLocation": "https://example.com/3f1c9a7e5b2d4f60.txt" },
    "export": ["articles", "topics", "profile"],
    "adopt": true,
    "handover": true,
    "types": ["answers"],
    "writer": "site"
  }
}
الحقلالمعنى
versionالقيمة 1. يُتجاهل الكائن إذا لم يكن الإصدار 1 أو أكثر
eventsالأوامر التي تتعامل معها، وتظهر بحالة «مُعلَن» إلى أن تُختبر
coversالقيمة true عندما تعرض صفحاتك غلافًا مع كل مقال. عندها ينشئ كاتب AutoPSEO الغلاف أثناء الكتابة، فيحمله التسليم الأول، ويُطلب من الذكاء الاصطناعي الخاص بك أن يوفّر واحدًا. راجع الأغلفة
indexNow{ key, keyLocation? }: مفتاح IndexNow الذي تقدّمه (من 8 إلى 128 حرفًا وأرقامًا وشرطات) وموقعه بصيغة https:// إذا لم يكن ملف المفتاح في /<key>.txt
exportما يسلّمه site.export: articles وtopics وprofile
adoptتقبل articles.adopted، فيمكن للفحص الكامل أن يتبنّى مقالاتك الموجودة
handoverتقبل site.handover
typesمفاتيح أنواع المحتوى التي تقبلها (حتى 50)
writerمن يكتب مقالاتك الجديدة اليوم: site أو autopseo

IndexNow للعناوين التي تقدّمها

عندما يعلن ردّ ping لديك عن indexNow، يرسل AutoPSEO إشعارًا إلى IndexNow على api.indexnow.org بعد التسليمات التي تقبلها، ولا يرسل موقعك أي إشعار بنفسه.

  • يرسل العناوين urls التي سردها ردّك. وبدون urls يرسل url من ردّك، وإلا فعنوان المقال. والقيمة urls: [] لا ترسل شيئًا.
  • لا تدخل إلا العناوين الواقعة على مضيفك أنت، كما يشترط بروتوكول IndexNow: مضيف keyLocation، وإلا مضيف عنوان موقعك.
  • ترسل كل دورة تسليم إشعارًا واحدًا لكل موقع، بما يصل إلى 10,000 عنوان.
  • يُقبل الرمزان 200 و202. أما 429 أو 5xx أو انعدام الرد فيُبقي العناوين ويعيد المحاولة بعد ساعة، مضاعِفًا المدة في كل مرة حتى يوم واحد (أكثر إذا طلب ذلك Retry-After)، مع ما أضافته التسليمات اللاحقة. أما 403 (المفتاح غير صالح) أو 422 (العناوين ليست على المضيف) فيُسجَّل ثم تُسقَط العناوين.
  • تعرض صفحة المحتوى آخر إشعار: وقته، وعدد العناوين، ورمز حالة HTTP، والمحاولة التالية.
  • ما دام AutoPSEO يستضيف المقالات أيضًا، تُرسل إشعارات الصفحات المستضافة بمفتاح AutoPSEO الخاص، ولا يُستخدم مفتاحك.

أداة IndexNow تحت الفهرسة هي مرسِل يدوي منفصل؛ راجع IndexNow.

التعارضات: نص موقعك هو الأصل

version في ردّك (أو version مقال في تصديرك) هو ما يعتقد AutoPSEO أن صفحتك عليه. يحمله كل article.updated بوصفه basedOn. إذا تغيّرت الصفحة في موقعك منذ ذلك الحين، مثلًا بعد تصحيح من محرر، فأجب بـ 409 مع { "error": "conflict" } ولا تخزّن شيئًا. يفشل التسليم دون إعادة محاولة.

قبل كتابة أي تغيير في الطابور على مقال منشور، يعيد AutoPSEO قراءة ذلك المقال من موقعك عبر site.export مع paths، إذا كان موقعك يتعامل مع site.export. فإن كان إصدارك قد تقدّم، يصبح نصك المراجعة التالية للمقال (الكاتب import)، ويحمل التحديث اللاحق إصدارك الحالي بوصفه basedOn.

path.add وpath.remove

يتعلق هذان التسليمان بقيمة واحدة من نوع محتوى مُنمَّط، مثل شركة في /interviews/{company}/{role}. وهما يحملان كائن path بدل article:

{
  "id": "7c1e9d2a-3b4f-4e8a-9c0d-1e2f3a4b5c6d",
  "event": "path.add",
  "occurredAt": "2026-10-12T08:30:00.000Z",
  "site": { "propertyId": "prop_123", "domain": "example.com" },
  "path": {
    "path": "/interviews/jane-street",
    "type": { "key": "interviews", "name": "Interviews", "path": "/interviews" },
    "part": "company",
    "value": {
      "value": "jane-street",
      "name": "Jane Street",
      "aliases": [],
      "url": "https://<its own site>/",
      "sameAs": ["https://<its careers page>/"],
      "description": "<what it is, in a few words>",
      "facts": [
        { "text": "<one sentence, as the source states it>", "source": "https://<the page that says it>", "lastVerified": "2026-10-01", "tier": "official" }
      ]
    },
    "takes": { "role": ["software-engineer"] }
  }
}
الحقلالمعنى
pathعنوان القيمة نفسها على موقعك
typeنوع المحتوى بصيغة { key, name, path }
partالجزء من العنوان الذي تملؤه القيمة
value{ value, name }، وفي path.add يضاف إليها aliases وurl وsameAs وdescription وfacts التي جمعها كاتب AutoPSEO. لكل حقيقة text وsource وlastVerified وtier بقيمة official أو reported
takesفي path.add: قيم الأجزاء الأخرى التي يبحث بها الناس معها
redirectToفي path.remove: الوجهة التي يجب أن تقود إليها عناوين القيمة من الآن فصاعدًا، بإعادة توجيه 301

أجب برمز 2xx عندما تتقبل الأمر؛ وفي path.add يمكنك إضافة url الذي توجد عليه القيمة الآن، وفي path.remove العناوين urls التي توقفت عن خدمتها، وتذهب إلى IndexNow. وللرفض أجب بـ 4xx مع { "error": "<code>", "details": ["<why>"] }. في path.add يعيد invalid_value القيمة إلى الكاتب مع سببك (الرفض الثالث نهائي)، ويرفضها أي رمز آخر رفضًا نهائيًا. وفي path.remove يُحتسب not_found منجزًا، ويترك أي رمز آخر، مثل 409 in_use، كل شيء كما كان. وتعليم أمر مسار معلّق بـ تم على الموقع يُحتسب موافقة من موقعك.

الاستدعاءات: طلبات يقرأ AutoPSEO ردّها

تُوقَّع الاستدعاءات مثل التسليمات. ينتظر الاستدعاء حتى 60 ثانية افتراضيًا، و30 ثانية لإعادة القراءة وفحوص المسارات، ويقرأ ردودًا حتى 24 MB. أي رد غير 2xx يُفشل الاستدعاء.

site.export

يحمل الطلب export: { what, cursor, limit }، إضافةً إلى since (وقت بصيغة ISO) للمواضيع أو paths لإعادة القراءة. أجب بـ { "items": [], "next": "<cursor>" }؛ ويطلب AutoPSEO مجددًا مع ضبط cursor على قيمة next لديك إلى أن تصبح فارغة أو null.

  • يعمل what: "articles" في فحوص الموقع الكاملة، بواقع 10 في الصفحة، عندما تعلن export مع articles وadopt: true. الرد 400 يعني «غير مدعوم». ومع paths: ["/answers/x"] وlimit: 1 يعيد قراءة مقال واحد؛ فأجب بذلك المقال وحده.
  • يعمل what: "topics" في كل فحص وفي المزامنة اليومية، بواقع 50 في الصفحة؛ وترسل المزامنة اليومية since، وهو وقت آخر سحب.
  • يعمل what: "profile" في كل فحص. أجب بـ { "profile": { types, languages, rules, categories, format } }، وكل منها اختياري: types بصيغة [{ key, name, path }]، وrules مع brandTerms وsynonyms وnoise وonTopic وoffTopic.

يحتاج المقال المُصدَّر إلى externalId، وslug (حروف وأرقام من أي كتابة وشرطات، بحد أقصى 200 حرف، والمقاطع مفصولة بـ /)، وpath ينتهي بذلك الـ slug، وcopy فيه title وh1 وقسم واحد على الأقل. الحقول الاختيارية: lang (الافتراضي en)، وtype (مفتاح نوع محتوى)، وurl، وkeyword، وcategory، وdatePublished، وdateModified، وversion، وtranslations، وowner (معرّف مقال AutoPSEO، بعد التبنّي)، وmaintenance: { kind: "refresh" or "optimize", reason } الذي يضع تلك المهمة في الطابور. يصبح كل مقال مُصدَّر مقالًا مُدارًا بنصك مراجعةً أولى له، ولا يُرسل شيء في المقابل. وإعادة التصدير آمنة، ولا يعود مقال سحبه AutoPSEO عبر التصدير أبدًا.

عنصر الموضوع هو { keyword, status?, volume?, source?, externalId?, type?, reason? }، حيث status إما queued أو planned أو rejected.

articles.adopted

بعد الاستيراد يطلب منك AutoPSEO السماح له بإدارة المقالات الجديدة: { "articles": [{ "externalId", "articleId", "slug", "path" }] }. أجب بـ { "adopted": ["<externalId>"], "refused": [{ "externalId": "<id>", "reason": "<why>" }] }. المقال الذي لا تؤكده يُزال من AutoPSEO مجددًا، ما لم يكن AutoPSEO قد كتب له مراجعة بالفعل.

site.handover

{ "handover": { "types": ["answers"], "writer": "autopseo" } }، يُرسل عندما يختار أحدهم دع AutoPSEO يكتبها أو أعِد الكتابة إلى الموقع في صفحة موقعك الخاصة بالموقع. أجب بـ { "ok": true, "writer": "autopseo" }. بعد التسليم إلى AutoPSEO، يمرّر موقعك المواضيع التي يراجعها إلى AutoPSEO بدل كتابتها.

path.check

{ "paths": [{ "path": "/interviews/google/data-engineer" }, { "path": "/interviews/jane-street", "new": { "part": "company", "value": "jane-street", "name": "Jane Street" } }] }. أجب بنتيجة لكل مسار: { "results": [{ "path": "/interviews/google/data-engineer", "ok": true }, { "path": "/interviews/jane-street", "ok": false, "error": "not_allowed", "details": ["We only list companies with a careers page."] }] }. يُحفظ الرفض مع سببك ولا يُسأل عنه مجددًا؛ أما المسار الذي تتركه خارج الرد فيُسأل عنه لاحقًا.

مستقبِل بسيط

Cloudflare Worker يتحقق من التوقيع عبر Web Crypto API، ويتعامل مع أوامر المقال الثلاثة، ويرفض الباقي بالرد unknown_event. يحفظ المقالات في مساحة أسماء KV مربوطة باسم ARTICLES، وسرّ التوقيع في سرّ Worker باسم AUTOPSEO_SECRET.

const HANDLED = ["article.published", "article.updated", "article.unpublished"];
const MAX_SKEW_SECONDS = 300;
const SITE = "https://example.com";

const json = (data, status = 200) =>
  new Response(JSON.stringify(data), { status, headers: { "content-type": "application/json" } });

function hexToBytes(hex) {
  if (!/^[0-9a-f]{64}$/.test(hex)) return null;
  const out = new Uint8Array(32);
  for (let i = 0; i < 32; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
  return out;
}

async function verified(request, body, secret) {
  const timestamp = request.headers.get("X-AutoPSEO-Timestamp") || "";
  const signature = (request.headers.get("X-AutoPSEO-Signature") || "").replace(/^sha256=/, "");
  if (!/^\d+$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > MAX_SKEW_SECONDS) return false;
  const bytes = hexToBytes(signature);
  if (!bytes) return false;
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
  // verify() compares in constant time.
  return crypto.subtle.verify("HMAC", key, bytes, enc.encode(`${timestamp}.${body}`));
}

export default {
  async fetch(request, env) {
    if (request.method !== "POST") return new Response("Method not allowed", { status: 405 });
    const body = await request.text();
    if (!(await verified(request, body, env.AUTOPSEO_SECRET))) return json({ error: "bad_signature" }, 401);
    let msg;
    try {
      msg = JSON.parse(body);
    } catch {
      return json({ error: "bad_json" }, 400);
    }

    if (msg.event === "ping") return json({ ok: true, autopseo: { version: 1, events: HANDLED } });

    if (msg.event === "test") {
      const command = msg.test && msg.test.event;
      // Check the sample as you would the real command, and store nothing.
      return HANDLED.includes(command)
        ? json({ test: command, ok: true })
        : json({ test: command, ok: false, error: "unknown_event" });
    }

    if (msg.event === "article.published" || msg.event === "article.updated") {
      const a = msg.article;
      const stored = JSON.parse((await env.ARTICLES.get(a.id)) || "null");
      if (a.basedOn && stored && stored.version && stored.version !== a.basedOn) {
        return json({ error: "conflict", details: ["The page changed on the site."] }, 409);
      }
      // Any change to the page, here or in your own editor, gets a new version.
      const version = crypto.randomUUID();
      await env.ARTICLES.put(a.id, JSON.stringify({ ...a, version }));
      return json({ url: SITE + a.path, version });
    }

    if (msg.event === "article.unpublished") {
      const a = msg.article;
      if (a.redirectTo) await env.ARTICLES.put(a.id, JSON.stringify({ path: a.path, redirectTo: a.redirectTo }));
      else await env.ARTICLES.delete(a.id);
      return json({ ok: true });
    }

    return json({ error: "unknown_event" }, 400);
  },
};

على Node.js، تحقق بالطريقة نفسها باستخدام createHmac وtimingSafeEqual، على الجسم الخام الذي استقبله إطار العمل لديك:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyAutopseo(rawBody, headers, secret) {
  const timestamp = String(headers["x-autopseo-timestamp"] || "");
  const given = String(headers["x-autopseo-signature"] || "");
  if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = "sha256=" + createHmac("sha256", secret).update(timestamp + ".").update(rawBody).digest("hex");
  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

الخطوة التالية: ما يصل إلى الـ webhook تحدده الكتابة والمراجعة والصيانة؛ والتدفق الكامل في نظرة عامة على محرك المحتوى.

شغّل محتوى SEO الخاص بك بشكل تلقائي

اربط Search Console وموقعك. يفحصه AutoPSEO ويخطط للمواضيع الأولى فورًا. لا حاجة إلى بطاقة ائتمان.