دليل
اربط أي موقع عبر Webhook
لا تستخدم WordPress? لا مشكلة. يرسل BlogTend كل مقال مكتمل عبر POST بصيغة JSON موقّعة إلى نقطة نهاية تتحكم فيها — أي حزمة تقنية, وأي نظام لإدارة المحتوى, ونحو عشرين سطرًا من التعليمات البرمجية من جانبك.
ما هو موصل Webhook
عند اكتمال توليد مقال — أو حلول موعده المجدول — يرسله BlogTend إلى نقطة النهاية لديك في طلب POST واحد بصيغة JSON: العنوان، ومحتوى HTML الكامل، والمقتطف، والبيانات الوصفية لتحسين محركات البحث، وأسماء التصنيفات والوسوم، واللغة، والمصادر، وعنوان URL عام للصورة البارزة. تخزّنه نقطة النهاية لديك بالطريقة التي يخزّن بها موقعك المحتوى، ويعمل كل ما عدا ذلك في BlogTend تمامًا كما يعمل مع مواقع WordPress: عمليات الأتمتة، والجدولة، والمسودات، ولوحة التحكم، والفهرسة في Google.
يُوقَّع كل طلب بسر لا يعرفه إلا أنت وBlogTend، لتتمكن نقطة النهاية لديك من التحقق من أن المقال جاء منا بالفعل ولم يُعبث به.
ما يجب أن تفعله نقطة النهاية لديك
- استقبل طلب POST عبر HTTPS بمتن بتنسيق JSON على عنوان URL متاح للعامة.
- أرسل استجابة خلال 15 ثانية برمز حالة 2xx. نفّذ المهام البطيئة (تنزيل الصور, إعادة بناء ذاكرة التخزين المؤقت) بعد إرسال الاستجابة, لا قبلها.
- أجب عن كل طلب اختبار اتصال برمز 2xx. لا يحمل طلب اختبار الاتصال أي مقال ويجب ألا يؤدي إلى كتابة أي شيء، لذا يمكنك الإجابة عنه قبل التحقق منه (أو دون التحقق منه): هذا ما يتيح نجاح الاختبار & الاتصال بينما لا يزال نشر مفتاحك السري جاريًا.
- تحقّق من X-BlogTend-Signature مع كل حدث متعلق بمقال (الشيفرة أدناه)، وأرسل استجابة 401 عندما لا يتطابق. لا نفرض هذا التحقق، لكن من دونه يمكن لأي شخص يعثر على عنوان URL إرسال مقالات مزيفة إلى موقعك.
- استخدم article.id كمفتاح لضمان عدم تكرار العملية: إذا كان هناك منشور بهذا المعرّف بالفعل, فحدّثه بدلاً من إنشاء نسخة مكررة.
- عند رفض طلب، وضّح السبب في متن الاستجابة (مثلاً {"error": "توقيع غير صالح"}). نعرض أول 300 حرف في سجل التسليم.
إعداد الاتصال
- انشر نقطة نهاية الاستقبال لديك (المثال في الأسفل مكتمل).
- في BlogTend, انتقل إلى المواقع → ربط موقع → Webhook, وأدخل اسمًا وعنوان URL لنقطة النهاية, ثم اضغط على التالي.
- نعرض مفتاح التوقيع السري whsec_…. انسخه إلى بيئة خادمك الآن وانشر التطبيق: هذه الشاشة هي المكان الوحيد الذي يُعرض فيه.
- اضغط على اختبار & اتصال. نرسل طلب تحقق موقّعًا بذلك المفتاح السري; وتُحفظ بيانات الاتصال عند تلقي استجابة 2xx. إذا رفضت نقطة النهاية الطلب, نعرض رمز حالتها ورسالة الخطأ الصادرة عنها لتتمكن من إصلاح المشكلة والضغط على اختبار & اتصال مجددًا.
إعادة ربط عنوان URL نفسه (لتغيير اسم الموقع, أو بعد حدوث خطأ) لا تغيّر المفتاح السري مطلقًا: يعرض مربع الحوار المفتاح الحالي ويُوقَّع طلب الاختبار به. للحصول على مفتاح سري جديد, جدّده.
الترويسات المرسلة
يتضمن كل طلب, سواء كان لاختبار الاتصال أو لإرسال مقال, هذه الترويسات:
| الترويسة | القيمة | ملاحظات |
|---|---|---|
| X-BlogTend-Signature | t=<unix seconds>,v1=<hex>[,v1=<hex>] | توقيع HMAC-SHA256. توقيع v1 واحد لكل مفتاح سري نشط: توقيعان فقط خلال مهلة السماح البالغة 24 ساعة عند تبديل المفاتيح, والأحدث أولاً. |
| X-BlogTend-Event | ping | article.published | article.updated | مطابق لحقل event في متن الطلب. |
| X-BlogTend-Delivery | whd_… | يطابق delivery_id في جسم الطلب. يتجدد في كل محاولة, بما في ذلك إعادة المحاولة. |
| X-BlogTend-Test | 1 | فقط عند إرسال طلبات التحقق (اختبار & اتصال, إعادة التحقق, تحديث الفئات). لا يتضمن طلب التحقق أي مقال ويجب ألا يكتب أي شيء. |
| Content-Type | application/json | متن الطلب بصيغة JSON بترميز UTF-8. |
| User-Agent | BlogTend-Webhook/1 | كان الاسم YoDon-Webhook/1 قبل تغييره. |
| X-Yodon-Signature, X-Yodon-Event, X-Yodon-Delivery | deprecated | تُرسل إلى الأنظمة المستقبِلة التي بُنيت قبل تغيير الاسم, بالقيم نفسها, باستثناء أن X-Yodon-Signature يحتوي دائمًا على توقيع v1 واحد فقط, يُنشأ باستخدام أحدث مفتاح سري. ينبغي للأنظمة المستقبِلة الجديدة قراءة ترويسات X-BlogTend-*. |
ترويسات X-Yodon-* مهملة. تظل تعمل مع الأنظمة المستقبِلة التي بُنيت قبل تغيير الاسم, لكن X-BlogTend-Signature وحده يحمل كلا التوقيعين أثناء تدوير المفتاح السري, لذا انتقل إلى الأسماء الجديدة عند إجراء التعديل التالي على نظامك المستقبِل.
البيانات المرسلة, حقلاً بحقل
تصل ثلاثة أنواع من الأحداث، يمكن تمييزها من حقل event وترويسة X-BlogTend-Event: ping (اختبار الاتصال، بلا مقال)، article.published (تسليم المقال لأول مرة) وarticle.updated (المقال نفسه مجددًا: بعد إعادة محاولة أجريتها، أو نشر مسودة، أو تحديث منشور منشور بالفعل، وفي هذه الحالة يحدد url المنشور المنشور بالفعل المراد استبداله ويكون id هو المعرّف الذي خزّنته له). البنية الكاملة:
{
"event": "article.published",
"delivery_id": "whd_5f0c9c1e-…",
"site_id": "d2a41c3e-…",
"sent_at": "2026-08-28T15:04:05.000Z",
"article": {
"id": "a81f6a02-…",
"title": "How to Choose a Standing Desk",
"slug": "how-to-choose-a-standing-desk",
"status": "publish",
"html": "<h2>…</h2><p>…</p>",
"excerpt": "A practical buyer's guide…",
"seo": {
"title": "Standing Desk Buyer's Guide (2026)",
"description": "Everything to check before…",
"keyword": "standing desk"
},
"category": "Office Setup",
"categories": ["Office Setup", "Buying Guides"],
"tags": ["desks", "ergonomics"],
"language": "en",
"featured_image": {
"url": "https://www.blogtend.com/api/images/…?sig=…",
"mime": "image/webp"
},
"sources": [{ "title": "OSHA guidance", "url": "https://…" }],
"published_at": "2026-08-28T15:04:05.000Z",
"updated_at": "2026-08-28T15:04:05.000Z",
"url": "https://example.com/blog/how-to-choose-a-standing-desk"
}
}- article.status هي الحالة التي نطلبها: "publish" أو "draft". تصل المقالات المجدولة في موعدها المحدد بالحالة "publish".
- article.html هو محتوى المقال الكامل. تشير مراجع الصور داخله بالفعل إلى عناوين URL متاحة للجميع.
- category و categories أسماء وليست معرّفات — اربطها بنظام التصنيف الخاص بك, أو تجاهلها.
- تكون قيمة featured_image هي null عندما لا يحتوي المقال على صورة. الرابط موقّع وثابت — يمكنك جلبه في أي وقت.
- يُعرّف delivery_id هذه المحاولة وحدها ويتغير مع كل إعادة محاولة; أما article.id فيُعرّف المقال ولا يتغير أبدًا. امنع التكرار باستخدام article.id
- يحتوي متن طلب اختبار الاتصال على event, delivery_id, site_id و sent_at فقط; ولا يتضمن مقالًا.
التحقق من التوقيع
يحمل كل طلب X-BlogTend-Signature: t=<unix seconds>,v1=<hex>. كل قيمة v1 هي ناتج HMAC-SHA256 للسلسلة "<t>.<raw body>" باستخدام سر whsec_ كمفتاح. تحقّق باستخدام جسم الطلب الخام، قبل أي تحليل لـ JSON، وارفض الطوابع الزمنية التي تختلف عن الوقت الحالي بأكثر من 5 دقائق، وقارن باستخدام دالة آمنة من هجمات التوقيت، واقبل الطلب إذا تطابقت أي قيمة v1 مع السر لديك: أثناء تدوير السر توجد قيمتان.
Node.js
import crypto from "crypto";
// header: the X-BlogTend-Signature value, e.g. "t=1767225600,v1=ab12…,v1=cd34…".
// During a secret rotation it holds one v1 per live secret: accept if ANY matches.
export function verifyBlogTendSignature(secret, header, rawBody) {
const parts = (header || "").split(",").map((p) => p.trim());
const t = parts.find((p) => p.startsWith("t="))?.slice(2);
// Reject anything older than 5 minutes: replay protection.
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = Buffer.from(
crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex")
);
return parts
.filter((p) => p.startsWith("v1="))
.map((p) => Buffer.from(p.slice(3)))
.some((sig) => sig.length === expected.length && crypto.timingSafeEqual(sig, expected));
}PHP
function verify_blogtend_signature(string $secret, string $header, string $rawBody): bool {
$t = null;
$sigs = [];
foreach (explode(',', $header) as $part) {
[$k, $v] = array_pad(explode('=', trim($part), 2), 2, '');
if ($k === 't') $t = $v;
if ($k === 'v1') $sigs[] = $v; // one per live secret
}
if (!ctype_digit((string) $t) || abs(time() - (int) $t) > 300) return false; // replay window
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
foreach ($sigs as $sig) {
if (hash_equals($expected, $sig)) return true; // timing-safe
}
return false;
}Python
import hashlib, hmac, time
def verify_blogtend_signature(secret: str, header: str, raw_body: bytes) -> bool:
parts = [p.strip().split("=", 1) for p in (header or "").split(",") if "=" in p]
t = next((v for k, v in parts if k == "t"), "")
if not t.isdigit() or abs(time.time() - int(t)) > 300: # replay window
return False
expected = hmac.new(
secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256
).hexdigest()
# One v1 per live secret during a rotation: accept if any matches.
return any(hmac.compare_digest(expected, v) for k, v in parts if k == "v1")يستمر المستقبِل القديم الذي يقرأ X-Yodon-Signature بنمط يطابق قيمة v1 واحدة فقط في العمل: فلا تزال هذه الترويسة تحتوي على قيمة v1 واحدة بالضبط، مُنشأة باستخدام أحدث مفتاح سري.
تجديد المفتاح السري
هل فقدت المفتاح السري، أو تستبدله دوريًا؟ في صفحة المواقع، افتح بطاقة موقع Webhook واضغط على تجديد المفتاح السري. يُعرض المفتاح السري الجديد مرة واحدة.
- خلال الساعات الـ 24 القادمة يحمل كل طلب توقيعين في X-BlogTend-Signature: أحدهما مُنشأ بالمفتاح السري الجديد, والآخر بالقديم. يستطيع المستقبِل الذي يملك أيًا من المفتاحين التحقق من التوقيع, فلا يحدث أي إخفاق أثناء تحديث خادمك.
- بعد 24 ساعة يُستخدم المفتاح السري الجديد فقط للتوقيع. تعرض البطاقة موعد توقف استخدام المفتاح القديم.
- إذا تسرّب المفتاح السري القديم، فحدّد خيار "أوقفه الآن" عند استبداله: لا توجد فترة سماح، وتفشل عمليات التسليم بالرمز 401 حتى يُزوَّد خادمك بالمفتاح السري الجديد.
- الجهات المستقبِلة التي لا تزال تقرأ الترويسة المهملة X-Yodon-Signature لا ترى سوى توقيع المفتاح السري الجديد, لذا يسري التبديل فورًا بالنسبة إليها.
الإجابة المطلوبة
أي رمز 2xx يُعد تأكيدًا للتسليم; محتوى الاستجابة اختياري وقد يكون فارغًا. أرسل استجابة بصيغة JSON ليصبح BlogTend أكثر ذكاءً:
{ "ok": true, "id": "123", "url": "https://example.com/blog/my-post" }- url — يُخزَّن بوصفه رابط المقال المنشور: يفتحه زر عرض في لوحة التحكم, ويُرسَل إلى Google للفهرسة.
- id — معرّف منشورك، يُخزّن لتتمكن أيضًا من مطابقة أحداث article.updated المستقبلية من جانبك.
- held — سبب موجز عندما تقبل المقال لكن لا تنشره على موقعك (قائمة انتظار تحريرية, قاعدة داخلية). يعرضه BlogTend حينها كمسودة مع ذلك السبب بدلاً من منشور منشور, ولا يرسل رسالة بريد إلكتروني تفيد بأنه "منشور". الرد بالقيمة status: "draft" يحقق النتيجة نفسها دون ذكر سبب.
لطلب التحقق من الاتصال, يكفي أيضًا أي رمز 2xx. الحقل الوحيد الذي نقرأه من استجابة طلب التحقق من الاتصال هو categories: أرسل استجابة بالشكل { ok: true, categories: ["Guides", "News"] } (مصفوفة تضم حتى 100 اسم) وستظهر هذه الأسماء كخيارات تصنيفات فعلية في نماذج الإنشاء لدى BlogTend. تُحدَّث عند كل إعادة تحقق وعند الضغط على زر التحديث في أداة اختيار التصنيفات. إذا حذفت الحقل, فسيقترح الذكاء الاصطناعي أسماء تصنيفات بدلًا من ذلك.
حالات الفشل, وإعادة المحاولة والنسخ المكررة
كيف يقرأ BlogTend كل استجابة لتسليم مقال:
| إجابتك | ما يعنيه لنا | هل أُعيد الإرسال? |
|---|---|---|
| 2xx | تم التسليم. محتوى الاستجابة اختياري; ويُقرأ المحتوى بتنسيق JSON كما هو موضح في قسم كيفية الرد. | تم. |
| 3xx | فشل. لا يتم اتباع عمليات إعادة التوجيه: اربط عنوان URL النهائي بدلاً من ذلك. | لم تتم إعادة الإرسال فورًا. |
| 401 / 403 | فشل: يظهر بالرسالة "رُفض التوقيع". المفتاح السري على خادمك غير مطابق. ألصقه مجددًا أو استبدله بمفتاح جديد. | لم تتم إعادة الإرسال فورًا. |
| 503 | فشل: يظهر بالرسالة "لم يتم الإعداد بعد", وهي الاستجابة المعتادة من جهة استقبال لم يُضبط مفتاحها السري. | أُعيد الإرسال مرة واحدة, على الفور. |
| أخطاء 4xx الأخرى | فشل: فهمت الطلب ورفضته. | لم تتم إعادة الإرسال فورًا. |
| أخطاء 5xx الأخرى | فشل: تعطلت نقطة النهاية لديك أو أنها غير متاحة. | أُعيد الإرسال مرة واحدة, على الفور. |
| عدم تلقي استجابة خلال 15 ثانية, أو حدوث خطأ في الشبكة | فشل: يظهر بالرسالة "تعذر الوصول إلى نقطة النهاية لديك". | أُعيد الإرسال مرة واحدة, على الفور. |
- المقالات التي ينشرها BlogTend تلقائيا (عمليات النشر الآلي والمنشورات المجدولة): إذا استمر فشل التسليم، تُعاد محاولة إرسال المقال بالكامل في عمليتي التشغيل التاليتين، بفاصل دقيقة تقريبا، أيا كانت الاستجابة. بعد المحاولة الفاشلة الثالثة يتوقف BlogTend، ويحتفظ بالمقال المكتمل، ويرسل إليك السبب بالبريد الإلكتروني. انشره مجددا بعد إصلاح نقطة النهاية لديك.
- المقالات التي تنشرها يدويًا: لا تُعاد المحاولة تلقائيًا. يظهر السبب على المقال فورًا; اضغط على نشر مجددًا, أو على إعادة المحاولة في عمليات التسليم الأخيرة.
- لن يضيع أي مقال: إذا فشل نشره، يبقى في BlogTend مع ذكر السبب، جاهزًا للنشر مجددًا.
- أين تجد التفاصيل: في صفحة المواقع، تتضمن بطاقة موقع Webhook قسم عمليات التسليم الأخيرة. تعرض كل عملية رمز الحالة، ومعناه المعتاد، وأول 300 حرف من رسالة الخطأ؛ وتحتوي العمليات الفاشلة على زر إعادة المحاولة. يُحفظ أول كيلوبايت من كل استجابة.
- بسبب إعادة المحاولات، قد تستقبل نقطة النهاية لديك المقال نفسه مرتين، مع delivery_id جديد في كل مرة. لهذا من المهم إدراج المقال أو تحديثه بناءً على article.id.
مثال كامل على جهة استقبال
مسار في Next.js App Router يتحقق من البيانات ويزيل التكرار ويخزنها. استبدل savePost بآلية التخزين الخاصة بك ليصبح جاهزًا للاستخدام الفعلي:
// app/api/articles/webhook/route.ts — a complete Next.js receiver
import { NextResponse } from "next/server";
import { verifyBlogTendSignature } from "./verify"; // the Node.js function above
const SECRET = process.env.BLOGTEND_WEBHOOK_SECRET; // the whsec_… shown when you connected
export async function POST(req: Request) {
const body = await req.text(); // raw body — verify BEFORE parsing
let payload;
try {
payload = JSON.parse(body);
} catch {
return NextResponse.json({ error: "invalid JSON" }, { status: 400 });
}
// A ping carries no article and writes nothing, so it is answered without
// verifying: Test & connect then passes even before SECRET is deployed.
// Return nothing private here.
if (payload.event === "ping") {
// Optional: advertise your categories so BlogTend's forms can offer them.
return NextResponse.json({ ok: true, categories: await listMyCategories() });
}
// Every content event must be verified. Clear messages here show up in
// BlogTend's delivery log, so "wrong secret" never looks like "down".
if (!SECRET) {
return NextResponse.json({ error: "receiver not configured: BLOGTEND_WEBHOOK_SECRET is not set" }, { status: 503 });
}
const header =
req.headers.get("x-blogtend-signature") ?? req.headers.get("x-yodon-signature") ?? "";
if (!verifyBlogTendSignature(SECRET, header, body)) {
return NextResponse.json({ error: "invalid signature: check BLOGTEND_WEBHOOK_SECRET" }, { status: 401 });
}
const a = payload.article;
// Upsert by a.id — retries and updates must not create duplicates.
const post = await savePost({
externalId: a.id,
slug: a.slug,
title: a.title,
html: a.html,
excerpt: a.excerpt,
metaTitle: a.seo.title,
metaDescription: a.seo.description,
tags: a.tags,
category: a.category,
imageUrl: a.featured_image?.url ?? null,
published: a.status === "publish",
});
// Answer with the live URL so the BlogTend dashboard links to the post.
return NextResponse.json({ ok: true, id: post.id, url: post.url });
}يمكن نقل المنطق نفسه إلى أي إطار عمل في بضع دقائق — وتغطي الأسئلة الشائعة أدناه الاستفسارات المعتادة حول المنصات.
الأسئلة
ما المنصات التي يتوافق معها هذا؟
فقدت مفتاح التوقيع السري. أين أجده مجددًا؟
هل تعمل المقالات المجدولة?
ماذا عن المسودات?
كيف تصل الصور؟
هل يُعاد إرسال البيانات؟ هل سأرى نسخًا مكررة؟
هل تُنقل البيانات الوصفية الخاصة بتحسين محركات البحث?
هل تنشر على WordPress بدلًا من ذلك? اقرأ دليل WordPress
تقنياتك, مقالاتنا
اربط نقطة نهاية مرة واحدة، ليصل كل مقال — سواء أُنشئ يدويًا أو ضمن دفعة أو تلقائيًا — مباشرةً إلى موقعك.