تخطّ إلى المحتوى
كيف بُني هذا الموقع

المشروع ٠٩ · النظام الذي خلف هذا الموقع

كيف بنيت هذا الموقع، والنظام الذي يدير العمل خلفه.

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

هذه جولة كاملة: الكلام البسيط أولاً، ثم مخططات تستطيع استكشافها، والكود الحقيقي خلف كل جزء.

خدمات
٥
قاعدة بيانات، ٣٠ جدولاً
١
اختبار آلي
١٬٢١٢
تشغيلاً مجدولاً يومياً
٢٨٨
شاشة في اللوحة
١٤
هجرة لقاعدة البيانات
٣١
لغتان لكل صفحة
٢
زاحفاً للذكاء الاصطناعي
٢٧

بقلم ماجد الأندجاني · آخر تحديث ٢ نوفمبر ٢٠٢٦

في هذه الصفحة

الفكرة

بدأ صفحةً تعرّف بي، وصار النظام الذي أجرّب عليه كل شيء.

في يوليو ٢٠٢٦ كان هذا الموقع صفحة واحدة: من أنا، وماذا أعمل، وأين عملت. ثم احتجت مكاناً أستقبل فيه طلبات العملاء، فأضفت متجراً للخدمات وحجزاً للمواعيد. ثم احتجت أن أرسل عروض أسعار، فبنيتها. ثم العقود، والفواتير، والدفع الإلكتروني، والبريد، والمدونة. ومع كل حاجة حقيقية كبر الموقع، حتى صار نظاماً صغيراً يدير عملاً كاملاً.

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

أتعلّمأطبّق هنا أولاًأقيسأقرّرأنقل للشركاتأرفض، مع السبب
اختر أي مربع لترى ما يفعله وبماذا يرتبط. اسحب جانبياً لترى المخطط كاملاً.

أقيس

السرعة في متصفحات الزوار الحقيقيين، والأخطاء، ونتائج الاختبارات، وهل وفّرت وقتاً فعلاً. الانطباع ليس قياساً.

هذه الصفحة جولة كاملة: ماذا يفعل النظام، وكيف تتحدث أجزاؤه مع بعضها، والكود خلف الأجزاء التي أعتز بها. كل قسم يبدأ بكلام بسيط، ومن يريد التفاصيل الهندسية يفتح «التفاصيل التقنية»، أو يفتحها كلها مرة واحدة من القائمة.

صناديق الكود مقتطفات حقيقية من النظام، مختصرة، وغيّرت فيها الأسماء حيث تكشف شيئاً خاصاً. وتركت خارج الصفحة عمداً طريقة حماية اللوحة الخاصة، وكل مفتاح وعنوان وبيانات أي عميل.

المعمارية

خمس خدمات، وقاعدة بيانات واحدة، وكل ما تتصل به.

يعمل النظام على شبكة Cloudflare كخمس خدمات صغيرة: الموقع، والمتجر، والمدونة، والواجهة البرمجية، ولوحة التشغيل الخاصة بي. لكل خدمة مهمة واحدة وتُنشر وحدها، وكلها تقرأ وتكتب في قاعدة بيانات واحدة، فلا توجد إلا نسخة واحدة من الحقيقة.

روابط خاصةالزوارالعملاءأناالموقعNext.js، صفحات ثابتةالمتجرAstro، ثابت مع طبقة حيةالمدونةHono، من قاعدة البياناتلوحة التشغيلHono، خاصةمحركات البحث والذكاء الاصطناعيخريطة الموقع، llms.txtالواجهة البرمجيةHono، جدولة، بريدقاعدة واحدةD1، ٣٠ جدولاًTapالمدفوعاتالبريدوارد وصادرGoogleالتقويم، بيانات البحثLinkedInمشاركة المقالاتGitHub Actionsاختبار، نشر، نسخ
اختر أي مربع لترى ما يفعله وبماذا يرتبط. اسحب جانبياً لترى المخطط كاملاً.

قاعدة بيانات واحدة

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

التفاصيل التقنية

Cloudflare D1 (SQLite): ٣٠ جدولاً، و٥٥ فهرساً، و٧ قيود تلقائية (triggers)، بُنيت عبر ٣١ هجرة مرتبة. ستة منها تجعل جداول الأدلة للإضافة فقط، وواحد يقفل العقود الموقعة.

مسارات الواجهة البرمجية
٢٢
مسارات اللوحة
١٢٤
وحدات مشتركة
٢٠
جداول قاعدة البيانات
٣٠
هجرات مرتبة
٣١
قيود حماية تلقائية
٧
التفاصيل التقنية

عشرون وحدة TypeScript مشتركة تحمل كل قاعدة يجب أن تتفق عليها الواجهة البرمجية واللوحة: ختم المستندات، والأرقام المرجعية القصيرة، وتوقيت الرياض، وتعارض المواعيد، وطابور البريد، وعميل الدفع. كل قاعدة مكتوبة مرة واحدة، فلا تختلف الخدمتان أبداً.

وتوجد قاعدة بيانات ثانية فيها الهيكل فقط، لغرض واحد: كل هجرة تُجرَّب عليها قبل أن تلمس قاعدة الإنتاج.

حالات الاستخدام

خمسة أنواع من المستخدمين، وما يستطيع كل منهم فعله.

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

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

أنا

أدير البيع، والتنفيذ، والبريد، والمحتوى، والأرقام من لوحة واحدة، على الكمبيوتر أو الجوال.

المسار التجاري

من أول طلب، إلى عقد موقّع، إلى فاتورة مدفوعة.

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

يصل طلب

يختار الزائر خدمة من المتجر ويرسل طلباً أو يحجز موعداً. يُنشأ الطلب، ويصل العميل إشعار بالاستلام، ويظهر تنبيه على جوالي.

التفاصيل التقنية

دفعة ذرية واحدة تكتب العميل والطلب ورقمه المرجعي القصير والرسائل المنتظرة. والنموذج يرسل مفتاحاً لكل محاولة، فإعادة الإرسال بعد ضياع الرد تعيد الطلب نفسه ولا تنشئ طلباً ثانياً.

// One key per booking attempt, created when the form opens and sent with
// every retry. A retry after a lost answer gets the same order back, never a
// second one. The key is retired only when the attempt is really over.
const res = await fetch(`${apiBase}/api/orders`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ ...body, attemptKey }),
});
const j: any = await res.json().catch(() => ({}));

// Nothing was charged, so the next press is a new attempt, not a retry.
if (j.error === 'charge_create_failed' || j.error === 'attempt_closed') attemptKey = '';
if (!res.ok) throw new Error(j.error || 'error');

// A paid product answers with the payment page, also on a retry.
if (j.redirectUrl) { location.assign(j.redirectUrl); return; }

msgEl.textContent = en()
  ? 'Request received. I will contact you within one business day.'
  : 'تم استلام طلبك. سأتواصل معك خلال يوم عمل واحد.';
form.reset();
attemptKey = ''; // the next submission is a genuinely new booking

١ / ٨

المال

مدفوعات مصممة لليوم السيئ.

مع أموال العملاء لا يكفي أن تعمل الدفعة. كل مسار يلمس المال صُمم لليوم السيئ: الرد الذي يضيع، والضغطة المزدوجة، والرسالة المكررة، والعميل الذي يدفع مرتين.

  • عملية الدفع تُنشأ لحظة الضغط. صفحة الدفع في Tap تنتهي بعد ٣٠ دقيقة، لذلك لا يُنشأ شيء عند إرسال الرابط بالبريد. الصفحة تُنشأ حين يضغط العميل زر الدفع، فتكون صالحة دائماً.

  • التحقق أولاً، ثم الكتابة. كل رسالة دفع من Tap يُتحقق منها قبل تسجيل أي شيء، ثم تُقرأ عملية الدفع من Tap من جديد. ولا يغيّر حالة أي مبلغ إلا جواب Tap نفسه.

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

  • الاسترداد يُكتب قبل أن يُطلب. إذا ضاع الرد، يبقى السجل شاهداً أن استرداداً ربما تم، ومفتاح إعادة المحاولة مرتبط بذلك السجل، فلا يمكن أن تتحول الإعادة إلى استرداد ثانٍ.

  • تنبيه واضح واحد لأي حالة غير معتادة. دفع مزدوج، أو دفع بالبطاقة بعد تحويل بنكي، أو مبلغ تجريبي على طلب حقيقي، أو مبلغ تغيّر: كل حالة تُنتج تنبيهاً واحداً فقط يخبرني ماذا أراجع.

  • طريقة بيع لكل منتج. كل منتج يُباع بطلب عرض سعر، أو بحجز موعد، أو بدفع مباشر، وأستطيع تغيير ذلك من اللوحة دون إعادة بناء المتجر.

  • التشغيل يدوي، بعد فحص حقيقي. الدفع الإلكتروني لا يُشغَّل إلا مني، من اللوحة، والزر يجري فحصاً حقيقياً لدى Tap قبل أن يقبل.

أول دفعة كاملة تمت بـ Apple Pay في بيئة Tap التجريبية: اكتملت الدفعة، وتحقق النظام من توقيعها، ووصلت رسالتا البريد.

// Verify first, write second. The duplicate check is built from fields
// inside the message, so recording an unverified message would let a forged
// one block the real one.
if (!(await verifyWebhookSignature(tap, payload, received))) {
  return json(c, { error: "bad_signature" }, 401);
}

const dedupeKey = await sha256Hex(`${payload?.id}|${payload?.status}|${payload?.transaction?.created}`);

// The same event twice is one event.
const ins = await env.DB.prepare(
  "INSERT OR IGNORE INTO webhook_events (tap_charge_id, tap_status, dedupe_key, signature_valid, processing_status) VALUES (?, ?, ?, 1, 'verified')",
).bind(String(payload?.id || ""), String(payload?.status || ""), dedupeKey).run();
if (ins.meta.changes === 0) return json(c, { ok: true, duplicate: true });

// A signature proves who sent the message, not that its body is true. The
// charge is read again from Tap, and only that answer moves any money state.
const charge = await retrieveCharge(tap, String(payload.id));
await reconcile(env, charge);
التفاصيل التقنية

حالات التسابق تُختبر ولا يُراهن عليها. مختبر صممته يعيد تمثيل عميلين يتصرفان في اللحظة نفسها، وخادم يتوقف في منتصف الطلب، بطريقة قابلة للتكرار. يشغّل المعالجات الحقيقية جملة بعد جملة على قاعدة البيانات، أمام نسخة محاكية من Tap تلتزم بحالاته المنشورة، ويتحقق من ١٤ قاعدة يجب أن تصمد دائماً، مثل ألا يُدفع طلب مرتين، وألا يوجد مال بلا سجل.

المواعيد

محرك حجز مملوك: شخصان، وموعد واحد، ولا حجز مزدوج.

بدل أداة حجز على منصة شخص آخر، للنظام محركه الخاص. أحدد ساعات العمل الأسبوعية، وأغلق تواريخ بعينها، وأحجز وقتاً لنفسي. والزائر لا يرى إلا المواعيد المتاحة فعلاً، حتى ١٤ يوماً للأمام.

حين يضغط شخصان زر الحجز على وقتين متداخلين في اللحظة نفسها، يُكتب الطلبان، ثم يعيد كل منهما النظر، ويحتفظ الأسبق بالموعد. أما الأحدث فيلغي نفسه ويطلب من الزائر اختيار موعد آخر. والحجز المدفوع يحتفظ بموعده أثناء الدفع، ويتحرر الموعد وحده إن لم يصل الدفع.

تأكيد الطلب يُنشئ رابط اجتماع مرئي خاصاً به، ويرسله للعميل بالبريد، وينسخ الموعد إلى تقويم Google. ويصل تذكير قبل الموعد بيوم. وأستطيع أيضاً ربط تقويم جوالي برابط خاص يعمل دون Google أصلاً.

// Two people press "book" on overlapping times in the same instant. Both
// rows are written; then each request re-reads the window, and the older
// claim keeps the time. The newer one cancels itself and tells the visitor.
export function slotVerdict(
  mineId: number,
  startsAt: string,
  hours: number,
  windowRows: SlotRow[],
): { overlapping: SlotRow[]; mustYield: boolean } {
  const startMs = new Date(startsAt).getTime();
  const endMs = endOf(startsAt, hours);
  const overlapping = windowRows.filter((r) =>
    r.id !== mineId && collides(startMs, endMs, new Date(r.starts_at).getTime(), endOf(r.starts_at, r.hours)),
  );
  // If we cannot identify our own row, we do not yield: destroying a real
  // booking on an uncertain fact is worse than an overlap I can see and fix.
  const mustYield = mineId > 0 && overlapping.some((r) => mineId > r.id);
  return { overlapping, mustYield };
}
التفاصيل التقنية

فهرس فريد على وقت البداية يرفض البداية المطابقة فوراً، وقاعدة التداخل في وحدة مشتركة تستخدمها الواجهة البرمجية واللوحة معاً. وأي طلب لم أؤكده خلال ٣٦ ساعة يتحرر مع رسالة اعتذار، فلا يبقى موعد محجوزاً بطلب لم يرد عليه أحد.

رسالة العميل تخرج قبل نسخة التقويم. فـ Google إضافة لطيفة، ولا دخل له في وصول التأكيد للعميل.

الأتمتة

كل خمس دقائق تعمل ثلاث عشرة مهمة، وكل واحدة آمنة عند التكرار.

مهمة مجدولة تعمل كل خمس دقائق، ٢٨٨ مرة في اليوم. تطابق المدفوعات، وتحرر الحجوزات المنتهية، وتعيد إرسال البريد المتعثر، وتصدر فواتير الاشتراكات الشهرية، وتفحص النظام كله، وتُنهي الطلبات التي لم يُرد عليها، وترسل التذكيرات، وتجهز ملخصي الصباحي، وتعيد التحقق من توقيعات العقود، وتنظف السجلات القديمة، وتنشر المقال التالي من قائمة المدونة.

العمل ٢٨٨ مرة في اليوم يعني أن كل مهمة يجب أن تكون آمنة عند التكرار. التذكير يُحجز في قاعدة البيانات قبل إرساله، فيُرسل مرة واحدة مهما حدث. وأول ما تفعله المهمة في كل تشغيل أن تسجل أنها عملت، فإن توقفت يوماً ظهر تحذير أعلى كل شاشة في لوحتي، لأن المهمة لا تستطيع أن تبلغ عن توقفها بنفسها.

// Every five minutes, 288 times a day. Each step is isolated: one failing
// step is logged and the rest still run. The order is deliberate: money is
// settled before holds are released, and housekeeping always comes last.
async scheduled(_event: ScheduledController, env: Env, _ctx: ExecutionContext) {
  const step = <T>(name: string, work: () => Promise<T>) => work().catch((e) => {
    console.error(`cron: ${name}:`, String(e?.message || e));
    return null;
  });
  const lastTick = await step("heartbeat", () => heartbeat(env));
  await step("reconcile", () => reconcilePending(env, lastTick));
  await step("booking holds", () => expireUnpaidBookings(env));
  await step("mail retry", () => retryWhenDue(env));
  await step("retainer billing", () => generateRetainerInvoices(env));
  await step("estate check", () => monitorEstate(env));
  await step("request expiry", () => expireStaleBookings(env));
  // Claimed once per row, so 288 runs a day still send one reminder.
  await step("appointment reminders", () => sendAppointmentReminders(env));
  await step("digest", () => sendReminderDigest(env));
  await step("contract reminders", () => remindExpiringContracts(env));
  // Before housekeeping: a signature that stopped proving anything cannot wait.
  await step("signature sweep", () => sweepSignatures(env));
  await step("retention", () => purgeOperationalLogs(env));
  await step("blog queue", () => publishFromQueue(env));
}
التفاصيل التقنية

البريد المتعثر يُعاد كل ساعة: البريد العادي حتى ثلاث محاولات، وبريد المال طوال يوم كامل. وفحص النظام يختبر الموقع العام واللوحة، وينبّه إلى طابور بريد عالق أو رسائل دفع استنفدت محاولاتها، ويراسلني مرة واحدة لكل مجموعة جديدة من المشكلات، لا كل خمس دقائق.

ومراقب خارجي على GitHub يفحص المتجر والمدونة والواجهة البرمجية كل ٣٠ دقيقة، لأن الـ Worker لا يستطيع مراقبة جيرانه بدقة من داخل النطاق نفسه.

المكتب الخلفي

لوحة واحدة بدل عشر أدوات.

أغلب الأعمال الصغيرة تتنقل بين بريد، وأداة لإدارة العملاء، وأداة توقيع، وأداة فواتير، وتقويم، ولوحة تحليلات. أردت ذلك كله في مكان واحد، مرتباً بالترتيب الذي يحدث به العمل: البيع، ثم التنفيذ، ثم المحتوى، ثم النظام نفسه. على الكمبيوتر تظهر الشاشات في شريط جانبي، وعلى الجوال تفتح من الأسفل حيث يصل الإبهام.

رسم توضيحي للوحة التشغيل1 / 15

الطلبات

كل طلب بوصفه وحالته والمبلغ المتفق عليه، مع البحث، والتواصل بضغطة عبر واتساب أو الهاتف أو البريد.

  • كل تغيير في الحالة يرسل للعميل تحديثاً قصيراً، مرة لكل حالة
  • تسجيل التحويل البنكي بضغطة، والمبلغ الذي وصل مرتين يُنبَّه إليه
  • تحويل الطلب إلى مسودة عرض، أو إنشاء فاتورته
  • استرداد كامل أو جزئي، يُكتب قبل أن يتحرك أي مبلغ
التفاصيل التقنية

اللوحة صفحات HTML تُبنى على الخادم مباشرة من قاعدة البيانات، بلا إطار واجهات وبلا خط لتجميع الملفات: ٩٬٣١٠ أسطر TypeScript في ٢٧ ملفاً، و١٢٤ مساراً، ومكتبتان فقط وقت التشغيل. مكتوبة بالعربية أولاً، وزر واحد يحولها إلى الإنجليزية عبر قاموس من ٥٦١ مدخلاً. وكل رفض يعود إلى الشاشة التي كنت فيها برسالة مترجمة، لا بصفحة خطأ مجردة، وكل إجراء تقريباً يكتب سطراً في سجل التدقيق.

// The panel is written in Arabic and rendered on the server. One button
// walks the live page and swaps each text for its English entry (561 of
// them), flips the direction, and re-formats every date in Riyadh time.
function apply(lang){
  var en = lang === "en", html = document.documentElement;
  html.lang = lang; html.dir = en ? "ltr" : "rtl";
  document.querySelectorAll("[data-en]").forEach(function(el){
    if (el.dataset.ar === undefined) el.dataset.ar = (el.textContent || "");
    el.textContent = en ? (el.dataset.en || el.dataset.ar) : el.dataset.ar;
  });
  texts.forEach(function(p){
    if (!en) { p[0].nodeValue = p[1]; return; }
    var t = swap(p[1]); if (t !== null) p[0].nodeValue = t;
  });
  var fmt = tsFmt(en);
  if (fmt) document.querySelectorAll("time[datetime]").forEach(function(el){
    var iso = el.getAttribute("datetime"); if (!iso) return;
    try { el.textContent = fmt.format(new Date(iso)); } catch (e) {}
  });
  document.title = en ? (DICT[docTitle] || docTitle) : docTitle;
  btn.textContent = en ? "عربي" : "EN";
}

البريد

بريد لا يمكن أن يضيع، وصندوق بريد يعيش داخل اللوحة.

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

البريد المرسل إلى النطاق يصل إلى لوحتي، فيُقرأ ويُرتب في محادثات ويُحفظ قبل أي شيء آخر. والرد بالعربية مع الرسالة الأصلية بالإنجليزية يجتمعان في المحادثة نفسها. ويصل تنبيه إلى جوالي، وأرد من الشاشة نفسها.

// An email is first a row, written in the same batch as the fact it
// announces. If the order is marked paid, the receipt is already queued;
// if the batch fails, neither exists. The send comes after the commit.
export function mailClaim(env: { DB: D1Database }, msg: Mail, when?: { sql: string; bind: unknown[] }): D1PreparedStatement {
  return env.DB.prepare(
    `INSERT OR IGNORE INTO notifications_log (order_id, template_key, recipient, status, provider, idempotency_key, subject, body_text, attempts, claimed_at)
     SELECT ?, ?, ?, 'queued', 'resend', ?, ?, ?, 1, ? ${when ? `WHERE ${when.sql}` : ""}`,
  ).bind(msg.orderId, msg.templateKey, msg.to, msg.idem, msg.subject, msg.text, new Date().toISOString(), ...(when?.bind ?? []));
}

// The send behind a claim this caller won. A failed send is left to the
// hourly retry; it is never lost and never sent twice.
export async function sendClaimed(env: { DB: D1Database } & EmailEnv, msg: Mail): Promise<boolean> {
  const r = await sendEmail(env, { to: msg.to, subject: msg.subject, text: msg.text });
  if (r.ok) {
    await env.DB.prepare("UPDATE notifications_log SET status = 'sent', provider_message_id = ?, claimed_at = NULL WHERE idempotency_key = ?")
      .bind(r.id || null, msg.idem).run();
  } else {
    await env.DB.prepare("UPDATE notifications_log SET claimed_at = NULL WHERE idempotency_key = ?")
      .bind(msg.idem).run().catch(() => { /* the lease runs out by itself */ });
  }
  return r.ok;
}
التفاصيل التقنية

البريد الصادر يمر عبر Resend ويحمل معرّف رسالة خاصاً به يُحفظ، حتى تنتظم ردود العملاء في محادثاتها. والوارد يصل عبر Cloudflare Email Routing إلى معالج البريد في الواجهة البرمجية: تُقرأ الرسالة الخام، ويتحول البريد المكتوب بـ HTML فقط إلى نص مع الإبقاء على روابطه، ويُحدّ طول كل حقل قبل حفظه.

المحتوى

مدونة تنشر نفسها، وتشارك على LinkedIn بضغطة واحدة.

المدونة تقرأ المقالات مباشرة من قاعدة البيانات، فالنشر فوري ولا يحتاج إعادة بناء. أستطيع النشر يدوياً، أو وضع المقالات الجاهزة في قائمة انتظار: بإيقاع أحدده، وفي ساعة ثابتة بتوقيت الرياض، ينشر النظام المقال التالي ويبلّغ محركات البحث عنه عبر IndexNow.

حين يذكر مقالٌ مقالاً آخر لم يُنشر بعد، يبقى الذكر نصاً عادياً، ويتحول إلى رابط وحده يوم يُنشر ذلك المقال. وتحت كل مقال أستطيع إضافة متابعات قصيرة تظهر كسلسلة.

يُربط LinkedIn مرة واحدة عبر تسجيل الدخول الرسمي. بعدها، ضغطة واحدة على مقال منشور تنشره في حسابي بعنوانه وملخصه ورابطه، وتنبهني اللوحة قبل انتهاء الربط بأسبوعين.

كتابة أو استيرادعربي وإنجليزيقائمة الانتظاربإيقاع أحددهنشر يدويمنشوريُعرض من القاعدةIndexNowتبليغ فوري للمحركاتLinkedInضغطة واحدةRSS وخريطة الموقع
اختر أي مربع لترى ما يفعله وبماذا يرتبط. اسحب جانبياً لترى المخطط كاملاً.

منشور

تعرضه المدونة من قاعدة البيانات خلال دقيقتين تقريباً، بالعربية والإنجليزية، مع بيانات منظمة لمحركات البحث.

التفاصيل التقنية

تُخزن الصفحات مؤقتاً على الحافة دقيقتين، والخلاصات وخريطة الموقع خمس دقائق. ولا يُخزن إلا الناجح منها، فلا تبقى صفحة مفقودة بعد النشر الذي يصلحها.

// Escape everything first, then allow a short list of Markdown. No raw tag
// can survive, a link must be http(s) or a path on this site, and a link to
// an article that is not published yet stays plain text until it is.
function esc(s: string): string {
  return s.replace(/[&<>"']/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch] as string));
}

function inline(s: string, isPublished: (slug: string) => boolean): string {
  return s
    .replace(/`([^`]+)`/g, "<code>$1</code>")
    .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
    .replace(/\*([^*]+)\*/g, "<em>$1</em>")
    .replace(
      /\[([^\]]+)\]\((https?:\/\/[^\s)]+|\/(?!\/)[^\s)]*)\)/g,
      (_m, text: string, href: string) => {
        const self = SELF_ABS.exec(href);
        const path = self ? self[1] : href;
        if (!path.startsWith("/")) return `<a href="${href}" rel="noopener">${text}</a>`;
        const m = ARTICLE.exec(path);
        if (m && !m[1].includes(".") && !isPublished(m[1])) return text;
        return `<a href="${href}">${text}</a>`;
      },
    );
}
التفاصيل التقنية

محوّل Markdown يعطّل كل شيء أولاً، ثم يسمح بثمانية عناصر فقط: العناوين، وكتل الكود، والقوائم، والاقتباسات، والخط العريض، والمائل، والكود داخل السطر، والروابط. والرابط يجب أن يكون http(s) أو مساراً في هذا الموقع، فلا يمكن تهريب أي سكربت عبر مقال.

العربية والإنجليزية

لغتان، مبنيتان من أول سطر.

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

تغيير اللغة يبقيك في الصفحة نفسها ويغير العنوان دون إعادة تحميل. وقبل رسم أي شيء، يحدد سكربت صغير اللغة والاتجاه. وبعد البناء، يكتب سكربت آخر العربية في ملفات كل صفحة عربية، فتراها محركات البحث وقارئات الشاشة دون تشغيل أي كود.

الدرس الذي أحمله دائماً: اختبر العربية أولاً. تباعد الحروف الذي يبدو أنيقاً في الإنجليزية يفصل الحروف العربية المتصلة، فتُقرأ «تقنية» كأنها «تقذية». صار هذا قاعدة في التنسيقات، وفحصاً في التدقيق البصري الآلي.

// Every page has a real route in both languages: the Arabic one is the
// English path under /ar. Switching maps the CURRENT path, so the reader
// stays on the same page instead of landing on the home page.
function pathFor(lang: Lang, current: string): string {
  const bare = current === '/ar' || current === '/ar/'
    ? '/'
    : current.startsWith('/ar/') ? current.slice(3) : current;
  const normalized = bare.startsWith('/') ? bare : `/${bare}`;
  if (lang !== 'ar') return normalized;
  return normalized === '/' ? '/ar/' : `/ar${normalized}`;
}

// No reload and no extra history entry: the address simply follows the language.
function syncPath(lang: Lang) {
  const current = window.location.pathname;
  const target = pathFor(lang, current);
  if (target !== current) {
    window.history.replaceState(null, '', target + window.location.search + window.location.hash);
  }
}
التفاصيل التقنية

التخطيط يستخدم الخصائص المنطقية (البداية والنهاية بدل اليسار واليمين)، فملف تنسيق واحد يخدم الاتجاهين. وتبديل اللغة يجري داخل انتقال React، فيظهر أثر الضغط على الزر أولاً ثم تتبعه إعادة الرسم الكاملة. والأرقام تُنسق لكل لغة، والخط العربي بثلاثة أوزان مع خطوط بديلة مقاسة، ويُحمَّل مسبقاً في الصفحات العربية بعد البناء.

SEO وGEO

مبني ليجده محرك البحث، ويفهمه الذكاء الاصطناعي.

SEO أن يجدك محرك البحث. وGEO أن يفهمك مساعد الذكاء الاصطناعي ويذكرك في إجاباته. وكلاهما يبدأ من الشيء نفسه: صفحة تقول بوضوح من كتبها، وعن ماذا تتحدث، وكيف ترتبط بكل ما حولها.

creatorauthorPersonهوية ثابتة واحدةProfilePageالصفحة الرئيسيةWebSiteهذا الموقعalumniOfالتعليمknowsAbout١٨ موضوعاًCreativeWorkكل صفحة مشروعTechArticleهذه الصفحةworkExampleمواقع بنيتهاsubjectOfمنشورات LinkedIn
اختر أي مربع لترى ما يفعله وبماذا يرتبط. اسحب جانبياً لترى المخطط كاملاً.

شخص واحد، معرّف واحد

كل صفحة تصف الشخص نفسه بالمعرّف الثابت نفسه، فلا تحتاج محركات البحث ولا مساعدات الذكاء الاصطناعي إلى التخمين: هل الصفحتان عن ماجد نفسه؟

التفاصيل التقنية

رسم JSON-LD في التخطيط الجذري يعرّف الشخص بمعرّف @id في الصفحة الرئيسية، وكل عقدة أخرى في أي صفحة تشير إلى ذلك المعرّف بدل أن تكرر بياناته.

  • كل كلمة موجودة في HTML قبل تشغيل أي سكربت.

  • كل صفحة تعلن عنوانها الرسمي ونسختها باللغة الأخرى عبر hreflang.

  • عنوان رسمي واحد للموقع كله: النسخ العامة المكررة أُغلقت أو حُوّلت إلى النطاق الرئيسي.

  • ملف robots.txt يرحب بـ ٢٧ زاحفاً للذكاء الاصطناعي بالاسم، وملف llms.txt يعطي محركات الذكاء الاصطناعي ملخصاً واضحاً عني وعن مكان كل شيء.

  • خريطة الموقع تضم كل صفحة باللغتين، والمقالات الجديدة تُعلن عبر IndexNow.

  • أرقام Search Console وGA4 تصل إلى اللوحة يومياً، فأتابع ما يُعثر عليه، لا ما يُنشر فقط.

وللأمانة: هذا يجعل الموقع مقروءاً ومفهوماً. أما أن يُذكر اسمك في الإجابات، فيحتاج أيضاً محتوى حقيقياً وإشارات من مصادر أخرى مع الوقت.

// Both language routes of a case page call this one builder, so the English
// and Arabic pages cannot disagree. `creator` points at the same Person id as
// the home page, which makes every project a link in one knowledge graph.
const graph = {
  '@context': 'https://schema.org',
  '@graph': [
    {
      '@type': 'CreativeWork',
      '@id': `${url}#project`,
      name: project.name[lang],
      description: project.description[lang],
      inLanguage: lang,
      url,
      sameAs: project.liveUrl,
      creator: { '@id': `${SITE}/#person` },
      keywords: project.stack.join(', '),
    },
    {
      '@type': 'BreadcrumbList',
      itemListElement: [
        { '@type': 'ListItem', position: 1, name: OWNER[lang], item: home },
        { '@type': 'ListItem', position: 2, name: project.name[lang], item: url },
      ],
    },
  ],
};

الأداء

شعار ثلاثي الأبعاد، وسرعة على الجوال.

تبدأ الصفحة الرئيسية بمشهد ثلاثي الأبعاد: ٤٧ رمزاً برمجياً تتطاير ثم تتجمع، فتمتصها خطوط معدنية تكتب شعاري أثناء التمرير. مشهد كهذا يبطئ أي موقع بسهولة، فكانت القاعدة بسيطة: النص يُحمَّل أولاً، والمشهد لا يقف في طريقه أبداً.

كان تقييم السرعة على الجوال ٤٤. المتهم الطبيعي كان المشهد ثلاثي الأبعاد، لكن القياس قال غير ذلك: مكتبة تمرير كانت تزيح فقرة قليلاً بعد ظهورها، فيحسب المتصفح أن المحتوى الأساسي ظهر مرة ثانية ومتأخراً. عرفت ذلك بحجب كل ملف على حدة وإعادة القياس. صار التثبيت أثناء التمرير CSS عادياً، وانتقل المشهد إلى مسار خلفي، وصارت أداة التحليلات تنتظر الزائر. فارتفع التقييم إلى نحو ٨٥، وطابقت الصور الملتقطة عند أربع نقاط تمرير النسخة السابقة.

  • المشهد ينتظر انتهاء المقدمة وفراغ المتصفح، ثم يُرسم داخل Web Worker. وإن تعذر ذلك يُرسم في المسار الرئيسي، ودون WebGL يظهر رسم مسطح للشعار بدلاً منه.

  • لا يُرسم إلا حين يتغير شيء، ويبطئ نفسه في الحركة الهادئة، ويتوقف تماماً حين يخرج من الشاشة.

  • كل قسم في الصفحة الرئيسية يُفعَّل وحده، والأقسام الأسفل تتجاوز حساب التخطيط حتى يفرغ المتصفح.

  • أداة التحليلات تُحمَّل عند أول تفاعل أو بعد سبع ثوانٍ، ولا ترسل إلا مسار الصفحة.

  • الملفات ذات الأسماء المشفرة تُخزن مؤقتاً لسنة، ومن يفضل تقليل الحركة يرى الحالة النهائية مع تلاشٍ سريع.

والزوار الحقيقيون هم الأهم. حين قست في سبتمبر، كان ٧٥٪ من الزيارات الحقيقية يظهر محتواها الأساسي خلال ثانية واحدة.

// The 3D scene draws inside a Web Worker, so three.js never blocks the page.
// Three outcomes race (ready, error, silence) and a single latch settles the
// race once: the worker keeps the canvas, or the page falls back to drawing
// on the main thread. The visitor sees the logo either way.
const worker = new Worker(new URL('./scene.worker.tsx', import.meta.url));
let settled = false;
const fallBack = () => {
  if (settled) return;
  settled = true;
  setUseWorker(false);
};
const timer = window.setTimeout(fallBack, READY_TIMEOUT_MS);
worker.onerror = fallBack;
worker.onmessage = (e: MessageEvent<{ type: string }>) => {
  if (e.data.type === 'error') fallBack();
  if (e.data.type === 'ready' && !settled) {
    settled = true;
    window.clearTimeout(timer);
  }
};

const rect = canvas.getBoundingClientRect();
const offscreen = canvas.transferControlToOffscreen();
const dpr = Math.min(Math.max(1, window.devicePixelRatio || 1), 1.5);
worker.postMessage(
  { type: 'init', canvas: offscreen, width: rect.width, height: rect.height, dpr },
  [offscreen],
);

واجهة المتجر

متجر ثابت يعرف رغم ذلك ما هو صحيح الآن.

يُبنى المتجر صفحات ثابتة من الكتالوج في قاعدة البيانات. وحين أعدّل الكتالوج في اللوحة وأضغط نشر، تعيد مهمة في GitHub Actions بناءه ونشره. وإن تعذر الوصول إلى الواجهة البرمجية أثناء البناء، يُستخدم الكتالوج المحفوظ في الكود، فلا يفشل النشر بسبب خلل عابر في الشبكة.

وفوق الصفحات الثابتة طبقة حية صغيرة: في كل زيارة يسأل المتجر الواجهة البرمجية كيف يُباع كل منتج الآن. وهكذا أستطيع تحويل منتج من طلب عرض سعر إلى دفع مباشر دون أي إعادة بناء.

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

// At build time the catalog comes from the database through the API. If the
// API cannot be reached, the catalog kept in code is used, so a network
// hiccup can never fail a publish.
let OFFS: Off[] = OFFERINGS.map(toCard);
try {
  const r = await fetch(`${SITE}/api/products`, { headers: { accept: 'application/json' } });
  if (r.ok) {
    const j: any = await r.json();
    if (Array.isArray(j?.offerings) && j.offerings.length > 0) OFFS = j.offerings;
  }
} catch { /* offline build: the fallback stays */ }
التفاصيل التقنية

المتجر مبني بـ Astro بمكتبتين فقط وقت التشغيل، وخطوط مستضافة ذاتياً، وصفحة لكل خدمة ببيانات Service المنظمة، وصفحتا خصوصية وشروط بلغة واضحة. واللغة التي تختارها تنتقل معك بين الصفحة الرئيسية والمتجر والمدونة وصفحة الحجز. ودوّار الفئات يقيس أبعاده نادراً ويحرك البطاقات بمتغيرات CSS فقط.

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

من التعديل إلى الإنتاج، دون أي نشر يدوي.

كل تعديل يسلك الطريق نفسه: الدفع يشغّل الاختبارات، ويحدد أي الخدمات الخمس تغيرت فعلاً، ويرفض النشر إن كانت في قاعدة البيانات هجرة تنتظر، وينشر ما تغير فقط، ويفحص كل عنوان حي، ثم يفتح الموقع العام في متصفح حقيقي باللغتين، على الجوال والكمبيوتر.

دفع التعديلالتخطيطماذا تغيّر؟بوابة الهجراتلا شيء معلّق؟النشرالخدمات المتغيرة فقطتحقق حيمع مهلة متزايدةتدقيق بالمتصفحلغتان، مقاسانالاختبارات١٬٢١٢ اختباراً آلياًنسخ ليليدون بيانات اعتمادمراقبة التشغيلكل ٣٠ دقيقة
اختر أي مربع لترى ما يفعله وبماذا يرتبط. اسحب جانبياً لترى المخطط كاملاً.

بوابة الهجرات

إن كانت في الإنتاج هجرة قاعدة بيانات تنتظر، يتوقف كل شيء. فلا يصل الكود إلى قاعدة بيانات غير جاهزة له.

اختبار آلي
١٬٢١٢
قاعدة في مختبر التسابق
١٤
شاشة لوحة مدققة
٢٣
مهام نشر
٧
نسخة ليلية محفوظة
١٤
يوماً من تاريخ الاستعادة
٣٠

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

# A push deploys only what it changed. Shared code is compiled into both the
# API and the panel, so a change there redeploys both. Test-only changes do
# not change what a Worker serves, so they deploy nothing.
- name: Plan
  run: |
    has() { grep -qE "$1" <<<"$CHANGED"; }
    WORKER_FILES="$(grep -vE '^store-system/[^/]+/test/' <<<"$CHANGED" || true)"
    hasw() { grep -qE "$1" <<<"$WORKER_FILES"; }

    SHARED=0
    if hasw '^store-system/shared/'; then SHARED=1; fi

    if hasw '^store-system/api/'   || [ "$SHARED" = 1 ]; then on api;   fi
    if hasw '^store-system/admin/' || [ "$SHARED" = 1 ]; then on admin; fi
    if hasw '^store-system/blog/'; then on blog; fi

    SITE_FILES="$(grep -vE '^scripts/ci/' <<<"$CHANGED" || true)"
    if grep -qE '^(src/|public/|assets/|scripts/|next\.config|wrangler\.worker\.jsonc|package\.json)' <<<"$SITE_FILES"; then
      on portfolio
    fi
التفاصيل التقنية

الاختبارات مكتوبة بـ node:test دون إطار إضافي، وتشغّل الـ Worker كما يُشحن عبر طلبات حقيقية على قاعدة بيانات مبنية من الهجرات الحقيقية. ومختبر للتزامن يعيد تمثيل حالات التسابق والانهيار بطريقة قابلة للتكرار، وإثبات تشغيلي يعمل داخل workerd نفسه، وتدقيق بصري منفصل يفتح ٢٣ شاشة من اللوحة بعرضين وباللغتين، ويفشل عند أي تسمية مقصوصة أو تباعد حروف على نص عربي.

الأمان

الأمان والخصوصية، بالقدر الذي يصح قوله علناً.

بعض الأشياء تركتها خارج هذه الصفحة عمداً: طريقة حماية اللوحة الخاصة، وأماكن الصفحات الخاصة، وكل مفتاح وكل حد. وما يمكن مشاركته هو الشكل العام.

  • عنوان واحد، مشفّر، بخطوات محسوبة. الموقع لا يجيب إلا عبر HTTPS، مع HSTS رُفع على درجات: خمس دقائق، ثم أسبوع، ثم سنة كاملة بعد أن ثبت كل شيء. فالمتصفح الذي حفظ HSTS لن يفتح الموقع دون تشفير، ولا يستطيع الخادم التراجع عن ذلك، لذلك لم أستعجل.

  • لا نسخ مكررة. وُجدت نسختان عامتان كاملتان من الموقع على عناوين أخرى، إحداهما دون تشفير. أُغلقت الأولى، وصارت الثانية تحوّل إلى النطاق الرئيسي.

  • ترويسات حماية، وسياسة رُوقبت قبل فرضها. ترويسات حماية أساسية على الخدمات الخمس، وسياسة أمان للمحتوى (CSP) عملت في وضع التقارير فقط قرابة أسبوع قبل فرضها، فتمنع ما يجب منعه دون أن تكسر الموقع.

  • أدلة لا يستطيع التطبيق إعادة كتابتها. رسائل الدفع يُتحقق منها قبل تسجيلها، والعقود الموقعة تقفلها قاعدة البيانات، والموافقات والتوقيعات وسجل التدقيق للإضافة فقط.

  • لوحة خاصة خلف بوابة هوية. لا يمكن الوصول إلى لوحة التشغيل دون المرور ببوابة هوية، والنماذج العامة محمية بتحقق بشري لا يُحمَّل إلا عند الحاجة.

  • لا أسرار في الكود. لا يوجد أي سر في مستودع الكود. والنسخ الليلية تُزال منها بيانات الاعتماد وتُفحص قبل حفظها.

  • الخصوصية كإجراء. محو بيانات شخص بموجب نظام حماية البيانات الشخصية (PDPL) إجراء واحد: يُمحى من كل مكان دفعة واحدة، وتبقى المستندات التي يُلزمني النظام بحفظها مع استبدال اسمه.

/*
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()

التقدير

ما قررته، وما رفضته، وما لا تستطيع هذه البيئة إثباته.

  • مرفوض: ترقية رئيسية. الإصدارات الرئيسية التالية لإطار الموقع ومولّد المتجر عملت كلها. لكن كود JavaScript الأساسي للموقع زاد ١٧٪، وفقد المتجر تأثير الزجاج في Chrome إلا بحلول التفافية. فأبقيت الإصدارات الحالية وحددت موعداً للمراجعة.

  • مستبدَل: مكتبة تمرير. المكتبة التي كانت تثبّت مقدمة الصفحة شوّهت قياس سرعة تحميلها. وCSS العادي يؤدي العمل نفسه، بالشكل نفسه.

  • مُبقى عمداً: إصدار مكتبة الأبعاد الثلاثية. الإصدارات الأحدث تغيّر طريقة رسم الشعار، والشعار لا يتغير دون سبب.

  • تغيّر بعد المراجعة: طريقة التعامل مع الإصلاحات. بعد بناء طرق الدفع نجحت كل الاختبارات، ومع ذلك وجدت مراجعة الكود ٣٣ ملاحظة حقيقية، ووجدت مراجعة ثانية ١٤ أخرى، اثنتان منها سببتها إصلاحات المراجعة الأولى. ومنذ ذلك الحين، أي إصلاح يمس المال أو بيانات العملاء يُعامل ككود جديد يحتاج مراجعته الخاصة.

ما لا تستطيع هذه البيئة إثباته

  • الحجم. الموقع الشخصي يستقبل عدداً محدوداً من الزوار. وما يصمد هنا قد لا يصمد مع آلاف المستخدمين، وهذا يحتاج اختبار ضغط مستقلاً.

  • الفرق. أنا المستخدم الوحيد للوحة. فالصلاحيات، والموافقات بين الأقسام، وتسليم الحسابات عند خروج موظف، لا تُختبر هنا.

  • الامتثال. الموقع يدربني على الضوابط، لكنه لا يثبت امتثال شركة لنظام حماية البيانات أو لضوابط الأمن السيبراني.

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

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

التقنيات

كل تقنية في النظام، وما تفعله.

  • Next.js 15 وReact 19: الموقع، مصدّراً كصفحات ثابتة بلغتين.

  • TypeScript: كل الخدمات، من البداية إلى النهاية.

  • Tailwind CSS: متغيرات التصميم وتخطيط الموقع.

  • three.js وReact Three Fiber وdrei: الشعار ثلاثي الأبعاد ومشهد قصة ARA Brain، ويُرسمان داخل Web Worker حيث أمكن.

  • Lenis: التمرير الناعم في الصفحة الرئيسية، ويُتجاوز لمن يفضل تقليل الحركة.

  • Astro: متجر الخدمات، مبنياً كصفحات ثابتة من قاعدة البيانات.

  • Hono: الواجهة البرمجية، والمدونة، ولوحة التشغيل.

  • Cloudflare Workers: استضافة الخدمات الخمس، والمهمة المجدولة، والبريد الوارد.

  • Cloudflare D1 (SQLite): قاعدة بيانات واحدة لكل شيء، مع قيود تلقائية تحمي الأدلة.

  • Cloudflare Email Routing وResend: البريد الوارد والصادر.

  • postal-mime: قراءة البريد الوارد.

  • Tap Payments: الدفع الإلكتروني عبر صفحة مستضافة، ويشمل Apple Pay.

  • Web Crypto (SHA-256 وHMAC): ختم المستندات، ورموز التوقيع، والتحقق من رسائل الدفع.

  • Google Calendar وSearch Console وGA4: نسخ المواعيد إلى التقويم، وأرقام البحث والزيارات داخل اللوحة.

  • LinkedIn API: مشاركة المقالات بضغطة واحدة.

  • IndexNow: تبليغ فوري لمحركات البحث.

  • Cloudflare Turnstile: التحقق البشري في النماذج العامة.

  • Web Push وService Worker: الإشعارات وعدّاد التطبيق على جوالي.

  • GitHub Actions: الاختبارات، والنشر، وإعادة بناء المتجر، والنسخ الليلي، والمراقبة الخارجية.

  • node:test وnode:sqlite: ١٬٢١٢ اختباراً آلياً على قاعدة بيانات حقيقية مبنية من الهجرات.

  • Playwright: التدقيق البصري للوحة وللموقع الحي في متصفح حقيقي.

لماذا كتبت كل هذا

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

وإن كنت تبني شيئاً مشابهاً، أو تريد نظاماً كهذا لشركتك، يسعدني الحديث معك.