# Navbar SITE MAP baharu — struktur dilaksanakan (24 Okt 2026)

> Rujukan: site map yang diberikan pengguna. Navbar `site 2 (nres)` kini mengikut struktur ini.
> Skrip: `storage/app/cms-page-content/rebuild-navbar-sitemap.php` (idempotent) + `verify-navbar-sitemap.php`.

## 1. Struktur navbar (dilaksanakan)

```
UTAMA                                          /nres/home
MAKLUMAT KORPORAT                              (induk)
├ Amanat YBM · Latar Belakang · Visi, Misi dan Objektif · Pengurusan Tertinggi
├ Carta Organisasi · Ketua Pegawai Digital (CDO) · Piagam Pelanggan · Pencapaian dan Anugerah
SEKTOR                                         (induk)
├ Sektor Sumber Asli
│  ├ Bahagian Pengurusan Biodiversiti dan Perhutanan
│  ├ Bahagian Tanah, Ukur dan Geospatial
│  └ Bahagian Mineral dan Geosains
├ Sektor Kelestarian Alam
│  ├ Bahagian Pengurusan Alam Sekitar
│  └ Bahagian Perubahan Iklim
├ Sektor Pengurusan
│  ├ Bahagian Khidmat Pengurusan · Bahagian Pengurusan Sumber Manusia
│  ├ Bahagian Kewangan dan Perolehan · Bahagian Pengurusan Maklumat
│  └ Bahagian Pembangunan · Bahagian Akaun
└ Bahagian/Unit di Bawah KSU
   ├ Bahagian Perancangan Strategik dan Hubungan Antarabangsa
   ├ Unit Audit Dalam · Pejabat Penasihat Undang-Undang
   └ Unit Komunikasi Korporat · Unit Integriti
MEDIA                                          (induk)
├ Teks Ucapan                                  (induk — pecahan ikut penyampai)
│  ├ Ucapan Menteri (YBM) · Ucapan Timbalan Menteri (YBTM) · Ucapan Ketua Setiausaha (KSU)
├ Kenyataan Media / Siaran Media               /nres/media-statement
├ Penerbitan · Galeri (/nres/gallery) · FAQ
└ Arkib                                        /nres/electronic-archive
   ├ Teks Ucapan                               /nres/electronic-archive?jenis=ucapan
   ├ Kenyataan Media / Siaran Media            /nres/electronic-archive?jenis=kenyataan
   └ Keratan Akhbar                            /nres/electronic-archive?jenis=keratan
HUBUNGI KAMI                                   (induk)
├ Direktori                                    /nres/officer-directory
└ Lokasi
WARGA                                          (induk)
├ Portal WARGA                                https://warga.nres.gov.my/Pages/lamanutama.aspx
├ Portal Perkhidmatan Digital NRES            https://sso.nres.gov.my/login
└ Portal Perpustakaan                         https://library.nres.gov.my/
```

Item tanpa pautan (`#`) = halaman belum dibina → `menu_link` **NULL** (navbar render `href="#"`). **28 item** masih menunggu halaman.

**Kiraan:** 51 menu aktif (L1=6 · L2=17 · L3=22 + 6 tajuk kumpulan `<h6>`) · 6 menu dinyahaktifkan.

## 2. Menu lama yang dinyahaktifkan (`menu_status = 0`, bukan dipadam)

| # | Menu | Sebab |
|---|---|---|
| 20 | Maklumat Korporat | kini menjadi tajuk induk **MAKLUMAT KORPORAT** |
| 23 | Anggota Pentadbiran | tiada dalam site map baharu |
| 26 | Ketua Bahagian dan Unit | tiada dalam site map baharu |
| 44 | Keratan Akhbar Semasa | dipindah ke **Arkib › Keratan Akhbar** |
| 45 | Polisi, Dasar, Perundangan dan Garis Panduan | tiada dalam site map baharu |
| 47 | Berita | Arkib baharu tiada tab Berita |

Menu #19 `MENGENAI KAMI` dinamakan semula → **MAKLUMAT KORPORAT**; #46 `Arkib Elektronik` → **Arkib**; #28 `Direktori Pegawai` dan #22 `Lokasi` dipindah ke bawah **HUBUNGI KAMI**.

## 3. Skrip & pemulihan

```powershell
php storage/app/cms-page-content/rebuild-navbar-sitemap.php            # terapkan (backup automatik)
php storage/app/cms-page-content/rebuild-navbar-sitemap.php --dry-run  # papar pelan sahaja
php storage/app/cms-page-content/verify-navbar-sitemap.php             # semak navbar + UI assign-menu
```

- Skrip **idempotent**: menu baharu dipadankan ikut (nama + induk) sebelum dicipta → selamat dijalankan berulang.
- Backup automatik: `storage/app/backup/frontend_menu_backup_<stamp>.json` + `frontend_role_mapping_backup_<stamp>.json`.
- Mapping `frontend_role_mapping` (role `public`, kategori 1 = Navbar Public, site 2) dibina semula ikut susunan — jadi UI `/portal-k7m4x9/frontend-menu/assign-menu` terus menunjukkan struktur baharu ini.

## 4. Nota teknikal

- Navbar menyokong **3 aras sahaja** (L1 dropdown → L2 → L3; induk L3 dipaparkan sebagai tajuk `<h6>`). Sebab itu sub-halaman dalam sitemap (Dasar & Perundangan · Program · Halatuju & Sasaran · Jabatan & agensi · Objektif · Fungsi · Carta Organisasi) **tidak** diletakkan sebagai aras ke-4 — ia akan dipaparkan **di dalam halaman bahagian/unit** (Fasa 3 pelan sitemap).
- `menu_link` boleh mengandungi query string (`?jenis=…`). Status aktif navbar banding **path + query param**: setiap param dalam pautan menu mesti hadir dengan **nilai yang sama** dalam URL semasa (param tambahan dalam URL seperti `&tahun=2026` tidak menghalang). Kesannya:
  - paparan **"Semua"** (`/nres/speech`, `/nres/electronic-archive` tanpa param) → **tiada** anak dropdown aktif — halaman itu sendiri sudah menunjukkan tab aktif, jadi navbar tidak perlu bertindih ✓;
  - `?penyampai=MENTERI` / `?jenis=keratan` → **hanya** anak yang sepadan aktif (bukan semua);
  - halaman detail (`?id=…`) dan penapis tambahan (`?tahun=…`) → tiada anak tab aktif;
  - pautan unik yang ada query (`/nres/article?id=cdo`) → kekal aktif ✓.
  Skrip: `fix-navbar-active.php` (kemas kini komponen DB) · `verify-navbar-fix.php` (semak HTML sebenar) · `check-navbar-active-speech.php`.
- Item `menu_link` NULL (induk dropdown) tidak pernah "active" — kelakuan sedia ada.
- **Tajuk kumpulan kecil** (`<h6 class="dropdown-header">` — cth *SEKTOR SUMBER ASLI*, *TEKS UCAPAN*, *ARKIB*) **bold** untuk semua: `.navbar .dropdown-header{…font-weight:700}`. Sumber sebenar ialah CSS dalam **`frontend_sites.layout`** (site #2), disalin ke `LAYOUTS/SITE/nres.blade.php` oleh `BladeSyncService::syncSiteLayouts()`. Pulihkan/semak: `php storage/app/cms-page-content/fix-layout-bold.php` (idempotent, backup automatik). Disahkan melalui computed style: 6/6 tajuk = `font-weight: 700`.
- Sumber kod bahagian/unit: `content_directory_departments` (38 baris berhierarki) + ref `NRES_DEPARTMENT` (17 kod bahagian) + ref `SPEECH_SPEAKER` (`MENTERI`/`TIMBALAN_MENTERI`/`KETUA_SETIAUSAHA` = YBM/YBTM/KSU) untuk pecahan Teks Ucapan.

### ⚠️ Sumber sebenar navbar & layout — edit fail sahaja akan hilang

| Fail yang dibaca pada runtime | Sumber sebenar (edit di sini) | Cara sync |
|---|---|---|
| `storage/app/private/PHP/navbar_nres.blade.php` | `frontend_components` #20 (`code = navbar_nres`) | `BladeSyncService::syncComponents()` |
| `storage/app/private/LAYOUTS/SITE/nres.blade.php` | `frontend_sites.layout` (site #2 `nres`) | `BladeSyncService::syncSiteLayouts()` |
| `storage/app/private/PAGE_CONTENT/nres/*.blade.php` | `frontend_pages.page_content` | `BladeSyncService::syncPageContent()` |

Mengedit fail dalam `storage/app/private/**` sahaja **tidak kekal** — ia ditulis ganti pada sync seterusnya (cth simpan komponen/layout di CMS, atau `syncAll()`). Sentiasa kemas kini **sumber DB** dahulu, kemudian jalankan sync (skrip `fix-navbar-active.php` / `fix-layout-bold.php` sudah buat kedua-duanya + backup).

## 5. Halaman artikel (page 36, slug `article`) — siap

Satu halaman untuk **semua** item teks (MAKLUMAT KORPORAT, FAQ, dan lain-lain). Item pertama: **Ketua Pegawai Digital (CDO)**.

| Perkara | Nilai |
|---|---|
| Page | #36 · slug `article` · site 2 · `page_content` 23,597 bait · cermin `PAGE_CONTENT/nres/article.blade.php` |
| URL | `/nres/article` (hab) · `/nres/article?id=<kod>` (artikel) |
| Sumber | `content_article` + `content_article_translation` (HTML Summernote → `{!! !!}`) |
| Kategori | `ARTICLE` + subkategori `CORPORATE` → breadcrumb & kumpulan "MAKLUMAT KORPORAT" |
| Menu | #25 `Ketua Pegawai Digital (CDO)` → `/nres/article?id=cdo` |

**Kandungan (data sebenar dari portal):** halaman `cio.aspx` mengandungi **satu imej sahaja** (`PublishingImages/Pages/cio/CDO NRES.jpg.jpeg`, 2482×813) — teks di dalam imej itu ialah:
**Dato' Muhammad Ammir bin Haron** · Setiausaha Bahagian Kanan (Pengurusan) · Kementerian Sumber Asli dan Kelestarian Alam · `ammir@nres.gov.my`.

Yang dilakukan: imej portal dimuat turun → **disimpan sebagai ASET File Manager** dalam `public/dynaweb4/img/korporat/` (`cdo-nres.jpg` = potret 794×813, `cdo-nres-banner.jpg` = imej asal 2482×813) → pautan aset **ditanam dalam kandungan artikel** (`<img src="/dynaweb4/img/korporat/cdo-nres.jpg">`, BM + EN), **bukan** melalui `article_image` (dikosongkan/NULL) → kandungan teks artikel ditulis semula dengan nama/jawatan/e-mel sebenar + huraian peranan CDO (BM + EN) ✓. Skrip: `download-cdo-image.php` + `update-article-cdo.php` + `move-cdo-to-asset.php`.

### 5.1 Imej dalam artikel — gaya & pembetulan

**Cara edit (dua tempat berbeza):**

| Apa | Di mana |
|---|---|
| **Kandungan** artikel (termasuk `<img>` + inline `style`) | Admin → `/portal-k7m4x9/content-article/<id>/edit` (CDO = **id 6**) |
| **Fail imej** itu sendiri | File Manager → cth `/dynaweb4/nres/cdo/cdo-nres.jpg` (banner 2482×813) |

**Masalah lazim:** imej ditampal/di-seed dengan inline `style="max-width:320px;…"` → **banner nampak kecil** ✗ (inline style menang atas CSS halaman). Fail: `content_article_translation.article_translation_content` (per bahasa — `article_translation_language` = `ms`/`en`, `article_translation_main` = 1 untuk BM).

**Konvensyen gaya (ikut saiz sebenar fail):**

| Jenis imej | Gaya |
|---|---|
| **Banner** (nisbah lebar/tinggi ≥ 1.6) | `width:100%;max-width:100%;height:auto;border-radius:12px` → penuh lebar lajur (880px) |
| **Potret / segi** | `max-width:360px;width:100%;height:auto;border-radius:12px` → dikecilkan, di tengah ikut `<p style="text-align:center">` induk |

**Skrip pembetulan (idempotent, backup automatik):**

```powershell
php storage/app/cms-page-content/fix-article-images.php cdo            # satu artikel
php storage/app/cms-page-content/fix-article-images.php all            # semua artikel
php storage/app/cms-page-content/fix-article-images.php all --dry-run  # papar sahaja
```

Skrip hanya mengganti atribut `style` pada setiap `<img>` (teks/HTML lain tidak disentuh) dan memilih gaya ikut saiz fail sebenar (`getimagesize`) — jadi imej yang ditukar kemudian akan dibetulkan secara automatik. Backup: `storage/app/cms-page-content/backups/article-tr<id>-backup-*.json`.

**Status semasa CDO (2026-10-24):**

| Bahasa | Imej | Paparan disahkan |
|---|---|---|
| `ms` (BM) | `/dynaweb4/nres/cdo/cdo-nres.jpg` — banner 2482×813 (File Manager) | **830×272** px = penuh lebar kad ✓ (sama seperti page asal `cio.aspx`) |
| `en` | sama (banner 2482×813) | **830×272** px ✓ |

> Kedua-dua bahasa kini **hanya imej banner** (sama seperti halaman asal, yang teksnya berada **di dalam** imej). Selepas simpan melalui admin, baris `content_article_translation` **dicipta semula dengan id baharu** (kini `ms` = 24, `en` = 23) — jadi jangan bergantung pada id lama; sentiasa cari ikut `article_translation_parent_id` (CDO = 6).
>
> ⚠️ **Jangan** jalankan semula `update-article-cdo.php` — ia akan tulis ganti kandungan ini (versi BM/EN). Untuk betulkan saiz imej sahaja, guna `fix-article-images.php`.

**Ciri halaman:** breadcrumb **`Utama / Maklumat Korporat / <tajuk>`** — hanya "Utama" sebagai pautan, label kumpulan = **teks biasa** (tiada hyperlink) · tajuk + tarikh di **tengah** (tiada lencana/eyebrow kumpulan) · label kumpulan dalam **Title Case** ("Maklumat Korporat", bukan ALL CAPS; tiada `text-transform:uppercase`) · label kumpulan datang dari subkategori artikel (`CORPORATE`), dengan fallback: kategori `ARTICLE` tanpa subkategori → "Maklumat Korporat" · **semua kandungan di tengah** (`max-width:880px; margin:0 auto` — kad + senarai berkaitan + nota; hab `max-width:980px`) · imej dari **aset File Manager** ditanam dalam kandungan (bukan `article_image`) · badan HTML dengan gaya penuh (h2/h3/p/ul/ol/blockquote/table/img) · butang "Buka pautan berkaitan" **hanya jika** artikel ada `article_url` (**tiada** butang "Kembali ke senarai" · **tiada** lencana kategori/subkategori) · senarai pautan berkaitan (item subkategori sama) · hab kad untuk `/nres/article` · keadaan **tidak dijumpai** mesra (bukan 500) · BM/EN ikut sesi · dark mode + responsif.

> ⚠️ **Jangan jalankan semula `update-article-cdo.php` / `move-cdo-to-asset.php`** selepas kandungan disunting melalui admin (Kandungan → Artikel) — skrip itu **menulis ganti** kandungan. Gunakan `--delete`/seed hanya untuk reset data contoh.

**Skrip:** `template-article.txt` → `apply-article.php` (cipta/kemas kini halaman + cermin) · `seed-article-cdo.php` (+ `--delete`) · `verify-article.php` · `rebuild-site-cache.php` (**perlu** selepas cipta halaman dalam DB — cache senarai halaman site mesti dibina semula, jika tidak URL 404).

> **Penting — URL 3 segmen tidak disokong:** `/nres/article/<kod>` pulang **404** kerana `FrontendSiteRenderController::resolve()` hanya membaca **2 segmen** (`$parts[1]`) dan route `{site}/{slug}` tidak memadankan 3 segmen. Jadi semua pautan artikel guna bentuk **`?id=<kod>`** ✓. Untuk menyokong URL cantik, perlu ubah route + resolver (kerja berasingan).

**Item MAKLUMAT KORPORAT yang tinggal** (Amanat YBM, Latar Belakang, Visi/Misi/Objektif, Pengurusan Tertinggi, Carta Organisasi, Piagam Pelanggan, Pencapaian dan Anugerah) — hanya perlu: 1 rekod `content_article` (subkat `CORPORATE`) + `menu_link = /nres/article?id=<kod>` ✓ (halaman sama, tiada kod baharu).


## 6. Halaman Teks Ucapan (page 37, slug `speech`) — siap

| Perkara | Nilai |
|---|---|
| Page | #37 · slug `speech` · site 2 · `page_content` 33,989 bait · cermin `PAGE_CONTENT/nres/speech.blade.php` |
| URL | `/nres/speech` · `?penyampai=MENTERI\|TIMBALAN_MENTERI\|KETUA_SETIAUSAHA` · `?tahun=` · `?q=` · `?id=<speech_id>` |
| Sumber | `content_speeches` (terjemahan self-referencing: induk `speech_parent_id` NULL + anak ikut bahasa) |
| Pecahan penyampai | `speech_speaker_category` (ref `SPEECH_SPEAKER`) — tab **Semua / Ucapan Menteri (YBM) / Timbalan Menteri (YBTM) / Ketua Setiausaha (KSU)** |
| Kategori | `speech_category` (ref `SPEECH_CATEGORY`) — Ucapan / Perasmian / Majlis / Kenyataan |
| Menu | #42 `Teks Ucapan` → `/nres/speech` · #71 → `?penyampai=MENTERI` · #72 → `?penyampai=TIMBALAN_MENTERI` · #73 → `?penyampai=KETUA_SETIAUSAHA` |

**Ciri halaman:** breadcrumb (`Utama / Teks Ucapan` atau `… / <tajuk>`) · tab penyampai (dengan kiraan) · tab tahun + carian · jadual `# · Tarikh · Tajuk · Penyampai (+ jawatan) · Kategori · Dokumen` + butang **Muat turun** · **butiran** (`?id=`) dengan lencana penyampai/kategori, tarikh, penyampai, jawatan, majlis/program, lokasi, kotak dokumen · BM/EN ikut sesi · dark mode + responsif.

> **Nota navbar:** menu **#42 `Teks Ucapan`** dipaparkan sebagai **tajuk kumpulan `<h6>`** (bukan pautan) kerana ia mempunyai 3 anak — sama seperti **Arkib**. Jadi senarai penuh `/nres/speech` boleh dicapai melalui tab **Semua** di halaman itu sendiri (bukan dari navbar). Kalau mahu pautan "Semua Ucapan" dalam navbar, tambah satu anak baharu atau ubah komponen navbar.

**Data:** `seed-speech-sample.php` — isi `speech_speaker_category` + nama/jawatan penyampai untuk rekod sedia ada (#26–#33) dan tambah 3 ucapan contoh (satu bagi setiap penyampai). Kini **8 ucapan**: Menteri 3 · Timbalan Menteri 2 · Ketua Setiausaha 3 · tahun 2026 (5) / 2025 (2) / 2024 (1).

**Skrip:** `template-speech.txt` → `apply-speech.php` · `seed-speech-sample.php` (+ `--delete`) · `verify-speech.php` (16 semakan) · `rebuild-site-cache.php` (perlu selepas cipta halaman).



## 7. Fasa seterusnya (halaman bagi pautan `#`)

1. **MAKLUMAT KORPORAT** — 7 item lagi (Amanat YBM, Latar Belakang, Visi/Misi/Objektif, Pengurusan Tertinggi, Carta Organisasi, Piagam Pelanggan, Pencapaian dan Anugerah) + **FAQ** — setiap satu hanya perlu **1 rekod `content_article`** (subkat `CORPORATE`) + `menu_link = /nres/article?id=<kod>` (halaman #36 sedia ada, tiada kod baharu).
2. Halaman **profil bahagian/unit** (1 halaman + `?unit=<kod>`) untuk 4 blok SEKTOR (16 bahagian/unit) — mengandungi Objektif/Fungsi/Carta Organisasi (Pengurusan & KSU) atau Dasar & Perundangan/Program/Halatuju/Jabatan & Agensi (Sumber Asli & Kelestarian Alam).
3. **Penerbitan** — isi `content_downloads` (0 baris) + bina page 32 (`publication`, masih kosong) + `menu_link` #41.
4. **Polisi, Dasar, Perundangan dan Garis Panduan** (#45) — belum ada halaman (boleh guna `content_downloads` cat `POLICY`).
5. **Lokasi** ✅ **siap** (page 38, slug `location`, §8) — menu #22 sudah dipautkan.
6. **Teks Ucapan** ✅ **siap** (page 37, §6) — pecahan YBM/YBTM/KSU sudah berfungsi.

## 8. Halaman Lokasi (page 38, slug `location`) — siap

Konsep diambil daripada halaman asal `https://www.nres.gov.my/ms-my/Mengenai/Pages/LokasiNRECC.aspx`
(nama kementerian · alamat penuh · pautan Google Maps · peta embed · Tel / Fax / E-mel), dinaik taraf kepada susun atur 2 lajur + responsif.

| Perkara | Nilai |
|---|---|
| Page | #38 · slug `location` · site 2 · `page_content` **11,689 bait** · cermin `PAGE_CONTENT/nres/location.blade.php` |
| URL | `/nres/location` |
| Menu | **#22 `Lokasi`** → `/nres/location` (induk **#55 HUBUNGI KAMI**) — sebelum ini `menu_link = NULL` |
| Sumber data | **statik dalam `page_content`** (tiada jadual DB — konsep dari portal asal) |
| Kandungan | Nama: *Kementerian Sumber Asli dan Kelestarian Alam (NRES)* · Alamat: *Blok F11, Kompleks F · Presint 1, Pusat Pentadbiran Kerajaan Persekutuan · 62000 Wilayah Persekutuan Putrajaya* · Tel `+603 8000 8000` · Faks `+603 8889 2672` · E-mel `webmaster@nres.gov.my` |
| Peta | iframe Google Maps (`google.com/maps/embed?pb=…` — diambil bulat-bulat dari halaman asal) + butang **"Buka dalam Google Maps"** → `https://goo.gl/maps/pDtS53MSbWAJHgoF8` |

**Ciri halaman:** breadcrumb `Utama / Hubungi Kami / Lokasi` (hanya "Utama" pautan) · tajuk + lead di tengah · **grid 2 lajur ≥ 960px** (kad alamat kiri, peta kanan; 1 lajur di mudah alih) · kad alamat = nama kementerian + alamat + 3 baris kontak dengan **ikon inline SVG gaya `footer_nres`** (telefon → pautan `tel:`, faks, e-mel → `mailto:`) · peta iframe penuh lajur (`min-height:340px`, `loading="lazy"`, `referrerpolicy="no-referrer-when-downgrade"`, ada `title`) · blok **HALAMAN BERKAITAN** (Direktori Pegawai `/nres/officer-directory`, Aduan dan Maklum Balas `/nres/complaints-feedback`) · nota kontak · BM/EN ikut sesi (`session('locale')`) · dark mode + responsif (semua warna guna CSS var tema).

**Skrip:**

```powershell
php storage/app/cms-page-content/apply-location.php             # page #38 + menu #22 + sync + rebuild cache
php storage/app/cms-page-content/apply-location.php --dry-run   # papar sahaja
php storage/app/cms-page-content/verify-location.php            # sahkan navbar + elemen halaman
```

- Sumber template: `template-location.txt` (semua logik + CSS + markup dalam page_content — tiada komponen/partial).
- Backup: `storage/app/cms-page-content/backups/page38-location-backup-*.json` (kandungan page lama + baris menu #22).
- Disahkan: `/nres/location` **200** · pautan navbar ada · item aktif = **Lokasi** sahaja · breadcrumb ✓ · h1 ✓ · iframe peta ✓ · tel/faks/emel ✓ · pautan berkaitan ✓ · tiada ralat render · screenshot `lokasi-ms.jpg` / `lokasi-en.jpg` (peta + pin lokasi kelihatan).

> **Belum diubah:** butang **"Lihat di Peta"** dalam footer (`frontend_components` #21 `footer_nres`) masih `href="#"` — belum dipautkan ke halaman ini atau Google Maps.

## 9. Inventori: apa yang belum siap (audit 2026-10-24)

Skrip: `audit-navbar-pages.php` (menu vs halaman) · `audit-orphan-pages.php` (halaman tanpa pautan) · `audit-quicklinks.php` (pautan dalam komponen) · `audit-links-deep.php` (pautan mati footer/hero).

### 9.1 Menu navbar yang belum ada halaman — 29 item

| Blok | Bil. | Apa diperlukan |
|---|---|---|
| **MAKLUMAT KORPORAT** — Amanat YBM · Latar Belakang · Visi, Misi dan Objektif · Pengurusan Tertinggi · Carta Organisasi · Piagam Pelanggan · Pencapaian dan Anugerah | 7 | 1 rekod `content_article` (subkat `CORPORATE`) + `menu_link = /nres/article?id=<kod>` — **halaman #36 sedia ada, tiada kod baharu** |
| **SEKTOR** — 4 hub (Sumber Asli · Kelestarian Alam · Pengurusan · Bahagian/Unit di Bawah KSU) + **16 bahagian/unit** | 20 | 1 halaman baharu (cth `/nres/unit?unit=<kod>`) + `menu_link` pada 20 item |
| **MEDIA › Penerbitan** | 1 | ✅ **SIAP** (§10) — 83 penerbitan dalam `content_downloads` + page #32 + `menu_link` #41 |
| **MEDIA › FAQ** | 1 | ✅ **SIAP** (§10) — halaman baharu `/nres/faq` + `menu_link` #74 + bar-atas "Soalan Lazim" |

### 9.2 Bar-atas utiliti (komponen navbar #20) — 4 pautan `#`

| Item | Keadaan |
|---|---|
| Soalan Lazim | ✅ dipautkan ke **`/nres/faq`** (§10) |
| Hubungi Kami | ✅ dipautkan ke **`/nres/location`** |
| Aduan dan Maklum Balas | ✅ dipautkan ke **`/nres/complaints-feedback`** (halaman #24) |
| Peta Laman | ✅ dipautkan ke **`/nres/sitemap`** — halaman sitemap dijana automatik daripada menu (§11.11) |

> ✅ **SELESAI 2026-10-25:** keempat-empat pautan bar-atas kini berfungsi (`href="#"` = 0) dan **labelnya bilingual** (BM/EN) melalui `t()` + `app_translations` — lihat §11.11.

### 9.3 Halaman ada kandungan tetapi tiada pautan navbar

| Slug | Kandungan | Boleh dicapai dari |
|---|---|---|
| `calender` · `complaints-feedback` · `parliament-qa` · `tenders-and-quotations` | 34–48 KB | komponen **Capaian Pantas** (home) ✓ — **tidak** dalam navbar |
| `integrity-complaint` · `customer-satisfaction-2026` · `digital-satisfaction-2026` | ~34 KB | **tiada pautan langsung** ✗ |
| `publication` · `e-services` · `landing` | **KOSONG** ✗ | (publication & e-services ada di Capaian Pantas, tetapi kosong) |

### 9.4 Pautan mati (`#`) dalam komponen (bukan `page_content`)

| Komponen | Pautan mati | Cadangan |
|---|---|---|
| `hero_quick_nres` (#26) | **6/6**: Memohon lesen & permit · Aduan alam sekitar · Cari pegawai · Hutan & biodiversiti · Muat turun dasar & akta · Tender & sebut harga | 3 boleh dipaut terus: Aduan → `/nres/complaints-feedback` · Cari pegawai → `/nres/officer-directory` · Tender → `/nres/tenders-and-quotations`; 3 lagi tunggu halaman (e-Services · SEKTOR unit · Penerbitan/Polisi) |
| `quick_links_nres` (#23) | ✅ **dibetulkan 2026-10-25** — kini DB-driven (`QUICK_LINKS`); 8 pautan dari DB, bukan lagi hardcode. Lihat §11.6 |
| `news_nres` (#24) | ✅ **Berita Terkini** kini DB-driven (`content-article` / `NEWS`); "Lihat Semua" → `/nres/article`. Blok **Suara Anda / e-Penyertaan** masih statik (tiada modul konsultasi awam). Lihat §11.6 |
| `footer_nres` (#21) | **22/22** `href="#"` — ikon sosial · "Lihat di Peta" · Dasar Keselamatan / Privasi / Penafian | pautkan "Lihat di Peta" → `/nres/location`; dasar → halaman/PDF |

> ⚠️ Semua komponen di atas disunting melalui **komponen DB** (`frontend_components`) — edit fail `storage/app/private/PHP/*.blade.php` akan hilang (lihat §4).

### 9.5 Nota lain

- ✅ Imej **slider** dan **logo jabatan/agensi** kini disimpan **LOKAL** (`/storage/content-slider/…`, `/storage/content-application/…`) — tiada lagi hotlink ke `www.nres.gov.my` (dibetulkan 2026-10-25; lihat §11 & §11.5).
- Halaman `landing` (#21) kosong dan tidak dipautkan — mungkin memang tidak diperlukan.

### 9.6 Urutan cadangan

1. **Cepat (tanpa kod baharu):** ✅ bar-atas *Hubungi Kami* → `/nres/location`, *Aduan* → `/nres/complaints-feedback`, *Soalan Lazim* → `/nres/faq` (semua selesai) · ✗ 3 butang hero (`hero_quick_nres`) masih `#` · ✗ item berita masih `artikel.html`.
2. **7 × MAKLUMAT KORPORAT** (1 artikel + `menu_link` setiap satu) — belum.
3. ✅ **FAQ** — siap (§10).
4. ✅ **Penerbitan** — siap (§10).
5. **SEKTOR** — halaman `unit` + 20 `menu_link` (kerja terbesar) — belum.
6. **e-Services** (#33 kosong) · ✅ **Peta Laman** (siap §11.11) · **Polisi/Dasar** (#45, menu `[OFF]`) — belum.

## 10. Halaman Penerbitan & FAQ (MEDIA) — siap

### 10.1 Penerbitan — page #32 (`publication`)

Konsep: halaman asal `PustakaMedia/Pages/PenerbitanKementerian.aspx` (tab kategori sektor + jadual Tahun|Tajuk|Dokumen).

| Perkara | Nilai |
|---|---|
| Page | #32 · slug `publication` · site 2 · `page_content` **19,582 bait** · cermin `PAGE_CONTENT/nres/publication.blade.php` |
| URL | `/nres/publication` · penapis `?kat=<kod>` · `?tahun=<YYYY>` · `?q=<carian>` |
| Menu | **#41 `Penerbitan`** → `/nres/publication` (induk #39 MEDIA) — sebelum ini `NULL` |
| Data | **`content_downloads` — 83 baris** (status `ACTIVE`, `download_source = 'Penerbitan Kementerian'`, `download_language = ms`) |
| Kategori | `download_category` = kod sektor: **BP** Biodiversiti & Perhutanan (15) · **TUG** Tanah, Ukur & Geospatial (15) · **MG** Mineral & Geosains (13) · **PI** Perubahan Iklim (9) · **AS** Alam Sekitar (16) · **URUS** Pengurusan (15) |
| Fail | `download_file` = **laluan relatif** (cth `/ms-my/pustakamedia/Penerbitan/…pdf`) — lajur `varchar(255)`; page_content membina URL penuh + `rawurlencode` setiap segmen (portal asal `https://www.nres.gov.my`) |
| Tahun | daripada `download_date` (`YYYY-01-01`) → paparan tahun sahaja |

**Ciri halaman:** breadcrumb `Utama / Media / Penerbitan` · tab kategori (Semua + 6 sektor, dengan kiraan) · penapis **tahun** (chip + kiraan) · **carian** (GET `?q=`) · jadual `# | Tahun | Tajuk | Kategori | Dokumen` (Kategori disembunyikan < 820px) · satu tindakan **Muat turun** (PDF, tab baharu) · nota · keadaan kosong · BM/EN · dark mode + responsif.

**Skrip:** `extract-penerbitan.mjs` (headless Chrome → `penerbitan-source.json`) · `seed-publication.php` (+ `--delete` / `--fresh`) · `template-publication.txt` → `apply-publication.php` (page + menu + sync + cache; backup `backups/page32-publication-backup-*.json`) · `list-penerbitan.php` (senarai semak).

> Penerbitan asal di-**hotlink** dari portal asal (PDF kekal di `nres.gov.my`). Untuk pindah ke File Manager, muat naik fail dan tukar `download_file` kepada laluan `/dynaweb4/...`.

### 10.2 FAQ (Soalan Lazim) — page #39 (`faq`)

Halaman **baharu** (page asal portal `Pages/Soalan-lazim.aspx` masih kosong — "sedang dikemaskini"), jadi struktur + kandungan permulaan dibina sendiri. Setiap FAQ = 1 rekod `content_article` (**subkat `FAQ`**, ref `ARTICLE_SUBCAT` baharu) + terjemahan BM/EN — boleh disunting melalui **Kandungan → Artikel** dalam CMS.

| Perkara | Nilai |
|---|---|
| Page | #39 · slug `faq` · site 2 · `page_content` **14,397 bait** · cermin `PAGE_CONTENT/nres/faq.blade.php` |
| URL | `/nres/faq` |
| Menu | **#74 `FAQ`** → `/nres/faq` (induk #39 MEDIA) + **bar-atas "Soalan Lazim"** (komponen navbar #20) |
| Data | `content_article` (subkat `FAQ`, status `ACTIVE`) + `content_article_translation` — **8 FAQ** (BM `main=1` + EN) |
| Kandungan | 8 soalan: bidang kuasa NRES · cara menghubungi · lokasi pejabat · aduan/maklum balas · maklumat pegawai · muat turun penerbitan · tender & sebut harga · teks ucapan & kenyataan media (jawapan = pautan ke halaman berkaitan) |

**Ciri halaman:** breadcrumb `Utama / Media / Soalan Lazim` · carian **segera** (JS, tapis `data-q`, buka akordion automatik, papar "N soalan" / "X / N") · akordion `<details>` natif (aksesibel, tanpa JS) · blok CTA **"Masih ada soalan?"** (Aduan · Lokasi · Direktori Pegawai) · nota waktu pejabat · keadaan kosong (jika tiada artikel FAQ) · BM/EN · dark mode + responsif.

**Skrip:** `seed-faq.php` (+ `--delete` / `--fresh`) · `template-faq.txt` → `apply-faq.php` (cipta page + menu #74 + bar-atas navbar + sync + cache; backup `backups/faq-page-*.json`) · `compile-faq.php` (uji sintaks kompil Blade — berguna bila dapat "unexpected end of file").

> **Pengajaran:** `@endif` / `@endforeach` mudah tertinggal bila tampal blok besar — gunakan `compile-faq.php` (atau `php artisan view:clear` + muat semula) untuk mengesannya lebih awal. Gejala: kotak merah **"Page Content Error"** dengan mesej `syntax error, unexpected end of file`.

### 10.3 Integrasi FAQ dengan modul Artikel (CMS)

| Perkara | Nilai |
|---|---|
| Kategori (`ref.ARTICLE_CAT`) | **`FAQ`** ditambah — `descr = 'Soalan Lazim'`, `descr_en = 'FAQ'`, `sort = 6` → muncul dalam dropdown **Kategori** borang artikel (kini: News · Announcement · Article · Blog · Press Release · **FAQ**) |
| Subkategori (`ref.ARTICLE_SUBCAT`) | **`FAQ`** — **`parent = 'FAQ'`** (borang admin menapis subkategori ikut atribut `data-parent`; tanpa parent ia **tidak** muncul ✗) · `descr_en = 'FAQ'` |
| Artikel FAQ | 8 rekod: `article_category = 'FAQ'` **dan** `article_subcategory = 'FAQ'` (status `ACTIVE`, `article_portal_category = nres`) |
| Halaman #36 `/nres/article` | Query **mengecualikan** `article_category = 'FAQ'` di **2 tempat** (carian `?id=<kod>` + senarai hab) → hub korporat tidak bercampur dengan FAQ ✓; `/nres/article?id=faq-…` → "tidak dijumpai" ✓ |
| Halaman #39 `/nres/faq` | Query padan **kategori ATAU subkategori = FAQ** → tahan walaupun officer hanya set kategori FAQ (tanpa subkategori) |

**Cara tambah FAQ baharu (officer, tanpa kod):** CMS → **Kandungan → Artikel → Create** → Kategori = **FAQ** (Subkategori = **FAQ**) → isi **Soalan** sebagai tajuk + **Jawapan** sebagai kandungan (BM & EN) → Simpan. Ia terus muncul di `/nres/faq` ✓. Susunan ikut `article_sorting` (boleh drag & drop dalam senarai Artikel setelah tapis kategori FAQ).

**Skrip:** `fix-faq-category.php` (ref + kategori artikel + query halaman FAQ) · `fix-article-exclude-faq.php` (kecualikan FAQ dari halaman #36) · `check-admin-faq-options.mjs` (sahkan dropdown admin guna headless Chrome) · `check-article-hub.php` (hub bersih + FAQ berfungsi).

## 11. Slider halaman utama — data dari modul Content Slider (CMS)

Sebelum ini komponen slider **hardcode** 7 imej yang **hotlink** dari portal asal (`www.nres.gov.my/_catalogs/masterpage/Gp-pro/assets/img/slider/*.webp`) ✗. Kini ia membaca dari modul **Content Slider** dengan aset **lokal** ✓.

| Perkara | Nilai |
|---|---|
| Komponen | `frontend_components` **#22 `slider_nres`** (Section · digunakan pada page home #22) · `page_content` **5,776 bait** |
| Sumber data | `content_slider` (`slider_portal` = slug site, `slider_status = ACTIVE`, `slider_sort`, tetingkap `slider_start_*`/`slider_end_*`) + `content_slider_translation` (ms `main=1` + en) |
| Data | **7 slaid** (BM + EN): IGEM KLSS 2026 · Banci Ekonomi · Pelan Strategik NRES 2024–2030 · Portal MBJ · Garis Panduan Peralihan ke Kenderaan Elektrik · Kempen Kurangkan Plastik Sekali Guna · Media Sosial Rasmi NRES |
| Jenis pautan | `FILE` (2 PDF **lokal** ✓) · `EXTERNAL` (Portal MBJ ✓) · `NONE` (4 slaid tanpa pautan ✓) |
| Aset LOKAL | `storage/app/public/content-slider/` → diakses `/storage/content-slider/…` (junction `public/storage` ✓). **7 imej + 2 PDF** (PDF besar: 47.7 MB + 50.3 MB) |
| Admin | `/portal-k7m4x9/content-slider` — senarai 7 slaid, boleh tambah/ubah/susun/tukar imej |

**Ciri komponen:** portal diambil dari `$siteSlug` · tapisan status `ACTIVE` + tetingkap tarikh (toleran) · fallback bahasa (`ms` → `main=1` → pertama) · laluan relatif → `asset('storage/…')`, URL penuh dibiarkan · **markup/kelas `.cepa*` dikekalkan** supaya JS carousel dalam layout terus berfungsi (JS dinamik ikut bilangan `.cepa-slide` ✓) · seksyen disembunyikan jika tiada slaid · tag `SOROTAN` / `HIGHLIGHTS` (sebelum ini "KEMPEN CEPA").

**Skrip:** `seed-slider.php` (muat turun aset + seed; `--delete` / `--fresh` / `--no-download`) · `template-slider-component.txt` → `apply-slider-component.php` (kemas kini komponen DB + sync; backup `backups/component22-slider-backup-*.json`) · `check-slider-render.php` (render BM/EN) · `inspect-slider.php` / `inspect-slider-refs.php` · `audit-hotlinks.php`.

> ✅ **Hotlink dibetulkan (2026-10-25):** komponen `agency_nres` (#25) kini **DB-driven** dari modul **Content Application** (kategori `AGENCY_LINKS`) dengan **17 logo disimpan lokal** (`/storage/content-application/agency-*.png`). Lihat **§11.5**.

### 11.1 Saiz imej slider — SERAGAM 2560 × 741 px (auto-fit, tanpa potong)

**Masalah asal:** hint borang admin kata *"1920 × 1080 px recommended"* ✗ (nisbah 16:9 ✗) sedangkan slaid sebenar ber-nisbah **3.45:1** (kotak paparan `.cepa` = `aspect-ratio:3.455/1`) → imej yang di-upload tidak seragam ✗.

| Perkara | Nilai |
|---|---|
| Saiz kanonikal | **2560 × 741 px** (nisbah 3.4548:1) — **2× saiz papar sebenar maksimum**. `.cepa` duduk dalam `.container-xl` (Bootstrap, max 1320 px) → lebar papar maksimum = **1278 × 370 px**; 1278 × 2 (retina) = 2556 px → dibundarkan ke 2560 px. Tinggi 741 dipilih kerana `2560 ÷ 3.455` (CSS `aspect-ratio:3.455/1`) = **741.0** → padan tepat |
| Saiz terdahulu | ~~5938 × 1719 px~~ (nisbah sama, tetapi **4.65× oversample** — 8846×2562 PNG = 2.47 MB untuk kotak 1278 px; ditukar pada 2026-10-25) |
| Servis | **`app/Services/SliderImageService.php`** — `store(UploadedFile, ?prefix)` |
| Mod lalai | **`MODE = 'fit'`** — imej **PENUH, tiada dipotong**, diletakkan di tengah kanvas; kawasan kosong diisi dengan **salinan imej yang dikaburkan** (gaya sama seperti `.cepa-slide .blur` pada halaman) — **tiada kandungan hilang** ✓ |
| Mod alternatif | `MODE = 'crop'` → centre-crop memenuhi kanvas (kandungan tepi hilang ✗) — tukar satu pemalar sahaja |
| Output | **WebP kualiti 85** (`QUALITY`) · latar dikaburkan via `BLUR_FACTOR = 64` (downscale → upscale) + `BACKGROUND_DARKEN = 38` (≈ `brightness(.85)` CSS) |
| Bukan imej | PDF/DOCX/PPT → disimpan **asal** ✓ (tidak ditukar) |
| Dipakai oleh | `ContentSliderController::store()` + `update()` — **4 blok upload** (fail pautan `slider_url_upload` + imej terjemahan EN/MS/ZH — `Ref` kategori `LANGUAGE` ada 3 bahasa) |
| Hint borang | `create.blade.php` + `edit.blade.php` → `'Slider image · '.sizeLabel().' · JPG, PNG, WebP · Max 15MB · '.fitLabel()` — **satu sumber** ✓ (label & nota ikut pemalar servis) |
| Ujian | `test-slider-crop.php`: imej 1280×720 (16:9) berpenanda sudut → output **2560×741 WebP** · **semua penanda sudut & tengah kelihatan** (tiada potong ✓) · sisi diisi kabur ✓ (preview: `slider-fit-preview.jpg`) |

**Mengubah tingkah laku:** edit pemalar `WIDTH` / `HEIGHT` / `QUALITY` / `MODE` / `BLUR_FACTOR` / `BACKGROUND_DARKEN` dalam `SliderImageService` — label & nota borang admin ikut automatik ✓.

> **Nota:** 7 slaid sedia ada (di-seed dari portal asal) ber-px berbeza (1900×550 … 8846×2562) tetapi **nisbah ~3.45:1** — paparan kekal seragam kerana `.cepa` guna `aspect-ratio` + `object-fit:contain`. Upload **baharu** akan sentiasa 2560×741 ✓.
>
> **Cara saiz ditentukan (ukur dengan Chrome headless pada `/nres/home`, 2026-10-25):** `.cepa` = 1278 × 370 px pada viewport ≥1440px; 1098 × 318 px (1366/1280/1200); 935 × 271 px (992); 711 × 206 px (768); dan 16:9 pada ≤767.98px (mobile). Keperluan piksel peranti maksimum = **2556 px** (desktop 1920 @ DPR 2) → 2560 px menampung **semua** kes (desktop 1×/2×, tablet, mobile 3×) tanpa upscale. Julat upload yang munasabah: **1280 × 371 px** (minimum) … **3840 × 1112 px** (untuk banner berteks halus).
>
> **Disahkan pada aplikasi sebenar:** slaid yang di-upload melalui borang admin (2026-10-25) tersimpan sebagai **2560 × 741 px WebP · 63.1 KB** — berbanding 389 KB untuk saiz lama 5938×1719 (**6× lebih kecil**), kualiti paparan sama (skrin tetap 1278 × 370).

**Skrip:** `fix-slider-upload.php` (patch controller + hint borang; backup `backups/*-backup-*.txt`) · `test-slider-crop.php` (uji mod fit + preview) · `test-slider-resize.php` (uji saiz + fail bukan imej) · `inspect-slider-upload.php` (dimensi imej + keupayaan GD).

### 11.2 Cropper client-side (modal "Crop image") — nisbah dibetulkan

**Masalah:** modal *"Crop image"* dalam borang slider memaksa **1920 × 1080 px (16:9)** ✗ → banner slider (nisbah 3.45:1) **terpotong kiri/kanan** ✗.

**Punca:** `resources/js/filepond-image-cropper.js` → `getCropConfig()`:

```js
if (/^translations\[.+\]\[img\]$/.test(input.name)) {
    return { width: 1920, height: 1080 };   // ← lalai untuk SEMUA medan terjemahan
}
```

**Pembetulan:** sokongan nisbah **per-medan** melalui atribut `data-crop-ratio` ✓:

| Perkara | Nilai |
|---|---|
| Plugin | `resources/js/filepond-image-cropper.js` — `getCropConfig()` kini baca `input.dataset.cropRatio` (format `W:H`) dan pulangkan `{width, height}` **sebelum** jatuh ke lalai `translations[*][img]` = 1920×1080 |
| Borang slider | `contentSlider/create.blade.php` + `edit.blade.php` → input imej kini ada `data-crop-ratio="{{ SliderImageService::WIDTH }}:{{ SliderImageService::HEIGHT }}"` → **`2560:741`** (satu sumber dengan servis ✓) |
| Kesan | Modal memaparkan **"Final size: 2560 × 741 px"** ✓ dan kotak crop ber-nisbah 3.45:1 — **banner penuh** boleh dipilih (tidak lagi terpotong ✗) |
| Modul lain | Tidak terjejas ✓ (atribut hanya ditambah pada borang slider; lalai 1920×1080 kekal untuk medan terjemahan lain) |
| Mod tanpa cropper | Tambah `data-crop-width` + `data-crop-height` pada input → `getCropConfig()` pulangkan `null` = cropper **dilangkau** (imej penuh di-upload; `SliderImageService` mod `fit` mengendalikannya ✓) |
| Build | `npm run build` (Vite) → `public/build/assets/app-*.js` (mengandungi logik `cropRatio` ✓). **Nota:** semasa pembangunan admin memuat JS dari **Vite dev server** (`http://[::1]:5173/resources/js/app.js`) jadi perubahan sumber terus aktif; bundle production hanya perlu untuk deployment ✓ |

**Skrip:** `check-cropper-ratio.mjs` (login admin → semak HTML `data-crop-ratio` + senarai skrip yang dimuat) · `make-test-banner.php` (jana banner ujian 1725×500).

> **Sahkan dalam pelayar anda:** buka `/portal-k7m4x9/content-slider/create` → muat naik banner → modal patut menunjukkan **Final size: 2560 × 741 px** dengan kotak crop menutupi seluruh banner ✓.

### 11.3 Medan Type / Transition Dimension / Transition Type — status sebenar

**Kesimpulan (audit 2026-10-25):** ketiga-tiga medan ini **TIADA kesan pada laman awam** ✗ — ia hanya kawalan UI dalam borang admin.

| Medan | Fungsi sebenar | Laman awam |
|---|---|---|
| `slider_type` | `changeslidertype()` — tunjuk/sorok blok `#banner-items` | ❌ tiada kesan |
| `slider_dimension` | `getreflist()` — tapis pilihan transition (2d→fade/slide/zoom/static, 3d→cube/tumble/roll/twist/spin), reset nilai bila tukar | ❌ tiada kesan |
| `slider_transition` | disimpan ke DB sahaja | ❌ tiada kesan |

**Bukti:** 0 kemunculan `slider_type` / `slider_dimension` / `slider_transition` dalam **semua 6 komponen frontend**, **semua 19 halaman frontend**, dan layout `nres.blade.php`. Komponen `Slider NRES` (#22) hanya membaca `slider_portal`, `slider_status`, `slider_sort`, `slider_start_date`, `slider_end_date` + terjemahan + URL.

**Transisi di laman awam adalah HARDFCODE** (`nres.blade.php:138` CSS + `:327` JS): sentiasa slide mendatar `translateX` 0.7s, auto-advance 6000 ms (`:329`). Sebab itu slaid yang diset `fade` pun kelihatan **meluncur**, bukan memudar.

**Data semasa:** kesemua 8 slaid ditetapkan `slider_type = SLIDER_BACKGROUND`. Kesan: blok Transition Dimension/Type **tersembunyi** dalam borang create/edit; nilai lama (`dim`/`trans`) **kekal dalam DB** dan masih dihantar semasa simpan (input `display:none` bukan `disabled`).

**Ref:** `SLIDER_TYPE` = 2 nilai · `SLIDER_DIMENSION` = 2 nilai · **`SLIDER_TRANSITION` = 0 nilai** (senarai transition di-hardcode dalam `create.blade.php:311-328`, bukan dari Ref).

**Kalau mahu ia benar-benar berfungsi:** komponen #22 perlu output `data-transition`/`data-dimension` pada `.cepa`, dan layout perlu tambah CSS per-transisi + JS memilih strategi. Beri makna juga kepada `SLIDER_BACKGROUND` (cth. guna imejnya sebagai latar `.hero-media` menggantikan `hero-1..5.jpg` hardcode).

**Skrip:** `set-slider-background.php` (set semua slaid → `SLIDER_BACKGROUND`; `--dry-run`; backup `backups/content_slider-slider_type-backup-*.json`).

### 11.4 Tetingkap terbit slider — tarikh + MASA (dibetulkan 2026-10-25)

**Masalah:** `slider_start_time` / `slider_end_time` **langsung tidak berfungsi** ✗. Komponen #22 buat query mentah (`DB::table('content_slider')`) + closure `$sldInWindow()` yang **hanya banding tarikh** (komen sendiri mengaku: *"Tetingkap terbit (tarikh sahaja…)"*), sedangkan model `ContentSlider` **sudah ada** trait **`HasPublishWindow`** (`app/Models/Concerns/HasPublishWindow.php`) dengan `scopePublished()` yang mengendalikan tarikh **dan masa**.

**Bukti sebelum:** slaid #8 (`start_date=2026-10-25`, `start_time=01:08`) sudah muncul pada **01:02** — 6 minit awal.

**Pembetulan** (`fix-slider-publish-window.php`) — komponen #22:

| | Sebelum | Selepas |
|---|---|---|
| Sumber data | `DB::table('content_slider')` | `\App\Models\Backend\ContentSlider::query()` |
| Tetingkap | closure `$sldInWindow()` — **tarikh sahaja** | **`->published()`** — tarikh + **masa** |
| Penapis status | `where('slider_status','ACTIVE')` | sudah termasuk dalam `published()` |

**Bukti selepas — ujian langsung pada laman awam (tanpa tindakan manual):**
```
01:07:44  ->  7 slaid   (slaid #8 tersembunyi — start_time 01:08 belum tiba)
01:08:16  ->  8 slaid   (slaid #8 muncul AUTOMATIK)
```
Ujian sistematik `test-slider-date-window.php`: **9/9 lulus** (A–I: tarikh mula/tamat, masa mula/tamat, tiada tetingkap).

**Konsisten dgn:** `ContentArticle`, `ContentDownload`, `ContentSpeech`, `ContentVideo` — semuanya guna trait `HasPublishWindow` yang sama.

**Skrip:** `fix-slider-publish-window.php` (patch komponen #22; `--dry-run`; backup `backups/component22-slider-backup-*.json` + `backups/slider_nres.blade.php-backup-*.txt`) · `test-slider-date-window.php` (9 kes ujian tetingkap; memulihkan nilai asal di akhir).

> **Nota:** selepas mengubah komponen terus dalam DB, `BladeSyncService::syncComponents()` mesti dijalankan (skrip sudah buat) supaya `storage/app/private/PHP/slider_nres.blade.php` ikut — kerana `PortalHandler` merender dari fail itu. Editor komponen admin guna **Monaco Editor** (kod PHP/Blade selamat, bukan GrapesJS).

### 11.5 Komponen Agency (#25) — DB-driven dari modul Content Application + logo lokal

**Masalah asal:** komponen `agency_nres` (#25) ialah **HTML statik** dengan **34 kad** (17 agensi ×2 untuk marquee) yang **hotlink** logo dari `https://www.nres.gov.my/_catalogs/masterpage/Gp-pro/assets/img/jabatan/*.png` ✗ — tiada kawalan CMS, risiko jika portal asal ubah/buang fail.

**Keputusan modul:** guna **`content-application`** (label menu *"Links"*), **bukan** `content-image`:

| Modul | Sebab tidak dipilih / dipilih |
|---|---|
| `content-image` | Ref `IMAGE_CAT` ada `LOGO`, tetapi ia pustaka imej am — bukan senarai pautan |
| `content-directory-department` | Tiada medan logo/URL (hanya kod, nama BM/EN, parent) — untuk halaman Officer Directory |
| **`content-application`** ✅ | Ref `APPLICATION_CAT` + `application_url` + `application_img` + translations = padan 1:1 dengan kad agensi |

**Perubahan dibuat:**

| # | Perkara | Butiran |
|---|---|---|
| 1 | Migration | `2026_10_25_100000_add_application_code_to_content_applications_table.php` — tambah `application_code` (kod pendek, cth. `INSTUN`) |
| 2 | **Pepijat dibetulkan** | `ContentApplicationController::index()` sudah rujuk `application_code` dalam carian tetapi lajur tidak wujud → carian senarai Links gagal (`SQLSTATE[42S22] Unknown column`). Kini pulih ✓ |
| 3 | Model / Request | `application_code` ditambah ke `#[Fillable]` + rule `nullable\|string\|max:255` |
| 4 | Borang | Medan **Code** ditambah pada `create.blade.php` + `edit.blade.php` |
| 5 | Ref | `APPLICATION_CAT` → **`AGENCY_LINKS`** (Pautan Agensi / Agency Links) — ditambah ke DB + `RefDataSeeder` |
| 6 | Logo lokal | 17 PNG → `storage/app/public/content-application/agency-*.png` (250×250 / 500×500, **559 KB** jumlah) |
| 7 | Data | 17 baris `content_applications` (cat `AGENCY_LINKS`, `application_code`, `application_url`, `application_img`, `application_sort`) + 17 `content_application_translations` (ms, main=1) |
| 8 | Komponen #25 | Ditulis semula — query `content_applications` (cat `AGENCY_LINKS`, status `ACTIVE`) + translations, track digandakan ×2, heading/tagline ikut bahasa |

**Pemetaan medan → kad:**

| Kad | Medan |
|---|---|
| Nama pendek (`INSTUN`) | `application_code` |
| Tooltip (`INSTUN – Institut Tanah dan Ukur Negara`) | `application_code` + `application_translation_title` |
| Logo | `application_img` → `/storage/content-application/…` |
| Pautan | `application_url` |
| Susunan | `application_sort` |

**Bukti (diuji pada `/nres/home`):**

| Semakan | Hasil |
|---|---|
| Kad dirender | **34** (17 × 2) ✓ |
| Imej lokal `/storage/content-application/agency-` | **34** ✓ |
| Hotlink `img/jabatan` | **0** ✓ |
| Ralat render | tiada ✓ |
| BM | `JABATAN & AGENSI` · *"17 jabatan dan agensi…"* ✓ |
| EN | `DEPARTMENTS & AGENCIES` · *"17 departments and agencies…"* ✓ |
| Carian senarai Links | HTTP 200, tiada `Unknown column` ✓ |
| Borang edit (#2) | `application_code = INSTUN`, `application_cat = AGENCY_LINKS` ✓ |

**Skrip:** `seed-agency-links.php` (Ref + muat turun logo + seed data; `--dry-run` / `--force`) · `template-agency-component.txt` → `apply-agency-component.php` (pasang komponen + sync blade; `--dry-run`) · `patch-application-index.php` (badge kod dalam senarai).

**Backup:** `backups/component25-agency-backup-*.json` + `backups/agency_nres.blade.php-backup-*.txt`.

> **Nota:** nama penuh agensi hanya ada dalam **BM** (sumber asal hanya BM). Terjemahan EN boleh ditambah bila-bila melalui borang admin Content Application (modul menyokong multi-bahasa); komponen jatuh ke `main=1` untuk bahasa tanpa terjemahan.
>
> **Struktur data:** `application_img` (logo) dan `application_url` berada di peringkat **parent** — dikongsi oleh semua bahasa; hanya `application_translation_title` yang per-bahasa. Jadi satu logo + satu pautan per agensi, dengan nama penuh boleh diterjemah.

#### 11.5.1 Anak panah prev/next (2026-10-25)

Marquee asal guna **animasi CSS** (`@keyframes agency-scroll`) — arrows tidak boleh mengawalnya. Ditukar kepada **JS-driven** (`requestAnimationFrame`).

| Perkara | Sebelum | Selepas |
|---|---|---|
| Pergerakan | `animation:agency-scroll 55s linear infinite` | JS `requestAnimationFrame` (`speed = 0.45 px/frame`) |
| Pause | `.agency-viewport:hover .agency-track{animation-play-state:paused}` | JS flag `hover` (mouseenter/leave + focusin/out) |
| Wrap | `translateX(-50%)` pada track ×2 | JS: `offset = ((offset % half) + half) % half` (`half = scrollWidth / 2`) |
| Arrows | — | `.agency-nav.prev` / `.agency-nav.next` (bulat, 38px; 32px pada ≤575.98px) |
| Langkah | — | 1 kad setiap klik (lebar kad + `margin-right` dikira daripada computed style) |
| `prefers-reduced-motion` | animasi dimatikan | auto-scroll dimatikan, **arrows tetap berfungsi** |
| Tab tidak aktif | masih berjalan | `visibilitychange` → pause |

**Markup:** viewport dibalut dalam `<div class="agency-wrap" id="agencyCarousel">`; butang ialah **sibling** viewport (supaya tidak dipotong `overflow:hidden` atau dipudarkan `mask-image`).

**CSS** ada dalam layout site (`frontend_sites.layout` → `LAYOUTS/SITE/nres.blade.php`): `.agency-wrap`, `.agency-nav`, `.agency-nav.prev/.next`, `.agency-nav svg`.

**Bukti ujian fungsi (Chrome headless, `/nres/home`):**

| Ujian | Hasil |
|---|---|
| Struktur | wrap ✓ · track ✓ · prev ✓ · next ✓ · 34 kad · kad 168 px · separuh track 2856 px |
| Auto-scroll | −39.6 → −58.5 px — bergerak ✓ |
| Klik **next** | −58.5 → −230.6 px · beza **172 px** (≈1 kad) ✓ |
| Klik **prev** | −230.6 → −67.5 px · beza 163 px ✓ |
| Hover | −68.0 → −68.0 px — **paused** ✓ |
| Selepas hover keluar | −68.0 → −81.0 px — bergerak semula ✓ |

**Skrip:** `apply-agency-arrows.php` (patch layout CSS + komponen #25 + `syncAll()`; `--dry-run`).
**Backup:** `backups/site2-layout-backup-*.txt` + `backups/component25-agency-arrows-backup-*.json`.

### 11.6 Komponen Quick Links (#23) & News (#24) — DB-driven

#### #23 Quick Links ("Capaian Pantas") — `content-application` / `QUICK_LINKS`

| Perkara | Nilai |
|---|---|
| Kategori Ref baharu | **`QUICK_LINKS`** (Capaian Pantas / Quick Links, sort 9) |
| Medan baharu | **`application_icon`** (text) — markup SVG ikon inline (migration `2026_10_25_110000_*`) |
| Data | **8 pautan**: PUBLICATION · CALENDAR · MEDIA_STATEMENT · E_SERVICES · COMPLAINTS_FEEDBACK · PARLIAMENT_QA · TENDERS_QUOTATIONS · ONLINE_STATS |
| Terjemahan | ms + en (label UI) |
| Komponen | `.tile` / `.tring` / `.sec-title` dikekalkan; ikon dirender `{!! $icon !!}` dalam `.tring` |
| Heading | BM `CAPAIAN PANTAS` · EN `QUICK ACCESS` |

> ⚠️ Item **ONLINE_STATS** ("Statistik Perkhidmatan Atas Talian") masih `application_url = '#'` (placeholder) — tunggu halaman statistik.

#### #24 News ("Berita Terkini") — `content-article` / `NEWS`

| Perkara | Nilai |
|---|---|
| Sumber | `ContentArticle::query()->where('article_category','NEWS')->published()->orderByDesc('article_date')->limit(3)` |
| Tetingkap terbit | ✅ guna `scopePublished()` (tarikh **+ MASA**) — konsisten dengan modul lain |
| Imej | helper **`image_url()`** → fallback `public/dynaweb4/img/No_Image_Available.jpg` |
| Tarikh badge | hari + nama bulan (BM/EN) + tahun dari `article_date` |
| Ringkasan | `article_translation_content` (tag dibuang, dipotong ~150 aksara) |
| "Lihat Semua" | `artikel.html` (mati) → **`/nres/article`** |
| Data diimport | 3 item asal dijadikan artikel: **NRES-NEWS-IMPORT-01/02/03** (`2026-07-15/11/07`) + imej lokal `content-article/news-01..03.jpg` |
| Blok **Suara Anda / e-Penyertaan** | **kekal statik** (tiada modul konsultasi awam); imej base64 → `content-article/consultation-1.jpg` |

**Kesan saiz:** komponen #24 **44,635 → 7,912 bait** (base64 dibuang ke fail lokal).

**Bukti (render terus via `Blade::render`, lang=ms & en):**

| Komponen | ms | en |
|---|---|---|
| #23 | `CAPAIAN PANTAS` · 8 tile · 8 ikon | `QUICK ACCESS` · 8 tile · 8 ikon |
| #24 | `BERITA TERKINI` · 3 item · tarikh `25 September 2026 / 30 Julai 2026 / 15 Julai 2026` · Suara Anda ada | `LATEST NEWS` · 3 item · `25 September 2026 / 30 July 2026 / 15 July 2026` · Suara Anda ada |
| #25 | `JABATAN & AGENSI` · 34 kad · 0 hotlink | `DEPARTMENTS & AGENCIES` · 34 kad · 0 hotlink |

**Skrip:** `seed-quick-links.php` (Ref + seed 8 pautan + ikon; `--dry-run`/`--force`) · `template-quick-links-component.txt` + `apply-component.php <id> <templat>` (generik) · `seed-news-articles.php` (ekstrak 3 item + imej base64 dari komponen → artikel + fail lokal) · `apply-news-component.php` (tulis semula #24, kekalkan blok Suara Anda) · `render-components.php [ms|en]` (render #22–#25 terus untuk semakan).

**Backup:** `backups/component23-backup-*.json` · `backups/component24-news-backup-*.json` + `*.blade.php-backup-*.txt`.

> **Perhatian:** 5 artikel NEWS sedia ada (`NRES-NEWS-*`) **tiada imej** → kad berita akan papar `No_Image_Available.jpg` untuk item tersebut. Tambah imej melalui borang admin Content Article untuk membetulkan.

#### 11.6.1 Blok "SUARA ANDA" (#24) — daripada rekaan → pautan sebenar (2026-10-25)

**Masalah:** blok kanan komponen #24 berjudul **"SUARA ANDA – e-PENYERTAAN"** adalah **100% hardcoded** dan tidak berfungsi:

| Item | Sebelum |
|---|---|
| "Konsultasi Awam Aktif" + "Lihat Semua" | `href="#"` |
| Imej konsultasi | `http://127.0.0.1:8000/storage/content-article/consultation-1.jpg` (**URL keras localhost**) |
| Badge `DIBUKA` · tajuk "RUUPIN Consultation Paper…" · "Tarikh Tutup: 31 Ogos 2026" | hardcoded |
| "Sertai Konsultasi" | `href="#"` |
| 3 kad `e-Information` / `e-Consultation` / `e-Decision Making` | **semua `href="#"`** |
| Bahasa | BM sahaja |

**Fakta:** portal asal nres.gov.my **tiada** blok ini (rekaan mockup) · **tiada modul konsultasi awam** dalam sistem (`content_feedback` = Aduan & Maklum Balas, bukan konsultasi) · tetapi **4 halaman berkaitan sudah ada**.

**Penyelesaian (pilihan A — paling cepat):** ganti dengan **4 baris `.eng` sebenar** ke halaman sedia ada:

| Label | URL |
|---|---|
| Aduan dan Maklum Balas | `/nres/complaints-feedback` |
| Aduan Integriti | `/nres/integrity-complaint` |
| Kaji Selidik Kepuasan Pelanggan 2026 | `/nres/customer-satisfaction-2026` |
| Kaji Selidik Kepuasan Digital 2026 | `/nres/digital-satisfaction-2026` |

- Tajuk baharu: **`SUARA ANDA`** / **`YOUR VOICE`** + intro ringkas
- Guna gaya `.eng` + `.eicon` sedia ada (konsisten dengan kad `e-*` lama) · ikon SVG berbeza setiap baris
- **BM/EN** penuh ikut `$nwsLang`

**Pengesahan:** `href="#"` = **0** · `127.0.0.1` = **0** · 4 pautan `.eng` ✓ · BM & EN ✓ · ralat render = 0 ✓

**Skrip:** `apply-home-voice-block.php` (`--dry-run`) — cari sempadan blok dengan regex **tahan-EOL** (komponen disimpan dengan **CRLF**), ganti, kemas kini komen header.

**Backup:** `backups/component24-news_nres-voice-block-backup-*.json` (7,912 bait)

> **Belum dibuat (dilaporkan kepada pengguna):**
> 1. **Kad BERITA TERKINI tidak boleh diklik** — tiada pautan ke artikel penuh walaupun ada chevron
> 2. **2 daripada 3 artikel teratas tiada imej** — #1 *Kempen Penanaman 100 Juta Pokok* & #2 *Laporan Status Biodiversiti* → `No_Image_Available.jpg`
> 3. Artikel #1/#2/#3 ialah **data ujian** yang di-seed — perlu diganti dengan berita sebenar

### 11.7 Borang Content Application — pembersihan kategori & susun atur (2026-10-25)

**1. Kategori `APPLICATION_CAT` yang tidak digunakan dibuang.**

| Kekal | Dibuang |
|---|---|
| `MAIN_LINKS` (sort 1) · `AGENCY_LINKS` (sort 2) · `QUICK_LINKS` (sort 3) | `EXTERNAL_LINKS` · `REGISTRATION` · `REQUEST` · `FEEDBACK` · `COMPLAINT` · `ENQUIRY` |

Kriteria: **tiada baris** dalam `content_applications` menggunakan kod tersebut. `sort` disusun semula 1–3. Dibuang dari **DB + `RefDataSeeder`** (supaya tidak kembali pada re-seed).

> ⚠️ `MAIN_LINKS` dikekalkan kerana 1 baris menggunakannya — baris itu ialah data ujian (`application_code` kosong, tajuk **"Test"**, tiada URL/imej). Boleh dipadam melalui admin; selepas itu `MAIN_LINKS` boleh dibuang juga.

**2. Susun atur borang `content-application/{create,edit}`:**

| Sebelum | Selepas |
|---|---|
| Grid 1: `Code` · `Category` · `Status`<br>Grid 2 (`sm:grid-cols-3`): `URL` (span-2) · `Sort` | Grid 1 (`sm:grid-cols-2`): `Code` · `Category`<br>**Baris 2: `Status` · `Sort`**<br>Blok penuh: `URL` |

**3. Hint medan ikon → Bahasa Inggeris** (selaras dengan hint lain dalam borang):
> `Icon markup (inline SVG). Used by the Quick Links section. Leave empty if not needed.`

**Bukti (ukur kedudukan elemen dalam pelayar, `getBoundingClientRect().top`):**

| Medan | top | Baris |
|---|---|---|
| `Code` / `Category` | 546 | 1 |
| `Status` / `Sort` | **636** | **2 (sama)** ✓ |
| `URL` | 730 | 3 (blok penuh) |

Pilihan kategori dalam dropdown: **3 sahaja** (`MAIN_LINKS`, `AGENCY_LINKS`, `QUICK_LINKS`) ✓ · tiada ralat render ✓ · kedua-dua borang create & edit disahkan ✓

**Skrip:** `clean-application-cats.php` (`--dry-run` · `--keep=CODE,…`; backup `backups/ref-application_cat-backup-*.json`).

> **Nota (2026-10-25):** senarai `/content-application` memaparkan **nama aplikasi sahaja** — badge kod (`application_code`) yang pernah ditambah kemudiannya **dibuang** atas permintaan. Medan Code masih ada dalam borang create/edit dan digunakan oleh komponen (cth. kod pendek agensi `INSTUN`). Skrip: `patch-application-index-remove-code.php`.

### 11.8 Komponen Footer (#21) — "Pautan Berkaitan" DB-driven

**Masalah asal:** komponen `footer_nres` (#21) ialah **HTML statik** — bahagian **PAUTAN BERKAITAN** ada 12 pautan tetapi **semua `href="#"`** ✗ (tidak berfungsi). Footer juga mengandungi 5 ikon sosial `#` dan 3 pautan dasar `#`.

**Kategori Ref baharu:** **`RELATED_LINKS`** (Pautan Berkaitan / Related Links, sort 4).

**Sumber data:** 12 pautan diambil dari footer portal asal
`https://www.nres.gov.my/ms-my/Pages/default.aspx` — **label + URL sebenar**:

| # | Label | URL |
|---|---|---|
| 1 | Portal Rasmi Kerajaan Malaysia (MyGov) | `https://www.malaysia.gov.my` |
| 2 | Jabatan Digital Negara (JDN) | `https://www.jdn.gov.my/` |
| 3 | Suruhanjaya Pencegahan Rasuah Malaysia (SPRM) | `https://www.sprm.gov.my/` |
| 4 | Portal Rasmi Pejabat Ketua Setiausaha Negara (KSN) | `https://www.ksn.gov.my/` |
| 5 | Portal Rasmi Biro Pengaduan Awam (BPA) | `http://www.pcb.gov.my/` |
| 6 | Idea Hatch | `https://goo.gl/MDyWCo` |
| 7 | TalentCorp | `https://www.talentcorp.com.my/` |
| 8 | Places of Interest POI | `https://archive.data.gov.my/…/places-of-interest-poi` |
| 9 | Pusat Rujukan Persuratan Melayu (DBP) | `https://prpm.dbp.gov.my/` |
| 10 | Perakaunan Akruan | `https://www2.anm.gov.my/akruan/Pages/default.aspx` |
| 11 | Malaysia Digital Economy Blueprint | `…/Penerbitan/Malaysia-Digital-Economy-Blueprint.pdf` |
| 12 | Penghijauan Malaysia | `https://gamma.malaysia.gov.my/appdetails/756` |

> ✅ **Label #4 dibetulkan:** footer lama tulis *"Portal Keselamatan Siber Negara (KSN)"* ✗ — portal asal kata *"Portal Rasmi Pejabat Ketua Setiausaha Negara (KSN)"* ✓ (KSN = Ketua Setiausaha Negara, bukan Keselamatan Siber).

**Perubahan komponen #21:**

| | Sebelum | Selepas |
|---|---|---|
| PAUTAN BERKAITAN | 12 `<li>` statik, semua `href="#"` | `@forelse` dari `content_applications` (cat `RELATED_LINKS`) |
| Pautan | `#` (mati) | URL sebenar + `target="_blank" rel="noopener"` |
| Heading | `PAUTAN BERKAITAN` (statik) | BM `PAUTAN BERKAITAN` · EN `RELATED LINKS` |
| Lain-lain | — | HUBUNGI KAMI · ikon sosial · QR · **STATISTIK PELAWAT** (sudah dynamic) · f-bottom — **tidak diubah** |

**Bukti (render terus + laman awam):**

| Semakan | ms | en |
|---|---|---|
| Heading | `PAUTAN BERKAITAN` | `RELATED LINKS` |
| Bilangan pautan | **12** | **12** |
| `href="#"` dalam `.f-links` | **0** | **0** |
| Statistik pelawat | ada ✓ | ada ✓ |
| Ralat render | tiada ✓ | tiada ✓ |

**Baki pautan `#` dalam footer** (belum dijadikan dynamic — tiada modul berkenaan): 5 ikon sosial · 3 pautan dasar (`Dasar Keselamatan` · `Dasar Privasi` · `Penafian`). URL sebenar dari portal: `/ms-my/Pages/DasarKeselamatan.aspx` · `/DasarPrivasi.aspx` · `/Penafian.aspx`.

**Skrip:** `seed-related-links.php` (Ref + 12 pautan; `--dry-run`/`--force`) · `apply-footer-component.php` (patch komponen #21 + sync; `--dry-run`) · `render-components.php` (kini termasuk #21).

### 11.9 Footer (#21) — Media Sosial DB-driven + Tarikh Kemaskini dinamik

**Kategori Ref baharu:** **`SOCIAL_MEDIA`** (Media Sosial / Social Media, sort 5).

**Sumber data:** 5 media sosial dari footer portal asal `https://www.nres.gov.my/ms-my/Pages/default.aspx`:

| Kod | Label | URL |
|---|---|---|
| `FACEBOOK` | Facebook | `https://www.facebook.com/nresmalaysia` |
| `X` | X (Twitter) | `https://twitter.com/nresmalaysia` |
| `INSTAGRAM` | Instagram | `https://www.instagram.com/nresmalaysia/` |
| `TIKTOK` | TikTok | `https://www.tiktok.com/@nresmalaysia` |
| `YOUTUBE` | YouTube | `https://www.youtube.com/@NRESMalaysia` |

Ikon SVG **diekstrak dari komponen #21 sendiri** (blok `.f-soc`) → disimpan dalam medan **`application_icon`** (medan yang sama digunakan oleh Capaian Pantas #23).

**Tarikh Kemaskini:** `Tarikh Kemaskini: 15 Julai 2026` (keras) → **tarikh HARI INI**, format ikut bahasa:

| Bahasa | Output |
|---|---|
| ms | `Tarikh Kemaskini: 25 Oktober 2026` |
| en | `Last Updated: 25 October 2026` |

> Dikira dari `now()` + peta nama bulan BM/EN dalam komponen (bukan `translatedFormat()` — elak bergantung pada locale app).

**Perubahan komponen #21:**

| Bahagian | Sebelum | Selepas |
|---|---|---|
| `.f-soc` (5 ikon) | 5 `<a href="#">` + SVG keras | `@foreach ($frtSocial …)` dari `content_applications` (cat `SOCIAL_MEDIA`) + `target="_blank" rel="noopener"` |
| Tarikh Kemaskini | `15 Julai 2026` (keras) | `{{ $frtUpdated }}` — tarikh hari ini (BM/EN) |

**Bukti (render terus ms & en + laman awam):**

| Semakan | ms | en |
|---|---|---|
| Bilangan sosial | **5** | **5** |
| `href="#"` dalam `.f-soc` | **0** ✓ | **0** ✓ |
| Label | Facebook, X (Twitter), Instagram, TikTok, YouTube | sama |
| Host URL | facebook.com · twitter.com · instagram.com · tiktok.com · youtube.com | sama |
| Tarikh Kemaskini | **25 Oktober 2026** | **Last Updated: 25 October 2026** |
| Tarikh lama `15 Julai 2026` | tiada ✓ | tiada ✓ |
| Ralat render | tiada ✓ | tiada ✓ |

**Baki pautan `#` dalam footer:** hanya **3 pautan dasar** (`Dasar Keselamatan` · `Dasar Privasi` · `Penafian`) — sesuai jadi halaman CMS. URL portal asal: `/ms-my/Pages/DasarKeselamatan.aspx` · `/DasarPrivasi.aspx` · `/Penafian.aspx`.

**Skrip:** `seed-social-media.php` (Ref + ekstrak ikon dari komponen + 5 pautan; `--dry-run`/`--force`) · `apply-footer-social.php` (patch `.f-soc` + tarikh + sync; `--dry-run`).

### 11.10 Footer (#21) — SEMUA teks bilingual (BM/EN)

Semua teks statik footer dipindahkan ke array terjemahan **`$frtT`** dalam blok `@php` (dipilih ikut `$frtLang`), jadi satu sumber untuk kedua-dua bahasa.

| Elemen | BM | EN |
|---|---|---|
| Brand (b1) | `PORTAL RASMI` | `OFFICIAL PORTAL` |
| Brand (b3) | `KEMENTERIAN SUMBER ASLI DAN KELESTARIAN ALAM` | `MINISTRY OF NATURAL RESOURCES AND ENVIRONMENTAL SUSTAINABILITY` |
| H6 1 | `HUBUNGI KAMI` | `CONTACT US` |
| Alamat | `Kementerian Sumber Asli dan Kelestarian Alam / Blok F11, Kompleks F, Lebuh Perdana Timur, / Presint 1, 62000 Putrajaya, Malaysia.` | `Ministry of Natural Resources and Environmental Sustainability / Block F11, Complex F, Lebuh Perdana Timur, / Precinct 1, 62000 Putrajaya, Malaysia.` |
| H6 3 | `LOKASI KAMI` | `OUR LOCATION` |
| QR (alt) | `Kod QR lokasi NRES di Google Maps` | `NRES location QR code on Google Maps` |
| QR (kapsyen) | `Imbas kod QR untuk lokasi kami di Google Maps` | `Scan the QR code for our location on Google Maps` |
| Butang | `Lihat di Peta` | `View on Map` |
| H6 4 | `STATISTIK PELAWAT` | `VISITOR STATISTICS` |
| Statistik | `Hari Ini` · `Minggu Ini` · `Bulan Ini` · `Tahun Ini` · `Jumlah` | `Today` · `This Week` · `This Month` · `This Year` · `Total` |
| Copyright | `© Hak Cipta Kementerian Sumber Asli dan Kelestarian Alam` | `© Copyright Ministry of Natural Resources and Environmental Sustainability` |
| Dasar | `Dasar Keselamatan` · `Dasar Privasi` · `Penafian` | `Security Policy` · `Privacy Policy` · `Disclaimer` |
| Tarikh | `Tarikh Kemaskini` | `Last Updated` |

**Bukti — laman awam langsung (`/lang/ms` vs `/lang/en`):**

| | ms | en |
|---|---|---|
| H6 | `HUBUNGI KAMI` · `PAUTAN BERKAITAN` · `LOKASI KAMI` · `STATISTIK PELAWAT` | `CONTACT US` · `RELATED LINKS` · `OUR LOCATION` · `VISITOR STATISTICS` |
| Label statistik | `Hari Ini=74` · `Minggu Ini=1,590` · `Bulan Ini=1,590` · `Tahun Ini=1,590` · `Jumlah=1,590` | `Today=75` · `This Week=1,591` · `This Month=1,591` · `This Year=1,591` · `Total=1,591` |
| Baris bawah | `© Hak Cipta … • Dasar Keselamatan • Dasar Privasi • Penafian • Tarikh Kemaskini: 25 Oktober 2026` | `© Copyright … • Security Policy • Privacy Policy • Disclaimer • Last Updated: 25 October 2026` |
| Kapsyen QR | `Imbas kod QR untuk lokasi kami di Google Maps` | `Scan the QR code for our location on Google Maps` |
| Brand | `PORTAL RASMI` | `OFFICIAL PORTAL` |
| Ralat | tiada ✓ | tiada ✓ |

> Statistik pelawat kekal **hidup** (74→75 antara muat turun) — bukan nilai keras.

**Skrip:** `apply-footer-i18n.php` (19 gantian teks → `$frtT`; `--dry-run`) · `render-components.php` (kini semak H6, label statistik, baris bawah, kapsyen QR, brand).

## 11.11 Halaman Peta Laman (page #41, slug `sitemap`) + bar-atas utiliti bilingual (2026-10-25)

### Konsep

Halaman **`/nres/sitemap`** dijana **AUTOMATIK** daripada menu navbar publik — jadi ia sentiasa selari dengan struktur navigasi sebenar tanpa penyelenggaraan manual.

**Rujukan konsep:** [nres.gov.my/ms-my/Pages/peta-laman.aspx](https://www.nres.gov.my/ms-my/Pages/peta-laman.aspx) — senarai `<ul class="dfwp-list">` bersarang gaya SharePoint: label induk sebagai `<span>` (bukan pautan), pautan bertingkat di bawahnya, dan pautan terus di aras atas (cth. *Laman Utama*, *Piagam Pelanggan*, *Tender dan Sebutharga*). Kita kekalkan **konsep** (senarai bersarang) tetapi isi kandungan datang dari menu kita sendiri yang terkini.

| Sumber | Guna |
|---|---|
| `$menuTree` (PortalHandler) | pokok menu publik 3 aras — nama sudah diterjemah via `t()` |
| `$sitePages` (PortalHandler) | senarai halaman site → seksyen **"Halaman Lain"** |

### Struktur halaman

- Tajuk + bilangan pautan (`$smTotal`)
- **`<ul class="sm-tree">` bersarang** — 2 lajur melalui CSS `columns`
- L1 = label induk `.sm-top` (bold, huruf besar, penanda segi empat teal) — jadi **pautan** jika ada `menu_link`, jika tidak `<span>` (cth. MAKLUMAT KORPORAT)
- L2/L3 = senarai `<ul>` bersarang dengan **garis penyambung** (`border-left` + `::before`)
- Item **tanpa `menu_link`** → `<span>` kelabu (struktur sahaja, bukan pautan mati)
- **"Pautan Lain"** → `.sm-tree--flat` (3 lajur) untuk halaman yang TIADA dalam menu (10 halaman)
- Footer: nota "dijana automatik" + tarikh kemaskini dinamik

### Keputusan reka bentuk

| Perkara | Sebab |
|---|---|
| Guna `$menuTree`, bukan query sendiri | mengelak duplikasi logik PortalHandler + auto-terjemah |
| Halaman sendiri **dikecualikan** dari "Halaman Lain" | `request()->path()` dibandingkan — elak pautan diri sendiri |
| `t($smP['name'])` untuk nama halaman | BM/EN ikut `app_translations`; fallback = nama asal |
| CSS dalam `<style>` page sendiri (kelas `.sm-*`) | sama konvensyen seperti page #40 (`visitor-statistics`) |
| **Senarai bersarang, bukan kad** | konsep portal asal — ringan, padat, mudah dibaca |
| `columns: 2` + `break-inside: avoid` | lajur seimbang tanpa memotong subtree |
| Semua L1 dalam pokok (termasuk UTAMA) | sama seperti portal asal (*Laman Utama* di aras atas) |
| `var(--teal)`, `var(--line)`, `var(--fade)` | warisi tema + sokong dark mode |

### Bar-atas utiliti — bilingual (BM/EN)

Label dahulunya **keras BM** dalam komponen #20. Kini guna `t()`:

| Label | ms | en |
|---|---|---|
| Soalan Lazim | Soalan Lazim | FAQ |
| Hubungi Kami | Hubungi Kami | Contact Us |
| Aduan dan Maklum Balas | Aduan dan Maklum Balas | Complaints and Feedback |
| Peta Laman | Peta Laman | Sitemap |
| `aria-label` nav | Pautan utiliti | Utility links |

Nama halaman untuk "Pautan Lain" juga diterjemah: `Calender`→`Kalendar` · `e-Services`→`e-Perkhidmatan` · `Integrity Complaint`→`Aduan Integriti` · `Parliament QA`→`Soal Jawab Parlimen` · `Tenders and Quotations`→`Tender dan Sebut Harga` · `Visitor Statistics`→`Statistik Pelawat` · `Complaints & Feedback`→`Aduan dan Maklum Balas` · kedua-dua kaji selidik 2026.

### Pengesahan

| Ujian | BM | EN |
|---|---|---|
| Pautan bar-atas | `/nres/faq` · `/nres/location` · `/nres/complaints-feedback` · `/nres/sitemap` — `href="#"` = **0** | sama |
| Label bar-atas | Soalan Lazim · Hubungi Kami · Aduan dan Maklum Balas · Peta Laman | FAQ · Contact Us · Complaints and Feedback · Sitemap |
| Halaman sitemap | 6 label L1 · 11 `<ul>` bersarang · 29 pautan · ralat=0 | "Other Links" + nama EN |
| Lint Blade | `No syntax errors` ✓ | ✓ |

> ⚠️ **Belum:** nama **menu utama** (`MAKLUMAT KORPORAT`, `SEKTOR`, `MEDIA`, `HUBUNGI KAMI`, `WARGA` …) masih BM dalam mod EN — `app_translations` belum ada kunci untuk nama menu. Sama juga di navbar.
>
> ⚠️ **Perangkap semasa menyunting templat:** `insert_line` yang salah boleh **memotong blok `@php`** dan menyebabkan `syntax error, unexpected token "<"` semasa render. Sentiasa jalankan `lint-sitemap-template.php` (kompil + `php -l`) selepas menyunting templat besar.

**Skrip:** `apply-sitemap-page.php` (page #41 + pautan navbar; `--dry-run`) · `seed-utility-translations.php` (28 terjemahan + label `t()`; `--dry-run`) · `lint-sitemap-template.php` (kompil + `php -l` templat).

**Templat:** `storage/app/cms-page-content/template-sitemap-page.txt` (189 baris · 8.4 KB)

**Backup:** `backups/page41-sitemap-backup-*.json` · `backups/component20-navbar_nres-sitemap-link-backup-*.json` · `backups/component20-navbar_nres-util-i18n-backup-*.json`

## 11.12 Widget chatbot "Agen Lestari" — ikon PNG besar → WebP kecil (2026-10-25)

Widget bantuan di penjuru kanan-bawah (`<div class="assist" id="assist">`) berada dalam **layout** `LAYOUTS/SITE/nres.blade.php` — **bukan** komponen. Ikonnya dahulunya **base64 inline** di dalam layout.

| Perkara | Sebelum | Selepas |
|---|---|---|
| Sumber ikon | `data:image/png;base64,…` inline (≈38 KB) | `/dynaweb4/nres/ai-chatbot.webp` |
| Imej asal | — | `public/dynaweb4/nres/AI Chatbot-1.png` (1024 × 1536 · **1.88 MB**) |
| Fail output | — | `public/dynaweb4/nres/ai-chatbot.webp` (**128 × 192** · **7.2 KB**) |
| Saiz layout | 67.1 KB | **29.1 KB** (−37.9 KB) |
| Saiz imej | 1,921.5 KB | **7.2 KB** (**−99.6%**) |

**Kenapa 128 × 192?** CSS `.assist img` menetapkan `height: 96px` → 2× retina = **192 px** tinggi; nisbah asal 1024:1536 (0.6667) dikekalkan → 128 px lebar. Imej asal **8× lebih besar** daripada yang dipaparkan.

**Guna:**
```powershell
php storage/app/cms-page-content/optimize-chatbot-icon.php   # PNG → WebP 128×192 (GD)
php storage/app/cms-page-content/apply-chatbot-icon.php      # patch layout + syncAll()
php storage/app/cms-page-content/apply-chatbot-icon.php --dry-run
```

**Perangkap:** tag `<img>` dalam layout sudah ada `alt="Agen Lestari"` **selepas** `src`, jadi corak `src="…">` **gagal** padan. Guna `/<img src="data:image\/png;base64,[^"]+"[^>]*>/`.

**Pengesahan:** `image/webp` · HTTP 200 · 7,354 bait · 0.12s · `chatbot=1` pada `/nres/home`, `/nres/faq`, `/nres/sitemap` · ralat=0

> ⚠️ **Belum:** 3 base64 lain masih inline dalam layout — logo navbar (`.brand-emblem`), logo footer (`.f-brand`), kod QR footer (`.f-qr`) — ≈20 KB. Boleh dioptimumkan dengan cara yang sama.

**Backup:** `backups/site2-layout-chatbot-icon-backup-*.json` (67.5 KB)

## 11.13 Butang "Kembali ke atas" (back to top) — widget terapung (2026-10-25)

Butang ↑ ditambah ke dalam **bekas terapung yang sama** dengan widget chatbot, supaya ia sentiasa tersusun dan turun ke bawah bila chatbot ditutup.

### Struktur

```html
<div class="floaters">                              <!-- position:fixed; flex-column -->
  <button class="btt" id="toTop" …>↑</button>       <!-- butang — anak PERTAMA (di atas) -->
  <div class="assist" id="assist">…chatbot…</div>   <!-- position:relative (✕ berlabuh di sini) -->
</div>
```

| Perkara | Butiran |
|---|---|
| Kemunculan | Kelas `.on` ditambah bila `scrollY > 300` (opacity 0→1, 250ms) |
| Klik | `window.scrollTo({top:0})` — `behavior:'smooth'`, atau `'auto'` bila `prefers-reduced-motion` |
| Saiz | **44 × 44 px** (sasaran sentuh ≥44px); 20px ikon SVG chevron-atas |
| Warna | `background:var(--teal)` · `color:var(--bs-body-bg)` → kontras automatik dalam dark mode |
| Aksesibiliti | `aria-label` + `title` melalui `t()` — BM "Kembali ke atas" / EN "Back to top"; `:focus-visible` outline |
| Prestasi | `addEventListener('scroll', …, {passive:true})`; ada listener `resize` |

### Perangkap yang ditemui

| Isu | Sebab | Betulan |
|---|---|---|
| **`✕` tutup chatbot bertindih dengan butang** | `.assist` kehilangan `position:fixed` (dipindah ke `.floaters`) → `✕` (`position-absolute`) berlabuh pada `.floaters` | tambah `position:relative` pada `.assist` |
| Ujian headless nampak "opacity 0" | Chrome headless **tidak memajukan transisi CSS** | bukan bug — sahkan dengan `b.style.transition='none'` → opacity jadi 1 |
| Acara `scroll` tidak tercetus dalam iframe headless | had headless | hantar `new Event('scroll')` secara manual dalam ujian |
| Bootstrap tetapkan `:root{scroll-behavior:smooth}` | `@media (prefers-reduced-motion: no-preference)` | bukan bug — paksa `scrollBehavior='auto'` dalam ujian |

### Pengesahan (ujian iframe + Chrome headless)

| Ujian | Hasil |
|---|---|
| Butang wujud & saiz | `btn=ADA` · **44 × 44 px** · `aria-label=Kembali ke atas` ✓ |
| Sebelum skrol | `class="btt"` · `opacity=0` · `visibility=hidden` ✓ |
| Selepas skrol (`y=900`) | `class="btt on"` · `opacity=1` · `visibility=visible` ✓ |
| Selepas klik | **`y=0`** — kembali ke atas ✓ |
| Semua halaman | `toTop=1` · `floaters=1` · ralat=0 pada `/nres/{home,faq,sitemap,visitor-statistics}` ✓ |
| EN | `aria-label="Back to top"` ✓ |
| Tangkapan skrin | ↑ teal di atas, chatbot + `✕` di bawah — tiada tindihan ✓ |

**Guna:**
```powershell
php storage/app/cms-page-content/apply-back-to-top.php            # pasang / tampal
php storage/app/cms-page-content/apply-back-to-top.php --dry-run
```
Skrip **idempotent**: kalau butang sudah ada, ia hanya menampal pembetulan CSS yang hilang.

**Backup:** `backups/site2-layout-back-to-top-backup-*.json` (30.5 KB)

