بناء التطبيق

ويدجتس صفحة الهبوط (Landing Widgets)

يقدر تطبيقك المُضمَّن يضيف بلوكات إلى محرر صفحة الهبوط العامة للجهة: الويدجت يظهر في تصنيف «تطبيقاتك» داخل المحرر (للجهات التي ثبّتت تطبيقك فقط)، والجهة تسحبه إلى موقعها العام فيُعرض لزوّارها كأي بلوك من بلوكات المنصة — دون أن تكتب الجهة سطر كود واحداً. أنت تصرّح بالويدجت في الـ manifest وتشحن مكوّن Livewire واحداً؛ المنصة تتكفّل بالباقي:

  • بطاقة المحرر باسم تطبيقك وأيقونته.
  • فورم إعدادات البلوك يتولّد آلياً من تصريح settings_schema — لا تكتب أي Blade للمحرر.
  • سلوك البلوك (نقل/نسخ/أعمدة/إظهار وإخفاء) يأتي من المحرر نفسه؛ الويدجت يُخزَّن داخل مستند الصفحة كبلوك جسر واحد من نوع partner-widget — تثبيت تطبيق جديد لا يضيف block class للمنصة.
  • الأمن مجاناً وإجبارياً: سياق التشغيل على الصفحة العامة تبنيه المنصة، لا أنت.

لتطبيقات Embedded فقط. الويدجت يُرسم in-process داخل okta-web كمكوّن Livewire؛ تطبيق مستضاف خارجياً لا يملك ما يُرسم هناك. تصريح landing_widgets مع أي integrationType آخر يُرسِب نشر الإصدار.

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

التصريح في الـ manifest

{
  "integrationType": "embedded",
  // ... بقية الـ manifest ...
  "landing_widgets": [
    {
      "key": "admission_register",
      "label": "التسجيل في القبول",
      "label_en": "Admission Registration",
      "description": "نموذج تقديم طلب قبول من موقع الجهة العام.",
      "description_en": "Admission application form on the tenant public site.",
      "icon": "clipboard",
      "category": "conversion",
      "livewire_component": "my-app-landing-register",
      "interaction": "static",
      "scopes": ["education.grades.read"],
      "cache_ttl": 300,
      "settings_schema": {
        "fields": [
          { "key": "title", "type": "text", "label": "العنوان", "label_en": "Title",
            "localized": true, "default": "سجّل الآن", "min": 1, "max": 80 },
          { "key": "show_deadline", "type": "toggle", "label": "إظهار آخر موعد", "default": true }
        ]
      },
      "preview": { "thumbnail_url": "https://cdn.example.com/widgets/register.png" }
    }
  ]
}

الحقول

landing_widgets مصفوفة بحد أقصى 10 ويدجت لكل تطبيق.

الحقل إلزامي القيمة
key ✓ ^[a-z][a-z0-9_]{1,39}$ — فريد داخل التطبيق. المعرّف العالمي هو المركّب <module-slug>:<key>؛ به يُشار للويدجت من البلوكات المخزَّنة على صفحات الجهات. تغييره بعد النشر = ويدجت جديد والبلوكات القديمة تفقد مرجعها — سمّه جيداً من أول إصدار.
label / label_en label ✓ اسم البطاقة في المحرر (2–60 حرفاً). label_en حسب locale ويتراجع إلى label.
description / description_en — وصف قصير تحت البطاقة (≤ 300 حرف).
icon — اسم أيقونة (≤ 40 حرفاً، الافتراضي puzzle). القيمة غير المعروفة تتراجع لأيقونة افتراضية بلا خطأ.
category — مجموعة البطاقة في المحرر: hero | content | media | conversion | layout (الافتراضي conversion).
livewire_component ✓ alias مكوّن Livewire بالصيغة المسطَّحة. أي alias يحوي :: يرفض النشر — Livewire 4 لا يحلّه أصلاً (نفس مصيدة لوحة تحكم التطبيق).
interaction — static (الافتراضي) أو interactive. التفاعلي عقد أثقل بالكامل — انظر الويدجت التفاعلي.
scopes — نطاقات القراءة التي يحتاجها الرسم وقت التشغيل (≤ 20)، تنتهي حصراً بـ .read — أي فعل آخر يرفض النشر. تصريح تقييد لا منح: ما تعلنه هنا يدخل تقاطع ضلع القراءة، وما لا تعلنه لا يصل للويدجت حتى لو كان ممنوحاً للتثبيت.
public_writes — نطاقات الكتابة التي ينفّذها الويدجت نيابةً عن زائر مجهول (≤ 10)، تنتهي حصراً بـ .write. ثلاثة شروط عند النشر: الصيغة، وأن كل مدخل نطاق نشط وموسوم public_writable في كتالوج okta-web (يُفحص ضد الكتالوج الحي — وإن تعذّر الفحص يُرفض التصريح بدل قبوله بلا تحقق)، وأن interaction=interactive — الساكن يُقدَّم من كاش بلا round-trip يمكن أن تحدث فيه الكتابة.
cache_ttl — ثواني تخزين HTML المرسوم على الصفحة العامة (0–86400، الافتراضي 300). يُتجاهَل كلياً للتفاعلي — جسم مخزَّن كان سيعيد عرض حالة فورم زائر لزائر آخر.
settings_schema — تصريح حقول الإعدادات — القسم التالي.
preview.thumbnail_url — صورة مصغّرة للبطاقة. رابط مطلق https حصراً.

فورم الإعدادات: settings_schema

{ "fields": [ { /* حقل */ }, ... ] }   // بحد أقصى 20 حقلاً

الأنواع مفردات مغلقة — ثمانية لا غير: text, textarea, number, toggle, select, color, image, link.

لا يوجد ولن يوجد نوع html (والتصريح به يرفض النشر برسالة صريحة). هذا قرار أمني لا نقص: كل قيمة إعداد تمرّ عبر escaping الـ Blade عند الرسم، فلا يمكن لأدمن جهة (أو لمخترق حسابه) حقن سكربت في الصفحة العامة عبر إعدادات ويدجتك. إن احتجت تنسيقاً غنياً، ابنه في مكوّنك من حقول مهيكلة.

خصائص الحقل:

الخاصية تنطبق على الوصف
key الكل ^[a-z][a-z0-9_]{0,39}$، فريد داخل الويدجت.
type الكل أحد الأنواع الثمانية.
label / label_en الكل عنوان الحقل في الفاحص (label إلزامي، ≤ 120 حرفاً).
localized الكل true = المحرر يكتب القيمة للّغة المفتوحة فقط؛ false/غائبة = يعكسها على اللغتين معاً. مكوّنك يستقبل دائماً settings لغةٍ واحدة — لغة الصفحة المعروضة — ولا يحتاج أن يعرف أي الحقول مترجَم.
default الكل القيمة الابتدائية عند الإدراج. تُفحص ضد النوع: toggle يقبل bool فقط، number رقماً فقط، وdefault لـ select لازم يكون من options المعلنة.
help / help_en الكل نص مساعدة تحت الحقل (≤ 300 حرف).
min / max text, textarea, number حدود الطول للنصوص والقيمة للأرقام. على بقية الأنواع تُسقَط بصمت.
options select [{"value": "...", "label": "...", "label_en": "..."}] قائمة ثابتة (≤ 50 خياراً، القيم فريدة). select بلا options يرفض النشر.
options_endpoint select محجوز ومرفوض حالياً. الصيغة (<provider>:<name>) تُفحص ثم يُرفض التصريح صراحةً عند النشر ما دام لا resolver على المنصة — رفض مقروء اليوم خير من حقل يتدهور صامتاً إلى صندوق نص في محرر كل جهة. استعمل options الثابتة.

مثال يغطي الأنواع الثمانية:

{ "fields": [
  { "key": "title",    "type": "text",     "label": "العنوان", "localized": true,
    "default": "سجّل الآن", "min": 1, "max": 80 },
  { "key": "intro",    "type": "textarea", "label": "المقدمة", "localized": true, "max": 400 },
  { "key": "max_rows", "type": "number",   "label": "عدد الصفوف", "default": 5, "min": 1, "max": 20 },
  { "key": "show_fees","type": "toggle",   "label": "إظهار الرسوم", "default": false },
  { "key": "layout",   "type": "select",   "label": "التخطيط",
    "options": [
      { "value": "grid", "label": "شبكة", "label_en": "Grid" },
      { "value": "list", "label": "قائمة", "label_en": "List" }
    ],
    "default": "grid" },
  { "key": "accent",   "type": "color",    "label": "لون التمييز", "default": "#0ea5e9" },
  { "key": "banner",   "type": "image",    "label": "صورة الترويسة" },
  { "key": "policy",   "type": "link",     "label": "رابط سياسة القبول", "localized": true }
]}

نقطتان تحكمان فهمك للتخزين:

  • البلوك يُخزَّن لكل لغة داخل مستند الصفحة، والحقول غير المترجَمة تُكرَّر عبر اللغتين بنفس القيمة — لا مخزن «مشترك».
  • بلوك أُدرج قبل أن تضيف حقلاً جديداً في إصدار لاحق لن يحمل مفتاحه. لذلك: كل قراءة من settings في مكوّنك خلف ?? $default، دائماً.

كتابة المكوّن

الويدجت مكوّن Livewire عادي داخل تطبيقك، يخضع لنفس عقد الكود Embedded كله — لا استثناء لكونه على صفحة عامة:

  1. البيانات حصراً عبر App\Services\PartnerApi\* — الـ policy scanner يرسب البناء على غيرها، وكل خدمة تستدعي فحص النطاق داخلياً فما ليس في السياق يُرفض تلقائياً.
  2. لا تفترض مستخدماً. على الصفحة العامة auth()->user() هو null دائماً. كود يفترض جلسة سينكسر هناك حتى لو عمل في معاينة المحرر.
  3. إعدادات المطوّر متاحة: GetAppSetting / GetAppSettings تعمل داخل الويدجت كأي سياق تشغيل — تقرأ مخزن تطبيقك أنت فقط، فتغيّر سلوك الويدجت دون نشر إصدار جديد.
  4. الواجهة بمعايير المنصة: مكوّنات <x-…> والـ tokens الدلالية — الويدجت يُرسم داخل صفحة الجهة بثيمها وبراندها، والألوان الصلبة تكسر ذلك.
  5. افترض الفشل: استثناء من مكوّنك على الصفحة العامة لا يُظهر صفحة خطأ — يُنتج فراغاً صامتاً مكان الويدجت (انظر أدناه).

الويدجت الساكن (static) يُرسم مرة ويُخزَّن ناتجه cache_ttl ثانية: أي wire:click أو wire:model في blade ساكن يتجمّد سمات ميتة في HTML مخزَّن. مناسب للعرض: قوائم، إحصاءات، بطاقات، روابط. ومكوّنه يستقبل عند الرسم بالضبط settings + locale ولا شيء غيرهما — ما يحتاجه عن الجهة يقرأه من الـ ModuleContext النشط كأي خدمة PartnerApi.

نموذج الأمن — سطحان غير متماثلان

سطحا الرسم غير متماثلين، والفرق بينهما هو جوهر التصميم كله:

  • معاينة المحرر: أدمن الجهة مسجّل دخوله، فالسياق يُحلّ كالمعتاد (نفس مسار بقية صفحات تطبيقك). المعاينة ترى ما يراه التطبيق عادةً.
  • الصفحة العامة: الزائر مجهول تماماً — لا مستخدم، لا جلسة. المنصة تعيد بناء سياق التشغيل لكل طلب من الجهة وحدها (تُحلّ من الدومين)، ونطاقات هذا السياق هي اتحاد مجموعتين، كلٌّ منهما تقاطع ثلاثي مستقل:
READ  = (الممنوح للتثبيت) ∩ (الموسوم public_surface في الكتالوج)  ∩ (scopes الويدجت)
WRITE = (الممنوح للتثبيت) ∩ (الموسوم public_writable في الكتالوج) ∩ (public_writes الويدجت)

السياق = READ ∪ WRITE

ليستا مساراً واحداً بعلمٍ مدموج عمداً: القراءة والكتابة تجيبان سؤالين مختلفين («هل يجوز لغريب أن يرى هذا؟» مقابل «هل يجوز لغريب أن يسبّب هذا؟»)، ومسار واحد كان سيسمح لتعديل مستقبلي على أحدهما أن يحرّك الآخر بصمت. ومرشَّح الفعل يعمل مرتين على كل ضلع: .write داخل scopes[] ليس تصريح قراءة مهما قال الـ manifest، وصف كتالوج وُسم خطأً لا يركب منحةً إلى السياق.

ضلع الكتابة يحمل شرطين إضافيين: علم تفاعلية عام تفحصه المنصة داخل الـ resolver نفسه، وتذكرة كتابة عامة لا تُسكّ إلا بعد اجتياز الحواجز (انظر الويدجت التفاعلي). بلا تذكرة = مجموعة كتابة فارغة — وهذا ما يحصل عليه كل مسار قراءة، افتراضياً وإلى الأبد.

المبدأ الحاكم: ما لا يدخل السياق لا يحتاج حارساً. لا فحص أذونات جديد ولا middleware إضافي ولا قائمة منع تُصان يدوياً — سياق فقير عمداً، والحارس القائم نفسه الذي يحرس كل خدمات PartnerApi يرفض كل نطاق غير موجود فيه.

النتيجة العملية التي يجب أن تعرفها قبل أن تبني: نطاقات الطلاب وأولياء الأمور والموظفين والماليات لا تُوسم public_surface ولا public_writable أبداً. استثناؤها بنيوي لا سياسة: ليست قاعدة في middleware يمكن أن يخطئها أحد — هي ببساطة لا تدخل السياق، فكل استدعاء لها يُرفض كأنها لم تُمنح قط. ويدجت يستدعي ListStudents على صفحة عامة سيفشل دائماً، مهما أعلنت في scopes. إن كان تصوّر ويدجتك يحتاج بيانات أشخاص على صفحة عامة، فالتصوّر نفسه يحتاج إعادة نظر.

قائمة public_surface اليوم (قد تتوسع بقرار منصّة، بتحفّظ شديد):

  • countries.directory.read — بيانات مرجعية عامة (قائمة الدول).
  • countries.education_levels.read — سُلّم المراحل التعليمية المرجعي.
  • education.grades.read — صفوف الجهة نفسها (بنية مؤسسية تنشرها المدرسة على موقعها أصلاً — لا شخص ولا سجل فيها).

الحقلان scopes وpublic_writes في تصريحك هما الضلع الثالث من تقاطعَيهما، وهما في صالحك: صرّح بأقل ما يلزم، فتضمن أن ثغرة في كود ويدجتك لا تصل إلا لما أعلنته — حتى لو كان تثبيتك ممنوحاً أكثر.

الفشل فراغ صامت — ومخزَّن

فشل الويدجت على الصفحة العامة — استثناء، نطاق مرفوض، alias لا يُحلّ، تطبيق أُزيل تثبيته — يُنتج فراغاً صامتاً مكان البلوك، ولا يُسقط موقع الجهة أبداً. وفشل الساكن يُخزَّن 30 ثانية لمنع الطَرق المتكرر: ويدجت ينهار لن يُعاد تنفيذه مع كل زائر. اعكسها على نفسك أثناء التطوير: إصلاحك قد لا يظهر فوراً بسبب كاش الفشل، وستظن أن «الويدجت لا يعمل بلا سبب» بينما السبب استثناء مبلوع — افحص سجلات okta-web ولا تعتمد على تحديث الصفحة المتكرر كاختبار.

على السطح الآخر — round-trip لويدجت تفاعلي — الفشل ليس صامتاً: طلب لا يمكن نسبته إلى ويدجت تملكه الجهة فعلاً يُرفض 403. الصمت للصفحة، والصراحة للـ round-trip.

الويدجت التفاعلي (interaction=interactive)

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

الوراثة إلزامية — و CI يفرضها

مكوّنك يجب أن يرث App\Support\LandingWidgets\PublicWidgetComponent — صنف تملكه المنصة. قاعدة landing-widget-base-class في policy scanner تُرسِب البناء لأي class تحت app/LandingWidgets/ في تطبيقك لا يرثه: Livewire\Component عادي هناك ليس «ويدجت بلا إضافات» — هو مسار كتابة عام بلا اشتقاق سياق وبلا حواجز، يفشل مفتوحاً لا مغلقاً. (صنف وسيط خاص بك يرث PublicWidgetComponent شكل مشروع — أعفِه على سطر التصريح بـ // partner-policy:allow=landing-widget-base-class.)

لماذا: /livewire/update لا يذكر شيئاً

الرسم الأول تصنعه المنصة وهي ممسكة بالجهة والتثبيت والسياق. لكن كل wire:click بعده يذهب إلى /livewire/update — route لا يعرف صفحات هبوط ولا جهات؛ كل ما معه snapshot أرسله المتصفح. لذلك الصنف الأساسي يعيد اشتقاق السياق كاملاً على كل hydrate من بيانات حيّة: جهة حقيقية → الباقة ما زالت تشمل الويدجتس → التفاعلية مفعّلة → الويدجت ما زال في كتالوج الجهة → مصرَّح interactive → سياق منحول بالتقاطعات. #[Locked] على خصائص الهوية يعمل عملاً حقيقياً لكنه ليس الحارس: tenantUlid مزوَّر يُحلّ على بيانات حيّة ويُرفض لأن تلك الجهة لم تثبّت تطبيقك. حتى settings المحمولة على الـ snapshot يُعاد ضبطها على schema الويدجت الحالية قبل أي استخدام.

mount() وboot() وdehydrate() وexception() كلها final عمداً. نقطتك أنت: mounted() تعمل مرة بعد قيام السياق. ولا تخزّن الـ context على خاصية — قيم per-request تُعاد صناعتها كل hydrate.

مكوّنك يستقبل عند الرسم — إضافةً إلى settings — ثلاثية الهوية moduleSlug / widgetKey / tenantUlid + widgetLocale، لأنه سيعيد بناء السياق بها على كل round-trip. (الاختيار بفحص الصنف المحلول، لا بحقل interaction المصرَّح.)

الكتابة: withPublicWrite() والحواجز الثلاثة

كل كتابة نيابةً عن الزائر تمرّ حصراً من:

public function submit(): void
{
    $this->validate([...]);

    $this->withPublicWrite('submit', function (): void {
        app(\App\Services\PartnerApi\...\CreateSomething::class)(...);
    });
}

بالترتيب، وكلها في الصنف الأساسي فلا تستطيع إسقاط أحدها:

  1. honeypot — حقل مخفٍ تملكه المنصة (انظر الـ partial أدناه). تعبئته = بوت.
  2. throttle — لكل (IP × جهة × module × ويدجت × action)؛ الافتراضي 5/دقيقة، أو maxPerMinute لكل نداء. دالة throttle() متاحة لك مستقلةً أيضاً لحماية قراءة مكلفة.
  3. captcha — يعمل فقط حين تربط المنصة provider وتفعّله الجهة (أو تفرضه المنصة عاماً). المنصة لا تشحن provider افتراضياً، فالحاجز غائب بدل أن يحجب جهةً بفحص لا يستطيع العمل.

الرفض ليس استثناء تعالجه: الـ callback ببساطة لا يعمل، withPublicWrite يرجع null، و$widgetBarrierMessage يحمل جملة مترجمة يعرضها الـ partial للزائر.

بعد اجتياز الثلاثة — وهناك فقط — تُسكّ تذكرة الكتابة العامة، والسياق يُعاد اشتقاقه معها فيدخل ضلع الكتابة، يعمل الـ callback، ثم finally يعيد الاشتقاق بلا تذكرة — تصريح الكتابة يدوم بالضبط بقدر الكتابة، والـ render التالي بسياق قراءة فقط. لا مكان آخر في المنصة يسكّ التذكرة، فلا مسار لكتابة عامة تخطّى حاجزاً.

الـ form-guard partial — include إلزامي لا يُتجاوز

@include('landing-builder::blocks.partner-widget.partials.form-guard')

مرة واحدة داخل عنصر جذر مكوّنك. يرسم الـ honeypot + علامة الحارس + رسالة الرفض. الفرض من الـ HTML الناتج فعلاً: الصنف الأساسي يفحص مخرَج الرسم ويشتق منه حالة الحارس — فورم بلا الـ partial يصل للكتابة التالية بحاجز honeypot فاشل دائماً، ولا يتجاوز. العرَض المضلِّل حين تنساه: الفورم يظهر ويعمل ظاهرياً، والكتابة ترجع null مع رسالة حاجز.

فخّ النشر: SESSION_DOMAIN — اعرفه قبل أن تنشر

الويدجت التفاعلي = POST بجلسة و CSRF، والكوكي يعود للخادم فقط إذا غطّى نطاقَ المضيف المخدوم:

  • SESSION_DOMAIN غير مضبوط: كل نطاق جهة (مخصص أو subdomain) يحصل على جلسته. هذا الترتيب الذي يعمل.
  • SESSION_DOMAIN مثبَّت على نطاق المنصة: جهة تخدم صفحتها من example.com لا تحصل على كوكي جلسة إطلاقاً — الصفحة تُرسم، الفورم يظهر، وأول نقرة 419 أمام زائر لا يستطيع الإبلاغ.

المنصة تكشف الحالة الثانية ولا «تصلحها» (نطاق كوكي منصّي يُضبط عادة عمداً): تقمع الويدجت التفاعلي على ذلك المضيف وتعرض كرتاً مهذّباً بدل فورم ينتهي بـ 419، مع سطر warning في سجلات okta-web. ليس خطأك ولا خطأ كودك — شرط نشر. اختبر على نطاق مخصص إن كانت جهاتك تستعملها.

النطاق الوحيد الموسوم public_writable اليوم

education.admission_applications.write — طلب قبول يقدّمه زائر من موقع الجهة العام. صندوق بريد: يدخل ولا يخرج — لا يوجد له .read مقابل إطلاقاً (قراءة عامة كانت ستكشف طلبات المتقدمين الآخرين لأي زائر)، وهو موسوم is_dangerous فيُعرض بألوان الخطر على شاشة منح التثبيت عمداً.

توسيع القائمة قرار منصّة لا يُطلب عبر الـ manifest، ومحكوم بثلاث قواعد غير قابلة للتفاوض: intake فقط (إنشاء سجل وارد جديد يخصّ الزائر نفسه — لا update ولا لمس سجل قائم)، لا أشخاص ولا أموال، لا delete أبداً. عندك حالة استخدام؟ قدّمها لفريق المنصة.

دورة الحياة

  1. النشر: تضيف landing_widgets إلى manifest إصدارك وتنشره. الفحص عند القبول (الصيغ، الحدود، integrationType=embedded، وسوم public_writes ضد الكتالوج الحي) — الكتلة المخالفة ترفض الإصدار.
  2. التثبيت: الجهة تثبّت تطبيقك من المتجر وتمنح النطاقات كالمعتاد.
  3. الظهور في المحرر: كتالوج ويدجتس الجهة يُشتق لكل طلب من تثبيتاتها — ويدجتس التطبيقات المثبَّتة فقط.
  4. الإدراج: أدمن الجهة يسحب الويدجت، والفاحص يعرض فورم الإعدادات المولَّد من settings_schema. المعاينة تُرسم بسياق الأدمن.
  5. نشر الصفحة: على الموقع العام يُرسم الويدجت بالسياق المجهول (اتحاد التقاطعين). الساكن يُخزَّن ناتجه cache_ttl ثانية (مفتاح الكاش يحمل بصمة الإعدادات — تعديل الإعدادات ونشره يظهر فوراً)؛ التفاعلي يُرسم حيّاً على كل طلب.
  6. إزالة التثبيت: الويدجت يختفي من كتالوج المحرر فوراً. البلوكات المدرَجة سابقاً على صفحات منشورة تفقد مرجعها فتسقط في مسار الفشل نفسه: فراغ صامت، لا خطأ على موقع الجهة. (الجهة تحذف البلوك اليتيم من محررها متى شاءت.)
  7. تحديث الإصدار: الكتالوج يعكس manifest الإصدار المثبَّت. حقل settings جديد لا يظهر في بلوكات قديمة إلا بقيمته الافتراضية من كودك (?? $default)؛ حذف ويدجت أو تغيير key يترك البلوكات القديمة يتيمة كما في الإزالة.

تسجيل الـ alias والمكوّن

// في ServiceProvider::boot() لتطبيقك — الصيغة المسطَّحة إلزامية.
use Livewire\Livewire;

Livewire::component(
    'my-app-landing-register',
    \Modules\MyApp\Livewire\Landing\RegisterWidget::class,
);
<?php

namespace Modules\MyApp\Livewire\Landing;

use Livewire\Component;

class RegisterWidget extends Component
{
    /** قيم settings_schema للّغة المعروضة، كما خزّنها المحرر. */
    public array $settings = [];

    public function render()
    {
        // القراءة حصراً عبر App\Services\PartnerApi\* — الخدمة تفحص
        // النطاق داخلياً وترفض ما ليس في السياق.
        $grades = app(\App\Services\PartnerApi\Education\Grades\ListGrades::class)(
            onlyActive: true,
        );

        return view('my-app::livewire.landing.register-widget', [
            'grades' => $grades->data,
            'title' => $this->settings['title'] ?? 'سجّل الآن',
        ]);
    }
}

هذا كل شيء للويدجت الساكن. التفاعلي يرث PublicWidgetComponent بدل Component، يعيش تحت app/LandingWidgets/، ويكتب عبر withPublicWrite() كما في قسمه أعلاه.

الأخطاء الشائعة وحلولها

العرَض السبب الحل
الويدجت لا يُرسم أبداً alias يحوي :: الصيغة المسطَّحة في livewire_component وفي Livewire::component() معاً
«لا يعمل بلا سبب» أثناء التطوير الفشل صامت + كاش فشل 30 ثانية افحص سجلات okta-web؛ لا تعتمد على تحديث الصفحة كاختبار
يعمل في المعاينة ويفرغ على الموقع العام نطاق غير موسوم public_surface أو غير معلَن في scopes المعاينة بسياق أدمن كامل؛ اختبر بالسياق الفقير قبل النشر
أزرار لا تستجيب على الموقع ويدجت static فيه wire:* التفاعلية تتطلّب interaction=interactive وعقده كاملاً
كل كتابة تُرفض برسالة حاجز نسيان @include(... form-guard) مرة واحدة داخل عنصر الجذر
419 عند أول نقرة على نطاق مخصص SESSION_DOMAIN مثبَّت على نطاق المنصة شرط نشر — انظر فخّ SESSION_DOMAIN
قيمة إعداد تظهر نصاً حرفياً كل قيم settings تُهرَّب عند الرسم مقصود — لا HTML عبر الإعدادات أبداً

قائمة تحقق قبل النشر

  • integrationType=embedded والكتلة landing_widgets ≤ 10 ويدجت.
  • كل key يطابق ^[a-z][a-z0-9_]{1,39}$ وفريد داخل التطبيق — واسمه نهائي.
  • livewire_component بالصيغة المسطَّحة (لا ::)، ومسجَّل بـ Livewire::component() في boot().
  • scopes هي الحد الأدنى الذي يحتاجه الرسم فعلاً — لا نسخ لقائمة نطاقات التثبيت.
  • لا استدعاء لأي نطاق أشخاص/ماليات من مسار الرسم العام — لن يُوسم public_surface أبداً.
  • settings_schema ≤ 20 حقلاً، الأنواع من الثمانية، وكل قراءة settings خلف ?? $default.
  • الويدجت الساكن بلا أي wire:* في الـ blade.
  • التفاعلي: يرث PublicWidgetComponent، تحت app/LandingWidgets/، وكل كتابة داخل withPublicWrite().
  • التفاعلي: @include('landing-builder::blocks.partner-widget.partials.form-guard') مرة واحدة داخل عنصر الجذر.
  • public_writes كلها موسومة public_writable في الكتالوج وinteraction=interactive.
  • التفاعلي مجرَّب على نطاق جهة مخصص إن كانت جهاتك تستعملها.
  • الرسم مجرَّب بسياق فقير (بلا مستخدم، بنطاقات scopes فقط) وبِلغتي الصفحة وبحالة بيانات فارغة.
  • preview.thumbnail_url رابط https يعمل، والبطاقة مراجَعة في المحرر.

عرض الدليل كاملاً في صفحة واحدة · ↧ Markdown خام