---
feature: Static pages and FAQs
slug: content
surface: delivery
consumer: flutter
version: 1.0.0
status: ready
brd_version: "1.3"
brd_ids: [DRIV-001, DRIV-019]
business_rules: [BR-01]
decisions: [DEC-18, TD-12, TD-30, TD-175]
schema_tables: [static_pages, static_page_translations, faqs, faq_translations]
screens: ["delivery--auth--*", "delivery--account--*"]
related_docs: [../admin/content.md, auth.md, support.md]
last_updated: 2026-11-08
---

# Static pages and FAQs — Flutter Integration Guide (delivery)

> **Base URL:** `{HOST}/api/v1/delivery` · Shared rules (headers, envelope,
> errors, pagination, Dio setup): [API_CONVENTIONS.md](../API_CONVENTIONS.md)

## Version history

Newest first. Never edit an old row.

| Version | Date | Type | Summary | App action required |
|---------|------|------|---------|---------------------|
| 1.0.0 | 2026-11-08 | Initial | Public `GET /pages/{slug}` and `GET /faqs` for the driver app (TD-175). | Link the consent checkboxes and the help screen to these endpoints. |

## Changelog

### 1.0.0 — 2026-11-08
- **Added:** `GET /pages/{slug}` (`delivery.pages.show`) and `GET /faqs` (`delivery.faqs.index`). Both are public: no token, so the consent screens can show the terms before login.
- **Changed:** —
- **Fixed:** the earlier `/client/faqs` and `/client/pages/{type}` were unregistered because they returned 500 (TD-30); they are replaced by these endpoints.
- **Removed / Deprecated:** —
- **Breaking:** No
- **Endpoints affected:** `GET /delivery/pages/{slug}`, `GET /delivery/faqs`
- **App action required:** Link the "terms", "privacy" and "female declaration" consent labels (register and login screens) to `GET /pages/{slug}`; build the help/FAQ list from `GET /faqs`.
- **BRD / decisions:** DRIV-001, DRIV-019, BR-01, DEC-18, TD-175.

---

## 1. Business summary

Registration requires `accept_terms`, `accept_policies` and the female-only guarantee (BR-01, DEC-18), so the driver app must show the legal texts before the user has a token. The admin edits them in [admin/content](../admin/content.md). This app reads the page written for the **driver** audience and falls back to the page written for **all** apps when there is none. The FAQ list is the active questions written for the driver audience.

## 2. User flow and screens

| Step | Screen / widget | Endpoint | Notes |
|------|-----------------|----------|-------|
| 1 | Consent checkboxes (register) | `GET /pages/terms`, `GET /pages/privacy`, `GET /pages/female_declaration` | Open the page in a full-screen reader; keep the checkbox unchecked until the user ticks it. Store `version` if you want to show which version was accepted. |
| 2 | Help / FAQ screen | `GET /faqs` | Expandable list in the order returned. Empty state when `data` is `[]`. |
| 3 | About / cancellation policy | `GET /pages/about`, `GET /pages/cancellation_policy` | Same reader as step 1. |

## 3. Endpoints

### 3.1 Read a page

`GET /api/v1/delivery/pages/{slug}`

| | |
|---|---|
| Auth | None. Works without a token. |
| Extra headers | `Accept-Language: ar` or `en` |
| Rate limit | The default `api` limiter (60 per minute per IP) |

**Path parameters**

| Name | In | Type | Required | Rules | Description |
|------|----|------|----------|-------|-------------|
| `slug` | path | string | yes | one of `terms`, `privacy`, `cancellation_policy`, `about`, `female_declaration` | Which page to show. |

**Success — `200`** (Postman: `delivery.pages.show` › "200 Page of the app audience")

```json
{
    "status": "success",
    "message": "تم جلب الصفحة.",
    "data": {
        "id": "01a1bab1-5759-7149-a459-991e2d4bc790",
        "slug": {
            "value": "terms",
            "label": "الشروط والأحكام"
        },
        "title": "عنوان driver",
        "content": "نص driver",
        "version": 3,
        "updated_at": "2026-11-08T10:25:46+00:00"
    }
}
```

**Response fields**

| Field | Type | Nullable | Description |
|-------|------|----------|-------------|
| `id` | uuid string | no | Public id of the page. |
| `slug` | `{value, label}` | no | `value` is the stable key; `label` is localized. |
| `title` | string | yes | Request locale, falling back to Arabic. |
| `content` | string | yes | Page body in the request locale, falling back to Arabic. Plain text or the markup the admin wrote; render it in a scrollable reader. |
| `version` | int | no | Bumped by the backend whenever any locale changes. |
| `updated_at` | ISO 8601 with offset | no | Last change. |

Postman: `delivery.pages.show` › "200 English text", "200 Falls back to the shared page".

**Errors**

| HTTP | `error_code` | When | What the app should do |
|------|--------------|------|------------------------|
| 404 | `ENTITY_NOT_FOUND` | Unknown slug, or no page exists for this app or for all apps | Hide the link or show a generic "not available" message |

Postman: `delivery.pages.show` › "404 Unknown slug", "404 Valid slug without a page".

```json
{
    "status": "fail",
    "message": "الصفحة غير موجودة.",
    "data": {
        "error_code": "ENTITY_NOT_FOUND",
        "correlation_id": "9ea834fe-3ec7-4d79-9111-357e8aadb946"
    }
}
```

### 3.2 List FAQs

`GET /api/v1/delivery/faqs`

| | |
|---|---|
| Auth | None. Works without a token. |
| Extra headers | `Accept-Language: ar` or `en` |
| Rate limit | The default `api` limiter |

No query parameters. The list is short, so it is **not paginated**: `data` is an array ordered by `sort_order`, then creation order. Inactive questions and the other apps' questions are never returned.

**Success — `200`** (Postman: `delivery.faqs.index` › "200 FAQs of the app audience")

```json
{
    "status": "success",
    "message": "تم جلب الأسئلة الشائعة.",
    "data": [
        {
            "id": "01a1bab1-58f8-72e7-86ea-9ef3fa12c965",
            "question": "س١",
            "answer": "ج١",
            "sort_order": 1
        },
        {
            "id": "01a1bab1-58f5-717f-b21e-3b1d4e79a923",
            "question": "س٢",
            "answer": "ج٢",
            "sort_order": 2
        }
    ]
}
```

| Field | Type | Nullable | Description |
|-------|------|----------|-------------|
| `id` | uuid string | no | Public id. |
| `question` | string | yes | Request locale, falling back to Arabic. |
| `answer` | string | yes | Request locale, falling back to Arabic. |
| `sort_order` | int | no | Display order, ascending. |

**Errors:** none specific. An empty list is `200` with `data: []`.

## 4. Statuses and enums

| Enum | `value` | Meaning |
|------|---------|---------|
| `slug` | `terms`, `privacy`, `cancellation_policy`, `about`, `female_declaration` | The five static pages in the schema. |

## Data model

| Table (`db-schema.sql`) | Read / write | Notes |
|-------------------------|--------------|-------|
| `static_pages`, `static_page_translations` | read | Audience `driver` first, else `all`; unique per (slug, audience). |
| `faqs`, `faq_translations` | read | `is_active = 1`, audience `driver`. |

## 5. Dart models

```dart
class ContentPage {
  final String id; // uuid (TD-19)
  final EnumValue slug;
  final String? title;
  final String? content;
  final int version;
  final DateTime updatedAt;

  ContentPage.fromJson(Map<String, dynamic> j)
      : id = j['id'] as String,
        slug = EnumValue.fromJson(j['slug'] as Map<String, dynamic>),
        title = j['title'] as String?,
        content = j['content'] as String?,
        version = j['version'] as int,
        updatedAt = DateTime.parse(j['updated_at'] as String);
}

class Faq {
  final String id;
  final String? question;
  final String? answer;
  final int sortOrder;

  Faq.fromJson(Map<String, dynamic> j)
      : id = j['id'] as String,
        question = j['question'] as String?,
        answer = j['answer'] as String?,
        sortOrder = j['sort_order'] as int;
}
```

## 6. Business rules enforced by the backend

| Rule | What the backend does | What the user sees |
|------|-----------------------|--------------------|
| BR-01 / DEC-18 | The legal pages are readable without login so consent can be given before registration. | The consent label opens the page. |
| TD-175 | Audience resolution: the driver page wins over `all`. | The right text for this app. |

## 7. Notifications and real-time events

N/A.

## 8. Localization and UI notes

- Send `Accept-Language`; Arabic is the fallback when the English text is missing.
- Titles come from the API; the consent label text itself is static app copy.
- Render `content` in RTL for Arabic.

## 9. Integration checklist

- [ ] Consent labels open `GET /pages/{slug}` for `terms`, `privacy` and `female_declaration`
- [ ] 404 handled (hide the link or show "not available")
- [ ] FAQ screen shows the empty state
- [ ] Tested in `ar` (RTL) and `en`

## 10. Acceptance scenarios

| AC ID | Scenario | Expected |
|-------|----------|----------|
| AC-DRIV-001 (consent) | A signed-out user opens the terms | `200` with the driver page, or the shared page when none exists. Tests: `tests/Feature/Api/V1/Shared/PublicContentTest.php`. |

## 11. Open decisions and known limitations

| ID | Impact on this feature | Current behavior |
|----|------------------------|------------------|
| DEC-15 | SMS gateway | Not related to this feature. |
| — | Per-user record of the accepted page `version` | Not stored; the registration consent timestamp is the record (TD-17). |
