# CMS Builder — Panduan Lengkap untuk Developer

> Dokumentasi teknikal untuk sistem FrontendSite CMS Builder.
> Rujukan cepat faham arsitektur, component rendering, content modules, menu, routing, dan seeding.
>
> **Auth Pages:** Sistem autentikasi frontend user (login, register, forgot/reset password, email verification) dibina sebagai CMS Pages. Rujuk [CMS-AUTH-SYSTEM.md](./CMS-AUTH-SYSTEM.md).

---

## 1. Konsep Asas

CMS Builder membolehkan pembinaan website **dynamic tanpa coding** melalui 3 entiti utama:

| Entiti | Table | Fungsi |
|--------|-------|--------|
| **Component** | `frontend_components` | Blok content boleh guna semula (Blade template) |
| **Site** | `frontend_sites` | Website dengan header, footer, layout tersendiri |
| **Page** | `frontend_pages` | Halaman dalam site, mengandungi komponen |

### 3 Langkah Setup

```
Step 1: Buat Component  →  Step 2: Buat Site  →  Step 3: Buat Page
                                                      ↓
                                               Attach component ke page
```

#### Step 1: Buat Component

Admin panel: `/admin/frontend-site/components/create`

| Field | Contoh | Penerangan |
|-------|--------|------------|
| `code` | `hero_banner` | Unik, jadi variable `$hero_banner` dalam Blade |
| `name` | Hero Banner | Nama paparan |
| `type` | `PHP` / `HTML` / `JS` / `CSS` | `PHP` jika guna query DB |
| `category` | `Header` / `Section` / `Content` / `Footer` / `Panel` / `Modal` | Kumpulan komponen |
| `content` | Blade template | Content sebenar komponen |

#### Step 2: Buat Site

Admin panel: `/admin/frontend-site/sites/create`

| Field | Penerangan |
|-------|------------|
| `name` | Nama website |
| `slug` | URL segment (unique) |
| `header_fk` | Pilih component untuk header (contoh: `navbar`) |
| `footer_fk` | Pilih component untuk footer (contoh: `footer`) |
| `layout` | Blade layout yang wrap header + page + footer |
| `is_default` | Site utama bila access `/` |

#### Step 3: Buat Page & Attach Component

Admin panel: `/admin/frontend-site/pages/create`

1. Pilih `site`
2. Isi `name`, `slug`
3. Attach component — tick component dalam list, set `sort_order`
4. Isi `page_content` (optional) — content utama page, guna `$hero_banner`, `$features` dll
5. Save → `BladeSyncService::syncAll()` akan sync ke storage

---

## 2. Styling — CSS

Ada 3 tempat boleh letak CSS:

### 2.1 Dalam Component (Inline)

Letak `<style>` terus dalam `content` component:

```blade
<style>
.cms-hero { background: linear-gradient(135deg, #0f172a, #1e293b); padding: 6rem 2rem; text-align: center; color: white; }
.cms-hero h1 { font-size: 3rem; margin: 0; }
</style>
<section class="cms-hero">
    <h1>Welcome to {{ $siteName }}</h1>
</section>
```

### 2.2 Dalam Site Layout

Letak `<style>` dalam field `layout` site, CSS dikongsi semua page:

```blade
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #1e293b; }
</style>
{!! $__header__ !!}
<main>{!! $__page__ !!}</main>
{!! $__footer__ !!}
```

### 2.3 Dalam Page Layout (Custom)

Tandakan `use_custom_layout = true` pada page, layout guna `LAYOUTS/PAGE/{siteSlug}/{slug}.blade.php`:

```
STORAGE PATH:
  LAYOUTS/SITE/{site-slug}.blade.php       ← Site layout (default)
  LAYOUTS/PAGE/{site-slug}/{slug}.blade.php ← Page layout (custom)
```

### CSS Classes Standard (Guna dalam Component)

```css
.cms-section { padding: 4rem 2rem; font-family: sans-serif; }
.cms-section--alt { background: #f8fafc; }
.cms-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; max-width: 1200px; margin: 0 auto; }
.cms-grid--sm { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1rem; }
.cms-card { background: white; border-radius: 1rem; overflow: hidden; border: 1px solid #e2e8f0; transition: 0.3s; text-decoration: none; color: inherit; display: block; }
.cms-card:hover { box-shadow: 0 10px 40px rgba(0,0,0,0.08); transform: translateY(-2px); }
.cms-card-img { width: 100%; height: 200px; object-fit: cover; display: block; }
.cms-thumb { width: 100%; height: 120px; object-fit: cover; border-radius: 0.75rem; border: 1px solid #e2e8f0; cursor: pointer; transition: 0.3s; }
.cms-thumb:hover { transform: scale(1.05); }
.cms-download-item { display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.5rem; background: white; border-radius: 0.75rem; border: 1px solid #e2e8f0; text-decoration: none; color: inherit; transition: 0.2s; }
.cms-download-item:hover { border-color: #3b82f6; }
.cms-slide { border-radius: 1rem; overflow: hidden; position: relative; height: 250px; background-size: cover; background-position: center; display: flex; align-items: flex-end; padding: 2rem; color: white; }
```

---

## 3. Navbar & Menu — $menuTree

### 3.1 $menuTree — Bagaimana Ia Dihasilkan

`PortalHandler::handle()` akan:

```
1. Query RoleMapping:
   → role_code = 'public'
   → category = 'navbar'
   → site_id = current site

2. Bina recursive tree berdasarkan parent_id → menu_id:

   $buildTree = function($parentId) use (&$buildTree, $mappings) {
       $branch = [];
       foreach ($mappings as $mapping) {
           if ($mapping->parent_id == $parentId) {
               $item = $mapping->menu->toArray();
               $children = $buildTree($mapping->menu_id);
               if ($children) $item['children'] = $children;
               $branch[] = $item;
           }
       }
       return $branch;
   };
   $menuTree = $buildTree(0);

3. Pass ke semua component/page/layout sebagai:
   $vars['menuTree'] = $menuTree
```

Struktur `$menuTree`:

```php
[
    [
        'menu_name' => 'About',
        'menu_link' => '/about',
        'menu_icon' => null,
        'menu_id' => 1,
        'children' => [
            [
                'menu_name' => 'Team',
                'menu_link' => '/team',
                'children' => [],
            ],
        ],
    ],
    [
        'menu_name' => 'Contact',
        'menu_link' => '/contact',
        'children' => [],
    ],
]
```

### 3.2 Manual Menu vs Auto Fallback

```
ADA role_mapping untuk public/navbar?
  ├── YA → $menuTree = nested tree dari frontend_menu + frontend_role_mapping
  └── TIDAK → $menuTree = flat list semua pages dalam site (auto fallback)
```

**Manual Menu:** Admin create di `Frontend Menu` → `Assign Menu` → pilih role `public`, category `main-navbar`.

**Auto Fallback:** Jika tiada langsung `RoleMapping` untuk `public`/`navbar`, system auto guna semua `FrontendPage` dalam site sebagai flat menu.

### 3.3 Link Auto-Prefix

Link `/about` auto jadi `/{siteSlug}/about` dalam component:

```blade
@php
$link = $item['menu_link'] ?? '#';
if (str_starts_with($link, '/') && !str_starts_with($link, '//') && !filter_var($link, FILTER_VALIDATE_URL)) {
    $link = '/' . $siteSlug . $link;
}
@endphp
<a href="{{ $link }}">{{ $item['menu_name'] }}</a>
```

### 3.4 Component Navbar — Contoh Lengkap

```blade
@php
$buildLink = function($link) use ($siteSlug) {
    if (str_starts_with($link, '/') && !str_starts_with($link, '//') && !filter_var($link, FILTER_VALIDATE_URL))
        return '/' . $siteSlug . $link;
    return $link;
};
@endphp
<nav style="background:#0f172a;padding:0 2rem;display:flex;align-items:center;justify-content:space-between;height:70px;">
    <a href="/{{ $siteSlug }}" style="color:white;font-size:1.25rem;font-weight:700;text-decoration:none;">{{ $siteName }}</a>
    <div style="display:flex;gap:0.5rem;">
        @foreach($menuTree as $item)
        <div style="position:relative;"
             onmouseover="var d=this.querySelector('.dropdown');if(d)d.style.display='block';"
             onmouseout="var d=this.querySelector('.dropdown');if(d)d.style.display='none';">
            <a href="{{ $buildLink($item['menu_link']) }}" style="color:#cbd5e1;text-decoration:none;padding:0.5rem 1rem;">
                {{ $item['menu_name'] }}
            </a>
            @if(!empty($item['children']))
            <div class="dropdown" style="display:none;position:absolute;top:100%;left:0;background:#1e293b;min-width:180px;">
                @foreach($item['children'] as $child)
                <a href="{{ $buildLink($child['menu_link']) }}" style="display:block;color:#94a3b8;padding:0.4rem 0.75rem;">
                    {{ $child['menu_name'] }}
                </a>
                @endforeach
            </div>
            @endif
        </div>
        @endforeach
    </div>
</nav>
```

### 3.5 Variable Tersedia dalam Komponen

| Variable | Type | Sumber |
|----------|------|--------|
| `$siteSlug` | string | `$site->slug` |
| `$siteName` | string | `$site->name` |
| `$sitePages` | collection | Semua page dalam site |
| `$menus` | collection | Flat list `Menu` models |
| `$menuTree` | array | Nested menu (parent-child) |
| `$__entry__` | string | Output entry script (rendered) |
| `$__header__` | string | Rendered header HTML |
| `$__page__` | string | Rendered page content HTML |
| `$__footer__` | string | Rendered footer HTML |
| `$_shared` | stdClass | Object untuk sharing data antara komponen |

---

## 4. Content Modules — Cara Query 7 Jenis Content

Untuk papar data content dalam component, guna `@php` dalam component content (type: PHP).

### Pattern Asas

```blade
@php
$items = \App\Models\Backend\ContentArticle::with('translations')
    ->where('article_status', 'ACTIVE')
    ->latest()
    ->take(3)
    ->get();
@endphp
@if($items->count())
<section class="cms-section">
    <h2>Latest News</h2>
    @foreach($items as $item)
        @php
        $t = $item->translations->firstWhere('article_translation_main', 1)
             ?? $item->translations->first();
        @endphp
        <div class="cms-card">
            <h3>{{ $t?->article_translation_title ?? 'Untitled' }}</h3>
        </div>
    @endforeach
</section>
@endif
```

### 4.1 ContentArticle

**Table:** `content_article`, PK: `article_id`
**Translation:** `content_article_translation`

```blade
@php
$items = \App\Models\Backend\ContentArticle::with('translations')
    ->where('article_status', 'ACTIVE')
    ->latest()
    ->take(3)
    ->get();
@endphp
@if($items->count())
<section class="cms-section">
    <h2>Articles</h2>
    <div class="cms-grid">
        @foreach($items as $item)
        @php
            $t = $item->translations->firstWhere('article_translation_main',1) ?? $item->translations->first();
            $img = $item->article_image ? (str_starts_with($item->article_image, 'http') ? $item->article_image : Storage::url($item->article_image)) : null;
        @endphp
        <div class="cms-card">
            @if($img)<img src="{{ $img }}" class="cms-card-img">@endif
            <div style="padding:1.25rem;">
                <small>{{ $item->article_category }}</small>
                <h3>{{ $t?->article_translation_title ?? 'Untitled' }}</h3>
                <p>{{ Str::limit(strip_tags($t?->article_translation_content ?? ''), 120) }}</p>
            </div>
        </div>
        @endforeach
    </div>
</section>
@endif
```

### 4.2 ContentSlider

**Table:** `content_slider`, PK: `slider_id`
**Translation:** `content_slider_translation` (gambar dalam translation)

```blade
@php
$items = \App\Models\Backend\ContentSlider::with('translations')
    ->where('slider_status', 'ACTIVE')
    ->latest()
    ->take(3)
    ->get();
@endphp
@if($items->count())
<section class="cms-section">
    <h2>Sliders</h2>
    @foreach($items as $item)
    @php
        $t = $item->translations->firstWhere('slider_translation_main',1) ?? $item->translations->first();
        $img = $t?->slider_translation_img ? (str_starts_with($t->slider_translation_img, 'http') ? $t->slider_translation_img : Storage::url($t->slider_translation_img)) : null;
    @endphp
    <div class="cms-slide" @if($img) style="background-image:url('{{ $img }}')" @endif>
        <h3>{{ $t?->slider_translation_title ?? 'Banner' }}</h3>
    </div>
    @endforeach
</section>
@endif
```

### 4.3 ContentApplication

**Table:** `content_applications`, PK: `application_id`
**Translation:** `content_application_translations`

```blade
@php
$items = \App\Models\Backend\ContentApplication::with('translations')
    ->where('application_status', 'ACTIVE')
    ->latest()
    ->take(3)
    ->get();
@endphp
@if($items->count())
<section class="cms-section cms-section--alt">
    <h2>Applications</h2>
    <div class="cms-grid">
        @foreach($items as $item)
        @php
            $t = $item->translations->firstWhere('application_translation_main',1) ?? $item->translations->first();
        @endphp
        <div class="cms-card" style="padding:1.5rem;">
            <h3>{{ $t?->application_translation_title ?? 'Service' }}</h3>
            <small>{{ $item->application_cat }}</small>
            @if($item->application_url)
            <br><a href="{{ $item->application_url }}" target="_blank">Apply Now</a>
            @endif
        </div>
        @endforeach
    </div>
</section>
@endif
```

### 4.4 ContentDownload

**Table:** `content_downloads`, PK: `download_id`
**Note:** Translation guna self-referencing `download_parent_id` (bukan table berasingan)

```blade
@php
$items = \App\Models\Backend\ContentDownload::where('download_status', 'ACTIVE')
    ->latest()
    ->take(4)
    ->get();
@endphp
@if($items->count())
<section class="cms-section cms-section--alt">
    <h2>Downloads</h2>
    @foreach($items as $item)
    <a href="{{ $item->download_file ? Storage::url($item->download_file) : '#' }}" class="cms-download-item" style="display:block;margin-bottom:0.5rem;">
        <span>{{ $item->download_title ?? 'Document' }}</span>
        <small>{{ $item->download_category ?? '' }}</small>
    </a>
    @endforeach
</section>
@endif
```

### 4.5 ContentImage

**Table:** `content_images`, PK: `image_id`
**Note:** Translation guna self-referencing `image_parent_id`

```blade
@php
$items = \App\Models\Backend\ContentImage::where('image_status', 'ACTIVE')
    ->latest()
    ->take(6)
    ->get();
@endphp
@if($items->count())
<section class="cms-section">
    <h2>Images</h2>
    <div class="cms-grid cms-grid--sm">
        @foreach($items as $item)
        @php
            $url = $item->image_file ? (str_starts_with($item->image_file, 'http') ? $item->image_file : Storage::url($item->image_file)) : null;
        @endphp
        @if($url)
        <img src="{{ $url }}" class="cms-thumb" alt="{{ $item->image_title ?? '' }}">
        @endif
        @endforeach
    </div>
</section>
@endif
```

### 4.6 ContentPhotoGallery

**Table:** `content_photo_gallery`, PK: `gallery_id`
**Translation:** `content_photo_gallery_translations`
**Child Photos:** `content_photo_list` (FK: `photo_gallery_id`)

```blade
@php
$items = \App\Models\Backend\ContentPhotoGallery::with('translations', 'photoLists')
    ->where('gallery_status', 'ACTIVE')
    ->latest()
    ->take(4)
    ->get();
@endphp
@if($items->count())
<section class="cms-section cms-section--alt">
    <h2>Photo Galleries</h2>
    <div class="cms-grid">
        @foreach($items as $item)
        @php
            $t = $item->translations->firstWhere('gallery_translation_main',1) ?? $item->translations->first();
            $thumb = $item->gallery_thumbnail
                ? (str_starts_with($item->gallery_thumbnail, 'http') ? $item->gallery_thumbnail : Storage::url($item->gallery_thumbnail))
                : null;
        @endphp
        <a href="/{{ $siteSlug }}/{{ $item->gallery_code }}" class="cms-card">
            @if($thumb)<img src="{{ $thumb }}" class="cms-card-img">@endif
            <div style="padding:1.25rem;">
                <h3>{{ $t?->gallery_translation_title ?? 'Gallery' }}</h3>
                <span>{{ $item->photoLists->count() }} photos</span>
            </div>
        </a>
        @endforeach
    </div>
</section>
@endif
```

### 4.7 ContentVideo

**Table:** `content_video`, PK: `video_id`
**Translation:** `content_video_translation`

```blade
@php
$items = \App\Models\Backend\ContentVideo::with('translations')
    ->where('video_status', 'ACTIVE')
    ->latest()
    ->take(3)
    ->get();
@endphp
@if($items->count())
<section class="cms-section">
    <h2>Videos</h2>
    <div class="cms-grid">
        @foreach($items as $item)
        @php
            $t = $item->translations->firstWhere('video_translation_main',1) ?? $item->translations->first();
            $img = $item->video_img ? (str_starts_with($item->video_img, 'http') ? $item->video_img : Storage::url($item->video_img)) : null;
        @endphp
        <div class="cms-card">
            @if($img)<img src="{{ $img }}" class="cms-card-img">@endif
            <div style="padding:1.25rem;">
                <h3>{{ $t?->video_translation_title ?? 'Untitled' }}</h3>
                @if($item->video_source == 'YOUTUBE' && $item->video_url)
                <a href="{{ $item->video_url }}" target="_blank">Watch on YouTube</a>
                @endif
            </div>
        </div>
        @endforeach
    </div>
</section>
@endif
```

### Image URL Pattern (WAJIB Guna)

Semua field gambar guna pattern ini:

```blade
@php
$img = $item->article_image
    ? (str_starts_with($item->article_image, 'http')
        ? $item->article_image
        : Storage::url($item->article_image))
    : null;
@endphp
```

Sebab: gambar boleh dari **external URL** (picsum.photos) atau **local storage** (upload admin).

---

## 5. Flow Component Render — DB → HTML

```
                     DATABASE
                    ┌──────────────────────────────┐
                    │ frontend_components.content   │
                    │ frontend_sites.layout         │
                    │ frontend_pages.layout         │
                    │ frontend_pages.entry_script   │
                    │ frontend_pages.page_content   │
                    └──────────┬───────────────────┘
                               │ BladeSyncService::syncAll()
                               ▼
                   STORAGE (storage/app/)
    ┌──────────────────────────────────────────────────┐
    │ PHP/{code}.blade.php               ← components  │
    │ LAYOUTS/SITE/{slug}.blade.php      ← site layout │
    │ LAYOUTS/PAGE/{site}/{page}.blade.php ← page lay  │
    │ ENTRY/PAGE/{site}/{page}.blade.php ← entry scr   │
    │ PAGE_CONTENT/{site}/{page}.blade.php ← page cont  │
    └──────────────────────────────────────────────────┘
                               │ PortalHandler::handle()
                               ▼
                    RENDER PIPELINE (PortalHandler)
    ┌──────────────────────────────────────────────────┐
    │ 1. Load site meta (cache → DB)                   │
    │ 2. Load page (default atau by slug)              │
    │ 3. Query RoleMapping → build $menuTree           │
    │ 4. Fallback: pages as flat menu                  │
    │ 5. Render ENTRY script → $__entry__              │
    │ 6. Loop components:                              │
    │      Storage::get("PHP/{code}.blade.php")        │
    │      Blade::render() → ${$comp->code}            │
    │ 7. Render PAGE_CONTENT → $pageHtml               │
    │    (guna ${code} variables dalam Blade)          │
    │ 8. Render HEADER component → $__header__         │
    │ 9. Render FOOTER component → $__footer__         │
    │ 10. Render LAYOUT (site/page):                   │
    │       Blade::render($layout, [                   │
    │         '__header__', '__page__', '__footer__'   │
    │       ])                                         │
    └──────────────────────────────────────────────────┘
                               │
                               ▼
                    view('frontend.cms', compact('html'))
                               │
                               ▼
                        {!! $html !!}
```

### 5.1 BladeSyncService — DB ke Storage

Dipanggil setiap kali CRUD komponen/site/page:

```php
// Sync semua component content ke storage/app/PHP/{code}.blade.php
BladeSyncService::syncComponents();

// Sync site layout ke storage/app/LAYOUTS/SITE/{slug}.blade.php
BladeSyncService::syncSiteLayouts();

// Sync page custom layout ke storage/app/LAYOUTS/PAGE/{site}/{slug}.blade.php
BladeSyncService::syncPageLayouts();

// Sync entry script ke storage/app/ENTRY/PAGE/{site}/{slug}.blade.php
BladeSyncService::syncEntries();

// Sync page content ke storage/app/PAGE_CONTENT/{site}/{slug}.blade.php
BladeSyncService::syncPageContent();
```

### 5.2 PortalHandler — Render Engine

```php
// Satu-satunya method — render page dalam site
PortalHandler::handle($siteSlug, $pageSlug = null);

// ContentArticle / Gallery / Video detail guna query param dalam component page
// portalHandler->handle($siteSlug, 'articles') dengan ?id={code} dalam component
```

### 5.3 Component Code Jadi Variable

Component dengan `code = hero_banner` akan render dan tersedia sebagai `$hero_banner` dalam page content:

```blade
{{-- Page Content --}}
{!! $hero_banner !!}
{!! $features !!}
```

### 5.4 Entry Script Pattern

Guna entry script untuk preprocessing atau set shared data.

**Cara 1 — Auto-capture (v2):** `@php $var = value` terus auto-carried forward ke component/page content/layout. Tak perlu guna `$_shared` untuk variable ringkas:

```blade
{{-- Entry Script — jalan SEBELUM components di-render --}}
@php
$pageTitle = 'Welcome to Our Site';
$showBanner = true;
$items = App\Models\Backend\ContentArticle::take(3)->get();
@endphp

{{-- Page Content: variables auto-available --}}
<h1>{{ $pageTitle }}</h1>
@if($showBanner) <div class="banner">...</div> @endif
```

**Cara 2 — `$_shared` (pass-by-reference):** Disarankan untuk data yang perlu diubah suai oleh pelbagai komponen:

```blade
@php
$_shared->pageTitle = 'Welcome to Our Site';
$_shared->showBanner = true;
@endphp

{{-- Guna dalam mana-mana component --}}
@if($_shared->showBanner ?? false)
    <div class="banner">{{ $_shared->pageTitle }}</div>
@endif
```

### 5.5 Layout Pattern

**Site Layout** (default untuk semua page dalam site):

```blade
<!DOCTYPE html>
<html>
<head><title>{{ $siteName }}</title></head>
<body>
    {!! $__header__ !!}
    <main class="container">{!! $__page__ !!}</main>
    {!! $__footer__ !!}
</body>
</html>
```

**Page Custom Layout** (override site layout bila `use_custom_layout = true`):

```blade
<!DOCTYPE html>
<html>
<head><title>Custom Page</title></head>
<body>
    {!! $__header__ !!}
    <div class="full-width">{!! $__page__ !!}</div>
    {!! $__footer__ !!}
</body>
</html>
```

---

## 6. Auto-Render vs Manual Render

### 6.1 Auto-Render (Tanpa Page/Component)

Tiada modul yang auto-render sebagai dedicated detail page melalui URL path. Semua module content dipapar sebagai **component list** dalam page biasa.

**ContentArticle / Gallery detail** dimuat melalui page component guna query param `?id=`:

| Module | URL Pattern | Kaedah |
|--------|-------------|--------|
| **ContentArticle** | `/{siteSlug}/{pageSlug}?id={article_code}` | Page component query `request()->query('id')` |
| **ContentPhotoGallery** | `/{siteSlug}/{pageSlug}?id={gallery_code}` | Page component query `request()->query('id')` |
| Module lain (Slider, Video, etc.) | — | Component list sahaja |

**Contoh URL:**
```
http://127.0.0.1:8000/new-site/article?id=pelancaran-2026
```
- `article` = page slug
- `?id=pelancaran-2026` = filter ContentArticle by `article_code`

### 6.2 Manual Render (Guna Component)

Semua modul lain **hanya display sebagai component list** dalam page:

| Module | Component | Kaedah |
|--------|-----------|--------|
| ContentSlider | `mod_sliders` | Component type PHP query terus |
| ContentVideo | `mod_videos` | Component type PHP query terus |
| ContentDownload | `mod_downloads` | Component type PHP query terus |
| ContentApplication | `mod_applications` | Component type PHP query terus |
| ContentImage | `mod_images` | Component type PHP query terus |
| ContentPhotoList | `mod_photolist` | Component type PHP query terus |

> Tiada dedicated page/URL untuk slider, video, download, application, image, photo list.
> Ia hanya dipapar dalam page melalui component `mod_*`.

---

## 7. Routing — Macam Mana URL Di-resolve

### 7.1 Route Definitions (routes/web.php:610-618)

```php
Route::prefix('/')->middleware('visitor.log')->group(function () {
    Route::get('/', [FrontendSiteRenderController::class, 'default'])->name('home');
    Route::get('{site}/{slug}', [FrontendSiteRenderController::class, 'resolve'])
        ->where('slug', '.*')        // slug boleh guna slash
        ->name('frontend-site.page');
    Route::get('{slug}', [FrontendSiteRenderController::class, 'resolve'])
        ->name('frontend-site.home');
});
```

### 7.2 Resolution Pipeline

**Default site pages (single slug — no site prefix):**

```
URL: /about       (default site page — tanpa site slug)
       ↓
FrontendSiteRenderController::resolve('about')
       ↓
1. Default site check: FrontendSiteCacheService::getDefaultSite()
   → Cari site dengan is_default = true
       ↓
2. Adakah 'about' dalam pages default site?
   ├── YA → PortalHandler::handle('default-site', 'about')
   └── TIDAK → proceed ke step 3 (site check)
```

**Non-default site / multi-segment URL:**

```
URL: /module-site/video-tutorial
       ↓
FrontendSiteRenderController::resolve('module-site', 'video-tutorial')
       ↓
3. Cache check: FrontendSiteCacheService::get('module-site')
   → Baca storage/app/site-meta.json
   → Cari site dengan slug 'module-site'
       ↓
4. Site ditemui dalam cache?
   ├── YA → proceed ke step 5
   └── TIDAK → fallback ke HomeController::contentArticle($slug)
       ↓
5. Page check: Adakah 'video-tutorial' dalam site pages?
   ├── YA → PortalHandler::handle('module-site', 'video-tutorial')
   └── TIDAK → abort(404) (tiada auto-render untuk article/gallery)
```

> **Detail ContentArticle/Gallery** tidak di-render melalui URL path. Guna query param dalam page component: `/{siteSlug}/{pageSlug}?id={code}`

> **Ringkasan URL Pattern:**
> - Default site: `base-url/page-slug` (no site slug needed)
> - Other sites: `base-url/site-slug/page-slug`
> - Backward compatible: `base-url/default-site-slug/page-slug` still works

### 7.3 Cache Layer (FrontendSiteCacheService)

Cache disimpan sebagai JSON di `storage/app/site-meta.json`:

```json
{
    "my-site": {
        "id": 1,
        "name": "My Modern Site",
        "slug": "my-site",
        "is_default": true,
        "header_code": "navbar",
        "footer_code": "footer",
        "pages": {
            "home": { "id": 1, "name": "Home", "is_default": true, "components": ["hero","features"] },
            "about": { "id": 2, "name": "About", "is_default": false, "components": ["about_content"] }
        }
    }
}
```

Cache di-rebuild setiap kali `BladeSyncService::syncAll()` dipanggil (iaitu setiap kali CRUD site/page/component).

---

## 8. Seeder Pattern — Order yang Betul

### 8.1 Order dalam DatabaseSeeder

```php
$this->call([
    BackendUserSeeder::class,            // 1. Admin user
    FrontendUserSeeder::class,           // 2. Frontend user
    AdminRolePermissionSeeder::class,    // 3. Roles & permissions
    BackendMenuSeeder::class,            // 4. Backend menu
    ThemeRuleSeeder::class,              // 5. Theme rules
    RefDataSeeder::class,                // 6. Reference data (categories, statuses)
    SettingSeeder::class,                // 7. System settings
    DataDashboardSeeder::class,          // 8. Dashboard widgets
    WebSiteBackupSeeder::class,          // 9. Components + Sites + Pages + Menus
    FrontendAuthPagesSeeder::class,      // 10. Auth pages

    ContentApplicationSeeder::class,     // 11-23. Content data seeders
    ContentArticleAboutUsSeeder::class,
    ContentArticleFaqSeeder::class,
    ContentCalendarSeeder::class,
    ContentDirectorySeeder::class,
    ContentDownloadSeeder::class,
    ContentGallerySeeder::class,
    ContentImageSeeder::class,
    ContentNewsSeeder::class,
    ContentPublicHolidaySeeder::class,
    ContentSliderSeeder::class,
    ContentTranslationSeeder::class,
    ContentVideoSeeder::class,
]);

app(BladeSyncService::class)->syncAll();
```

### 8.2 Dependency Graph

```
BackendUserSeeder (1)
  └── independent

FrontendUserSeeder (2)
  └── independent

AdminRolePermissionSeeder (3)
  └── perlu BackendUserSeeder — assign role super-admin ke user

BackendMenuSeeder (4)
  └── perlu AdminRolePermissionSeeder — guna Role model

ThemeRuleSeeder (5)
  └── independent

RefDataSeeder (6)
  └── independent — create reference codes

SettingSeeder (7)
  └── independent

DataDashboardSeeder (8)
  └── independent

WebSiteBackupSeeder (9)
  └── perlu RefDataSeeder — guna status 'ACTIVE'
  └── create: components + sites + pages + menu items + downloads

FrontendAuthPagesSeeder (10)
  └── perlu WebSiteBackupSeeder — lookup site

Content*Seeder (11-23)
  └── perlu WebSiteBackupSeeder — lookup site/page
  └── perlu RefDataSeeder — guna status/category codes
```

### 8.3 Important Notes

1. **WebSiteBackupSeeder** guna `create()` (bukan `updateOrCreate`). Jika run 2 kali tanpa truncate → duplicate entry error.

2. **BladeSyncService::syncAll()** dipanggil di akhir `DatabaseSeeder` — penting untuk ensure component content di-sync ke storage.

3. **Portal Display** column untuk ContentArticle dan ContentPhotoGallery sekarang guna `frontend_sites` (bukan `Ref(PORTAL)`).

4. **Component Categories** dalam `WebSiteBackupSeeder`:
   - `navbar_web` → category `Header` (bukan `Navbar`)
   - `footer_web` → category `Footer` (bukan `Footer Web`)
   - **Sebab:** Category mesti match dengan pilihan dalam form: Header, Footer, Section, Content, Panel, Modal

### 8.4 Seed Command

```bash
# Seed semua
php artisan db:seed

# Refresh total
php artisan migrate:fresh --seed
```

---

## 9. Common Issues & Troubleshooting

### 9.1 Component Tak Muncul di Page

```
Sebab 1: Component belum di-attach ke page
Fix: Edit page → tick component dalam list → Save

Sebab 2: BladeSync belum jalan
Fix: Save semula mana-mana site/component/page → trigger syncAll()

Sebab 3: Component code takde dalam page_content
Fix: Pastikan page_content ada {!! $component_code !!}
```

### 9.2 "Undefined variable $xxx" Error

```
Sebab: Component code tak wujud atau belum di-render
Fix: Pastikan component dengan code 'xxx' telah di-attach ke page
     dan component status = active

Variable available:
  $siteSlug, $siteName, $sitePages
  $menus, $menuTree
  $_shared, $__entry__, $__header__, $__page__, $__footer__
  ${component_code} — untuk setiap component yang di-attach
```

### 9.3 Blank Page / Layout Tak Muncul

```
Sebab: Site layout kosong atau tiada
Fix: 1. Edit site → pastikan field layout tidak kosong
     2. Layout mesti ada {!! $__header__ !!}, {!! $__page__ !!}, {!! $__footer__ !!}
```

### 9.4 Menu Tak Muncul di Navbar

```
Sebab 1: Tiada RoleMapping untuk public/navbar
Fix: Admin → Frontend Menu → Assign Menu
     → Role: public → Category: navbar → Add menu items

Sebab 2: Fallback ke pages (jika langsung tiada RoleMapping)
Check: Ada tak frontend_pages untuk site ni?
       PortalHandler akan auto fallback ke flat page list

Sebab 3: Site ID tak matching
Fix: Pastikan site_id dalam RoleMapping = site_id semasa
```

### 9.5 Article / Gallery 404 (Auto-Render)

```
Sebab 1: article_portal_category / gallery_portal tak sama dengan site slug
Fix: Edit ContentArticle → Portal Display pilih site yang betul

Sebab 2: Status bukan ACTIVE
Fix: Pastikan article_status / gallery_status = 'ACTIVE'

Sebab 3: Site slug tiada dalam cache
Fix: Save mana-mana site → trigger BladeSyncService → rebuild cache
```

### 9.6 Vite / Asset Tak Loading

```
Sebab: npm run build belum jalan
Fix: npm run build (root) + cd public/tailadmin && npm run build

Check: public/build/manifest.json wujud?
       public/tailadmin/dist/ ada?
```

### 9.7 Component Content Tak Update

```
Sebab: BladeSync tak trigger lepas edit component content
Fix: BladeSyncService::syncAll() dipanggil dalam controller setiap CRUD.
     Jika manual edit DB, kena trigger sync manual:
     
     php artisan tinker
     app(BladeSyncService::class)->syncAll();
```

### 9.8 Duplicate Slug / Code

```
Sebab: frontend_pages slug unique per site
       frontend_components code unique global
       frontend_sites slug unique global

Fix: Guna validation dalam controller:
     'slug' => 'required|unique:frontend_pages,slug,NULL,id,site_fk,'.$site_fk
```

### 9.9 Seeder Error — Duplicate Entry

```
Sebab: ContentModuleSeeder guna create()
       Jika run 2 kali → duplicate key error

Fix: php artisan migrate:fresh --seed  (reset total)
     atau truncate manual table content dulu
```

### 9.10 Cache Site Tak Update

```
Sebab: FrontendSiteCacheService cache (site-meta.json) outdated
Fix: Save mana-mana site → trigger syncAll() → rebuild cache
     Atau delete storage/app/site-meta.json (auto-rebuild on next request)
```

---

## 10. Monaco Editor — Popup Modal & Component Integration

Semua editor (Component, Site, Page) guna **popup modal pattern** — bukan inline editor.

### 10.1 Editor Modal Flow

```
Click editor trigger box → Modal opens → Edit code → Save/Close
         ↓
   Editor trigger shows preview (first 800 chars)
```

**Features:**
- **Ctrl+S** — AJAX save (tiada page reload)
- **Escape** — close modal, sync ke textarea
- **Click outside modal** — close modal
- **Format button** — auto-format code (Shift+Alt+F)
- **Language selector** — PHP/HTML/CSS/JavaScript

### 10.2 AJAX Save Pattern (SEBiji macam Component)

**Controller pattern** — ajax() check LEPAS update (bukan sebelum validation):

```php
public function update(Request $request, $id)
{
    $page = FrontendPage::findOrFail($id);
    $data = $request->validate([...]);

    // AJAX modal save hanya update kod, tak sentuh name/slug
    $page->update($request->ajax() ? Arr::except($data, ['name', 'slug']) : $data);
    app(BladeSyncService::class)->syncAll();

    // AJAX check LEPAS update — bukan sebelum validation
    if ($request->ajax()) {
        return response()->json(['message' => 'Code updated successfully!']);
    }

    flash()->success('Page updated successfully!');
    return redirect()->route('frontend-site.pages.index', [...]);
}
```

**JavaScript pattern** — FormData + `X-Requested-With` header:

```javascript
var form = document.getElementById('page-form');
var formData = new FormData(form);

// Hanya hantar field yang sedang diedit
formData.delete('layout');
formData.delete('entry_script');
formData.delete('page_content');
if (currentEditorType === 'entry') {
    formData.set('entry_script', entryInput.value);
} else if (currentEditorType === 'pageContent') {
    formData.set('page_content', pageContentInput.value);
} else if (currentEditorType === 'layout') {
    formData.set('layout', layoutInput.value);
}
formData.set('_method', 'PUT');  // untuk update

fetch(form.action, {
    method: 'POST',
    body: formData,
    headers: {
        'X-Requested-With': 'XMLHttpRequest',
        'Accept': 'application/json',
    },
})
.then(function (res) { return res.json(); })
.then(function (data) { showToast(data.message || 'Code updated successfully!'); })
.catch(function () { showToast('Failed to save.', true); });
```

**Penting:**
- `ajax()` check mesti LEPAS update — kalau SEBELUM, validation error (422) akan block
- `X-Requested-With: XMLHttpRequest` header wajib — tanpa ni `$request->ajax()` return false
- `_method: PUT` untuk update route — Laravel method spoofing
- AJAX modal save **tidak update** `name` & `slug` — main Save baru update metadata

### 10.2a Create Mode — page_id Coordination

Create form ada hidden field `page_id`.

```
First modal Save → create page → return page_id → set hidden field
Main Save → update page yang sama (tiada duplicate slug error)
```

**Sebab:** Modal Save perlu create page supaya code boleh disimpan. Main Save kemudiannya update page yang sama, bukan create baru.

### 10.2b Pre-editing Validation (Name & Slug)

Sebelum modal boleh dibuka atau disave:

- Editor trigger boxes disable (greyed out) kalau Name/Slug kosong.
- Butang Save dalam modal disable sehingga Name/Slug diisi.
- Kalau user cuba klik/drop tanpa isi:
  - Name & Slug border merah + shake animation.
  - Toast: *"Please fill in Name and Slug before editing code."*
- Error hilang automatik bila user taip.

### 10.3 Component Panel dalam Modal

Buka modal editor → klik "Components" button dalam toolbar → panel drop down.

**Features:**
- **Search/filter** — taip nama component, terus filter
- **Recently Used** — auto-save guna localStorage (max 6)
- **Drag & drop** — drag chip terus ke editor
- **Drag ghost preview** — nampak nama component semasa drag (hijau)

**Workflow:**
```
Open Modal → Click "Components" → Drag chip → Drop in Editor → Done
                                    ↓
                           Recently Used auto-saved
```

### 10.4 Component Count Badge

Butang "Open Component Panel" ada badge nombor hijau — berapa component dah dipilih.
Update automatik bila tick/untick checkbox.

### 10.5 Editor Trigger → Modal Drop

Drag component dari side drawer → drop pada editor trigger box:
1. Trigger show blue highlight (`.drag-over` class)
2. On drop → auto-open modal → insert component

**CSS:**
```css
#editor-trigger.drag-over {
    border-color: #3b82f6 !important;
    background: #eff6ff !important;
    box-shadow: 0 0 0 3px rgba(59,130,246,0.2);
}
```

---

## Appendix A: Storage Structure

```
storage/app/
├── PHP/{code}.blade.php              ← Component templates
├── LAYOUTS/SITE/{slug}.blade.php     ← Site layouts
├── LAYOUTS/PAGE/{site}/{page}.blade.php ← Page layouts
├── ENTRY/PAGE/{site}/{page}.blade.php   ← Entry scripts
├── PAGE_CONTENT/{site}/{page}.blade.php ← Page content
└── site-meta.json                    ← Site cache
```

## Appendix B: Service Classes Reference

| Service | File | Fungsi |
|---------|------|--------|
| `BladeSyncService` | `app/Services/BladeSyncService.php` | Sync DB → storage files |
| `PortalHandler` | `app/Services/PortalHandler.php` | Render engine untuk CMS site |
| `FrontendSiteCacheService` | `app/Services/FrontendSiteCacheService.php` | JSON cache site metadata |
| `MenuFilterService` | `app/Services/MenuFilterService.php` | Filter/redirect untuk menu assign |

## Appendix C: Model Reference

| Model | Table | PK |
|-------|-------|----|
| `Backend\FrontendSite` | `frontend_sites` | `id` |
| `Backend\FrontendComponent` | `frontend_components` | `id` |
| `Backend\FrontendPage` | `frontend_pages` | `id` |
| `Backend\FrontendPageComponent` | `frontend_page_components` | `id` (pivot) |
| `Backend\menu\frontend\Menu` | `frontend_menu` | `menu_id` |
| `Backend\menu\frontend\RoleMapping` | `frontend_role_mapping` | `id` |
| `Backend\ContentArticle` | `content_article` | `article_id` |
| `Backend\ContentSlider` | `content_slider` | `slider_id` |
| `Backend\ContentApplication` | `content_applications` | `application_id` |
| `Backend\ContentDownload` | `content_downloads` | `download_id` |
| `Backend\ContentImage` | `content_images` | `image_id` |
| `Backend\ContentPhotoGallery` | `content_photo_gallery` | `gallery_id` |
| `Backend\ContentPhotoList` | `content_photo_list` | `photo_id` |
| `Backend\ContentVideo` | `content_video` | `video_id` |
