# صفحات الهبوط

صفحة الهبوط صفحة تحويل مركّزة لمنتج واحد. مستقلة عن كتالوج الواجهة — يمكنك امتلاك صفحة هبوط بدون منتج حي (لإطلاقات قادمة)، أو واحدة مرتبطة بمنتج لإعلانات مدفوعة.

يمكن بناء الأقسام (السلايدرات، نماذج الطلب، الزوار الوهميون، العدّاد التنازلي…) عبر الواجهة البرمجية بنقاط الأقسام المشروحة أدناه، أو من لوحة التحكم.

## حدود الخطة[​](#حدود-الخطة "رابط مباشر إلى حدود الخطة")

| الخطة                  | صفحات الهبوط (كل الحالات — المسودات محسوبة)      |
| ---------------------- | ------------------------------------------------ |
| Free                   | 0 (شراء لمرة واحدة: 1000 دج/مدى الحياة لكل منها) |
| Pro                    | 3                                                |
| Unlimited / Enterprise | غير محدود                                        |

يُطبَّق الحد على كل طرق الإنشاء، بما فيها الواجهة البرمجية، مع احتساب كل صفحة هبوط بما فيها المسودات. على متجر بلغ حدّه، يُرجع `POST /v1/landing-pages` (و`POST /v1/landing-pages/generate`) الخطأ `403 limit_reached`. ولا يستطيع متجر على الخطة المجانية إنشاء صفحة إلا ما دام لديه شراء صفحة هبوط لم يُستعمل بعد؛ ويُعلِّم `POST /v1/landing-pages` تلك الصفحة بـ `is_purchased: true`، وهذا ما يجعلها مرئية على واجهة المتجر.

## `GET /v1/landing-pages`[​](#get-v1landing-pages "رابط مباشر إلى get-v1landing-pages")

اسرد صفحات الهبوط. ترقيم بالمؤشّر. تُخدَم حديثًا في كل نداء، مثل `GET /v1/landing-pages/{id}`.

**المصادقة:** مفتاح منصة بصلاحية `landing_pages:read`، وتحتاجها `GET /v1/landing-pages/{id}` أيضًا. المفتاح الذي لا يحملها يتلقى `403 forbidden`.

### معاملات الاستعلام[​](#معاملات-الاستعلام "رابط مباشر إلى معاملات الاستعلام")

| المعامل  | النوع               | ملاحظات      |
| -------- | ------------------- | ------------ |
| `limit`  | int 1–200           | الافتراضي 50 |
| `cursor` | string              | غير شفاف     |
| `status` | `active` \| `draft` | تصفية        |

القيمة غير المعروفة في `status` تُتجاهَل فتُرجَع كل الصفحات بدل `400`.

### الاستجابة 200[​](#الاستجابة-200 "رابط مباشر إلى الاستجابة 200")

```
{

  "data": {

    "items": [

      {

        "id":            42,

        "title":         "Black T-Shirt — 30% off",

        "slug":          "black-tshirt-30-off",

        "public_url":    "https://your-store.example.com/landing/black-tshirt-30-off",

        "status":        "active",

        "language":      "ar",

        "product_id":    26,

        "views":         1543,

        "is_purchased":  false,

        "created_at":    "2026-03-01 10:00:00",

        "updated_at":    "2026-03-15 14:22:11"

      }

    ],

    "next_cursor": null,

    "has_more": false

  }

}
```

## `GET /v1/landing-pages/{id}`[​](#get-v1landing-pagesid "رابط مباشر إلى get-v1landing-pagesid")

تفاصيل مع `section_count`.

```
{

  "data": {

    "id":               42,

    "title":            "Black T-Shirt — 30% off",

    "slug":             "black-tshirt-30-off",

    "public_url":       "https://your-store.example.com/landing/black-tshirt-30-off",

    "status":           "active",

    "language":         "ar",

    "product_id":       26,

    "views":            1543,

    "is_purchased":     false,

    "meta_title":       "Black T-Shirt — Cotton 200gsm — 30% off | DZBuild",

    "meta_description": "Limited-time offer on our cotton black t-shirt.",

    "section_count":    7,

    "created_at":       "2026-03-01 10:00:00",

    "updated_at":       "2026-03-15 14:22:11"

  }

}
```

### مرجع الحقول[​](#مرجع-الحقول "رابط مباشر إلى مرجع الحقول")

| الحقل                             | ملاحظات                                                                                                                                                                                                                              |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `status`                          | `active` أو `draft` فقط بصرامة — لا توجد حالة `archived` لصفحات الهبوط.                                                                                                                                                              |
| `language`                        | `ar` أو `fr` أو `en`.                                                                                                                                                                                                                |
| `public_url`                      | العنوان الحي للصفحة: عنوان المتجر (نطاقه المخصّص متى صار فعّالًا، وإلا النطاق الفرعي)، ثم `/landing/` ثم الـ `slug`. يكون `null` ما دام المتجر بلا عنوان. استعمله كما هو بدل بناء الرابط بنفسك.                                      |
| `product_id`                      | المنتج المرتبط، أو `null`. القسم الذي يستقبل الطلبات (`order_form` و`order_button` و`product_offers`) يحتاج إليه أو إلى `settings.product_id` الخاص به. بعد ضبطه، يمكن لـ `PATCH` تغييره إلى منتج آخر لكن لا يمكن إرجاعه إلى `null`. |
| `views`                           | **للقراءة فقط.** تُحتسب في كل مرة تُشاهَد فيها الصفحة العامة؛ ولا تستطيع الواجهة البرمجية كتابتها ولا توجد طريقة لتصفيرها.                                                                                                           |
| `is_purchased`                    | `true` بمجرد شراء الصفحة نهائيًا (1000 دج/مدى الحياة). على الخطة المجانية هذا ما يجعل الصفحة مرئية على واجهة المتجر.                                                                                                                 |
| `section_count`                   | في نقطة التفاصيل فقط — عدّ حيّ لأقسام الصفحة يُحسب مع كل طلب.                                                                                                                                                                        |
| `meta_title` / `meta_description` | وسوم SEO. انظر الملاحظة تحت الإنشاء.                                                                                                                                                                                                 |

## `POST /v1/landing-pages` — إنشاء[​](#post-v1landing-pages--إنشاء "رابط مباشر إلى post-v1landing-pages--إنشاء")

**المصادقة:** مفتاح منصة بصلاحيتَي `landing_pages:write` و`landing_pages:read`. تقرأ الاستجابة الصفحة بعد حفظها، فمع `landing_pages:write` وحدها تُحفَظ الصفحة ويُرجع النداء `403 forbidden`؛ والأمر نفسه مع `PATCH` و`/publish`. **يتطلب `Idempotency-Key`.**

### الجسم[​](#الجسم "رابط مباشر إلى الجسم")

| الحقل              | النوع                     | إلزامي | ملاحظات                                                                                                                                                                                               |
| ------------------ | ------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title`            | string، من 1 إلى 255 بايت | ✅     | الحد يحسب البايتات لا الحروف: الحرف العربي يأخذ 2 بايت، فالعنوان العربي يتوقف عند نحو 127 حرفًا                                                                                                       |
| `slug`             | string                    |        | يُشتق تلقائيًا من `title` إن أُغفل. أما الـ slug الذي ترسله هنا فيُخزَّن **بدون** تطبيع — أرسل قيمة نظيفة                                                                                             |
| `status`           | `active` \| `draft`       |        | الافتراضي `draft`. أي قيمة أخرى تُحوَّل بصمت إلى `draft`                                                                                                                                              |
| `language`         | `ar` \| `fr` \| `en`      |        | الافتراضي `ar`. أي قيمة أخرى تُحوَّل بصمت إلى `ar`                                                                                                                                                    |
| `product_id`       | int                       |        | يجب أن ينتمي إلى متجرك؛ الصفحة تربط بهذا المنتج                                                                                                                                                       |
| `meta_title`       | string ≤ 255              |        | عنوان SEO. إن أغفلته عبر الواجهة البرمجية يُخزَّن ويُرجَع كـ `null` (بخلاف نموذج لوحة التحكم الذي ينسخ `title` إليه). ومع ذلك تعرض الصفحة العامة `title` كبديل، فعنوان الصفحة الظاهر صحيح في الحالتين |
| `meta_description` | string                    |        | وصف SEO                                                                                                                                                                                               |

تُجعل الـ slugs فريدة داخل متجرك بإلحاق `-2` و`-3` وهكذا. وإن كان أساس الـ slug فارغًا فيُستعمل `landing-` متبوعًا بـ 6 أحرف hex.

### الأخطاء[​](#الأخطاء "رابط مباشر إلى الأخطاء")

| الكود                                                       | السبب                                                                                                       |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `bad_request` "Body must be valid JSON"                     | Content-Type خاطئ أو JSON تالف                                                                              |
| `bad_request` "title is required (1-255 chars)"             | العنوان مفقود أو طويل جدًا                                                                                  |
| `bad_request` "product\_id N does not belong to this store" | معرّف من متجر آخر                                                                                           |
| `limit_reached` (403)                                       | بلغ المتجر حد صفحات الهبوط في خطته (المسودات محسوبة). على الخطة المجانية: لم يبقَ شراء صفحة هبوط غير مستعمل |

### الطلب[​](#الطلب "رابط مباشر إلى الطلب")

```
curl -X POST 'https://api.dzbuild.app/v1/landing-pages' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Content-Type: application/json" \

  -H "Idempotency-Key: $(uuidgen)" \

  -d '{

    "title":      "Black T-Shirt — 30% off",

    "language":   "ar",

    "product_id": 26,

    "status":     "draft"

  }'
```

تُرجع **200** (وليس 201) وبنفس شكل `GET /v1/landing-pages/{id}`. صفحة الهبوط الجديدة بدون أقسام: أضفها بنقاط الأقسام المشروحة أدناه. فحص النشر لا يجري عند الإنشاء، لذا أنشئ الصفحة كـ `draft` وانشرها بعد إضافة أقسامها؛ فالصفحة المُنشأة بـ `status: active` تظهر للزبائن فارغة.

## `PATCH /v1/landing-pages/{id}`[​](#patch-v1landing-pagesid "رابط مباشر إلى patch-v1landing-pagesid")

تحديث جزئي.

يتحقّق PATCH بصرامة أكبر من الإنشاء: `status` غير صالح يُرجع `400 bad_request` ("status must be active or draft")، و`language` غير صالح يُرجع `400` ("language must be ar, fr, or en") بدل التحويل الصامت. ويجب أن يبقى `title` بين 1 و255 بايت. أما الـ `slug` المُرسَل في PATCH فيُطبَّع، بخلاف الإنشاء. ضبط `status` على `active` يُشغّل فحص النشر (انظر `/publish` أدناه). وبعد أن يصير للصفحة منتج، يُرجع `product_id: null` الخطأ `422 product_required`؛ أرسل معرّف منتج آخر لتغيير المنتج.

```
curl -X PATCH 'https://api.dzbuild.app/v1/landing-pages/42' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Content-Type: application/json" \

  -H "Idempotency-Key: $(uuidgen)" \

  -d '{ "title": "Black T-Shirt — Spring promo" }'
```

تغيير العنوان يُعيد توليد `slug` تلقائيًا فقط إن لم تُرسل `slug` صراحةً. وعندما يتغيّر الـ `slug`، بتغيير العنوان أو صراحةً، يبقى الرابط القديم يعمل ويُحوِّل إلى الرابط الجديد.

## الأقسام[​](#الأقسام "رابط مباشر إلى الأقسام")

تعرض الصفحة أقسامها من الأعلى إلى الأسفل. قراءة الأقسام تحتاج إلى `landing_pages:read`، وكتابتها تحتاج إلى `landing_pages:write`، وكل كتابة تحتاج إلى `Idempotency-Key`.

| النقطة                                                | الجسم                                                                         | الاستجابة                                                                     |
| ----------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `GET /v1/landing-page-section-types`                  |                                                                               | `200`: `section_types`، كل عنصر فيها `type` مع `default_settings` الخاصة به   |
| `GET /v1/landing-pages/{id}/sections`                 |                                                                               | `200`: `landing_page_id` و`sections` بترتيب العرض (حتى 500، بدون ترقيم)       |
| `POST /v1/landing-pages/{id}/sections`                | `section_type` (أو `type`)، و`settings` اختياري                               | `201`: الـ `section` الجديد، مُضافًا في آخر الصفحة                            |
| `POST /v1/landing-pages/{id}/sections/batch`          | `sections`: حتى 30 كائنًا فيها `type` (أو `section_type`) و`settings` اختياري | `201`: الـ `sections` الجديدة. الكل أو لا شيء: عنصر واحد خاطئ يمنع حفظ الجميع |
| `POST /v1/landing-pages/{id}/sections/reorder`        | `sections`: كل معرّفات أقسام الصفحة، كل واحد مرة واحدة، بالترتيب الجديد       | `200`: الأقسام بترتيبها الجديد                                                |
| `PATCH /v1/landing-pages/{id}/sections/{section_id}`  | `settings`، و`replace` اختياري                                                | `200`: الـ `section` بعد التحديث                                              |
| `DELETE /v1/landing-pages/{id}/sections/{section_id}` |                                                                               | `200`: `{ "deleted": true, "id": 901, "landing_page_id": 42 }`                |

يحمل القسم `id` و`section_type` و`sort_order` و`settings`. والاستجابات التي تعيد قراءة الصفحة (القائمة وإعادة الترتيب و`PATCH`) تحمل أيضًا `created_at` و`updated_at`. الأنواع الـ 14 هي `image` و`order_form` و`order_button` و`free_text` و`contact_button` و`countdown` و`fake_visitors` و`special_offer` و`price_display` و`product_offers` و`custom_form` و`image_carousel` و`announcement_bar` و`testimonials`. اقرأ `GET /v1/landing-page-section-types` قبل الكتابة، حتى تطابق مفاتيح إعداداتك ما تعرضه الصفحة.

* الإعدادات التي ترسلها تُدمج فوق القيم الافتراضية للنوع، فنداء واحد يكفي لإنشاء قسم مضبوط بالكامل. ومع `PATCH` تُدمج فوق الإعدادات المخزّنة؛ أرسل `"replace": true` لتبدأ من جديد من القيم الافتراضية للنوع. الكائنات المتداخلة تُدمج مفتاحًا بمفتاح، أما القوائم مثل `slides` و`items` و`offers` و`fields` فتُستبدل كاملة. ولا يمكن تغيير نوع القسم.
* قسم `order_form` أو `order_button` أو `product_offers` يحتاج إلى منتج: `product_id` الخاص بالصفحة أو `settings.product_id` الخاص به. بدونه يُرجع النداء `422 landing_page_has_no_product`. و`settings.product_id` من متجر آخر يُرجع `422 validation_error`.
* حقول نموذج الطلب `show_name` و`show_phone` و`show_wilaya` تُحفَظ دائمًا بقيمة `true` في أي قسم يحملها.
* قائمة `slides` تقبل 20 عنصرًا على الأكثر، وقائمة `items` تقبل 30 على الأكثر (`422 too_many_items`). ولا يجوز أن تتجاوز الإعدادات المُرمَّزة 262144 بايت (`422 settings_too_large`). والنوع غير المعروف يُرجع `422 invalid_section_type`.
* عند الكتابة، الصفحة التي ليست من متجرك تُرجع `404 landing_page_not_found` (القائمة والفحص يُرجعان `404 not_found`)، والقسم غير الموجود على الصفحة يُرجع `404 section_not_found`. وقائمة إعادة الترتيب التي تكرّر قسمًا أو تُغفله تُرجع `422 validation_error`.
* تظهر تغييرات الأقسام في `GET /v1/changes`. يمكن التراجع عن التعديل والحذف وإعادة الترتيب بـ `POST /v1/changes/{id}/undo`، والقسم المحذوف الذي يعود بالتراجع يأخذ رقمًا جديدًا. أما إضافة قسم فلا يمكن التراجع عنها: احذفه بدل ذلك.

```
curl -X POST 'https://api.dzbuild.app/v1/landing-pages/42/sections/batch' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Content-Type: application/json" \

  -H "Idempotency-Key: $(uuidgen)" \

  -d '{

    "sections": [

      { "type": "announcement_bar" },

      { "type": "order_form" }

    ]

  }'
```

## `GET /v1/landing-pages/{id}/check`[​](#get-v1landing-pagesidcheck "رابط مباشر إلى get-v1landing-pagesidcheck")

يُبلغ عمّا سيجده الزبون معطّلًا في الصفحة. **المصادقة:** `landing_pages:read`.

تحمل الاستجابة `landing_page_id` و`status` و`product_id` و`sections` (عدد الأقسام المفحوصة) و`publishable` و`blocked_by` (أول رسالة مانعة، أو `null`) و`problems`. لكل مشكلة `code` و`severity` (`blocking` أو `warning`) و`message`؛ والمشاكل المرتبطة بقسم واحد تحمل أيضًا `section_id`.

| الكود                        | الخطورة    | المعنى                                                                                                                            |
| ---------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `empty_page`                 | `blocking` | الصفحة بلا أقسام، فتظهر فارغة                                                                                                     |
| `order_form_without_product` | `blocking` | قسم يستقبل الطلبات، لكن لا الصفحة ولا القسم يحدّدان منتجًا، فتُسجَّل الطلبات بـ 0 دج                                              |
| `section_product_not_found`  | `blocking` | قسم يشير إلى منتج ليس في متجرك                                                                                                    |
| `no_order_form`              | `warning`  | لا شيء في الصفحة يستقبل الطلبات                                                                                                   |
| `multiple_order_forms`       | `warning`  | يظهر أكثر من `order_form` أو `order_button` واحد                                                                                  |
| `variants_need_page_product` | `warning`  | قسم يحدّد منتجًا بمتغيرات والصفحة بلا منتج. أدوات اختيار المتغيرات لا تظهر إلا من منتج الصفحة نفسه، فاضبط `product_id` على الصفحة |

يُرفض النشر ما دامت هناك مشكلة مانعة (انظر أدناه).

## `POST /v1/landing-pages/{id}/publish`[​](#post-v1landing-pagesidpublish "رابط مباشر إلى post-v1landing-pagesidpublish")

اختصار: تحويل الحالة إلى `active`. يكافئ `PATCH ... { status: "active" }`، ويُرفض بالطريقة نفسها: ما دام `GET /v1/landing-pages/{id}/check` يُبلغ عن مشكلة مانعة، يُرجع النداء `422 page_not_publishable` ويحمل الخطأ قائمة `problems`. أما التعديلات التي لا ترسل `status` فلا تمرّ بهذا الفحص، حتى على صفحة منشورة.

```
curl -X POST 'https://api.dzbuild.app/v1/landing-pages/42/publish' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Idempotency-Key: publish-42-$(date +%s)"
```

## `POST /v1/landing-pages/generate`[​](#post-v1landing-pagesgenerate "رابط مباشر إلى post-v1landing-pagesgenerate")

يبني صفحة هبوط من أحد منتجاتك بالذكاء الاصطناعي. يُرجع `202` فورًا مع مهمة تتابعها؛ وتستغرق العملية نحو دقيقتين. **المصادقة:** صلاحية `ai:generate`. المفاتيح المُنشأة من لوحة التحكم أو عبر `POST /v1/keys` والتطبيقات الخارجية لا تحملها وتتلقى `403 forbidden`؛ أما اتصالات Claude وChatGPT وDZBuild Copilot فتحملها. **يتطلب `Idempotency-Key`.**

| الحقل         | النوع                | إلزامي | ملاحظات                                                                         |
| ------------- | -------------------- | ------ | ------------------------------------------------------------------------------- |
| `title`       | string               | ✅     | 3 أحرف على الأقل، ويُقطع عند 255                                                |
| `product_id`  | int                  | ✅     | منتج نشِط من متجرك فيه صورة واحدة على الأقل                                     |
| `description` | string               |        | وصف موجز للصفحة، يُقطع عند 2000 حرف                                             |
| `language`    | `ar` \| `fr` \| `en` |        | الافتراضي `ar`                                                                  |
| `size`        | `medium` \| `tall`   |        | الافتراضي `medium`. تكلفة `medium` هي 20 نقطة ذكاء اصطناعي، وتكلفة `tall` هي 45 |

تحمل الاستجابة `task_id` و`size` و`credits_charged` و`eta_seconds` و`poll`. تُخصم النقاط عند بدء العملية وتُعاد إذا فشلت أو تجاوزت المهلة. الأخطاء: `400 bad_request` (العنوان مفقود أو أقصر من 3 أحرف)، `402 quota_exceeded` (نقاط الذكاء الاصطناعي غير كافية)، `403 limit_reached` (حد صفحات الهبوط في الخطة، مع `limit` و`current`)، `409 already_processing` (توليد واحد لكل متجر في الوقت نفسه)، `422 product_required` أو `product_not_found` أو `product_has_no_image`، `429 rate_limited` أو `too_many_concurrent`، `503 provider_unavailable` (بدون خصم أي نقاط).

تابِع `GET /v1/landing-pages/generate/{task_id}` بصلاحية `landing_pages:read`. تُرجع `task_id` و`status` (`processing` أثناء بناء الصفحة، ثم `completed` أو `failed`) و`landing_page_id` بمجرد وجود الصفحة و`current_step` و`error`. العملية التي تبقى قيد المعالجة بعد 10 دقائق تُعلَّم `failed` بالخطأ `timeout` عند المتابعة التالية، وتُعاد نقاطها.

## `DELETE /v1/landing-pages/{id}`[​](#delete-v1landing-pagesid "رابط مباشر إلى delete-v1landing-pagesid")

حذف نهائي. وتُحذف أقسام الصفحة معها.

```
curl -X DELETE 'https://api.dzbuild.app/v1/landing-pages/42' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Idempotency-Key: del-42"
```

الاستجابة: `{ "data": { "deleted": true, "id": 42 } }`.
