# RENCANA KERJA — SEO & Domain Hub (pribadi)

**10/10/26 · dasar: CLAUDE.md + PRD v3.1 (00–05) · status: menunggu persetujuan pemilik**

> Ini peta besar. Tiap langkah tetap dibuka dengan rencana detail langkah itu dulu, baru
> kode (CLAUDE.md §1 no. 1). Kalau isi file ini bertentangan dengan CLAUDE.md,
> **CLAUDE.md yang menang**.

---

## 0. CARA KERJA

```
mulai langkah N
  → aku kirim rencana langkah N (file apa saja, urutannya, yang perlu kamu siapkan)
  → kamu setuju / koreksi
  → satu file per respons, kode lengkap
  → kamu simpan di folder lokal → upload ke hosting → uji
  → semua "Selesai kalau" tercentang → CLAUDE.md §7 "Langkah saat ini" dinaikkan
```

Folder lokal meniru server, supaya upload tinggal salin:

```
D:\Project\SEOYONG\
├── CLAUDE.md · RENCANA.md     ← catatan, TIDAK di-upload
├── private\                   → /home/USER/private/
└── public_html\               → /home/USER/public_html/
```

Rahasia (password DB, `APP_KEY`, API key registrar, token bot) **tidak pernah** ditempel
ke chat. Aku kirim templatenya, kamu isi sendiri langsung di hosting.

---

## 1. KERJAKAN DULU — DI LUAR URUTAN (CLAUDE.md §9)

Kalau belum:

- [ ] Revoke **kedua** token bot lama di @BotFather
- [ ] Ganti password DB panel lama
- [ ] Hapus `bot.php.zip` dan `error_log` dari folder web lama

Token & password itu pernah ada di file yang bisa diunduh orang. Selama belum dicabut,
orang lain masih bisa memakai bot dan database lamamu.

---

## 2. TEMUAN — HASIL MENCOCOKKAN CLAUDE.md DENGAN PRD

Belum ada yang kuubah. Yang bertanda ✏️ kutambahkan ke CLAUDE.md §3 (perubahan terhadap
PRD) setelah kamu setuju.

**1. 🔴 Domain layanan harus didaftarkan di cPanel** · langkah 1, 9 · ✏️
Hosting hanya melayani domain yang terdaftar di cPanel. Domain yang belum terdaftar tidak
pernah sampai ke `public_html` kita, jadi `serve.php` tidak pernah dipanggil.
→ Tiap domain layanan ditambahkan sebagai **Alias** (= domain tambahan yang memakai folder
`public_html` yang sama; di cPanel baru: *Domains → Create A New Domain → centang Share
document root*) + SSL. Akun cPanel sebaiknya **khusus** proyek ini.

**2. 🔴 Aturan 4 `.htaccess` PRD merusak API** · langkah 1 · ✏️
Aturan itu me-301 (pindah permanen) semua GET ke file `*.php` supaya URL tampil tanpa
`.php`. Akibatnya `GET /api/domains.php` dilempar ke `/api/domains` → 404. Semua API baca
akan gagal.
→ Kecualikan `/api/` dan `/hooks/` dari aturan 4.

**3. 🔴 Halaman error bisa membocorkan app** · langkah 1–2 · ✏️
404/403 dari Apache memakai `ErrorDocument /404.php` milik app. Di domain layanan, itu bisa
menampilkan halaman berlogo app — melanggar CLAUDE.md §2.2 no. 2.
→ `404.php` cek host dulu: domain layanan → 404 polos.

**4. 🔴 Reset password melewati 2FA** · langkah 4 · ✏️
PRD 02 §19.7 langkah 8 langsung `do_login()` setelah password direset. Kalau email-mu
dibobol, penyerang bisa masuk tanpa kode 2FA → semua DNS ikut jatuh.
→ Setelah reset, tetap wajib kode 2FA.

**5. 🟠 "Ingat saya" melewati password & 2FA** · langkah 4 · ✏️
PRD 02 §5.8 = masuk 30 hari tanpa password dan tanpa 2FA.
→ Tidak dipakai. Akun ini memegang DNS semua domain.

**6. 🟠 CSP memblokir CSS LP** · langkah 11 · ✏️
CSP (= aturan browser soal script/CSS dari mana yang boleh jalan) versi PRD tidak
mengizinkan `<style>` inline, padahal CSS LP memang inline (CLAUDE.md §2.2 no. 6). Begitu
CSP ditegakkan, LP tampil polos tanpa gaya.
→ CSP zona layanan sendiri: tag `<style>` diberi *nonce* (= kode acak per halaman sebagai
"izin"). Melengkapi CLAUDE.md §3 no. 8.

**7. 🟠 Satu pixel tidak bisa dipasang di dua domain** · langkah 16 · ✏️
`tracking_tags` punya UNIQUE (provider, tag_id).
→ Kalau satu pixel memang dipakai di beberapa domain: UNIQUE (provider, tag_id, domain_id),
dan `tracking_snapshot` ikut menyimpan `domain_id`.

**8. 🟡 Backup belum punya jadwal di urutan kerja** · langkah 7 · ✏️
CLAUDE.md §8 mewajibkan backup tiap malam, tapi §7 tidak menaruhnya di langkah mana pun.
→ `backup_db.php` dibuat di langkah 7, saat data penting pertama masuk.

**9. 🟡 TrustPositif tidak punya API resmi** · langkah 14
Cara mengeceknya bisa berubah atau kena captcha.
→ Lampirkan `cron.php` lama (token & password dihapus dulu) untuk dipelajari caranya,
lalu diuji sebelum dipakai.

**10. 🟡 Cloudflare Bot Fight Mode** · langkah 14, 17
Bisa menolak webhook Telegram. Juga memasang cookie `__cf_bm` di domain layanan — bukan
dari kita, tapi muncul saat audit "tanpa Set-Cookie".
→ Diuji di langkah 14 dan 17.

**11. ℹ️ Login pakai email, bukan username** · langkah 4
PRD pakai username, CLAUDE.md pakai email → pakai **email** (CLAUDE.md menang).

**12. ℹ️ PHP cron bisa beda versi** · langkah 1, 14
PHP yang dipakai cron (CLI) bisa beda versi dengan PHP web.
→ Path PHP lengkap dicek lewat `_diag.php` (contoh `/usr/local/bin/ea-php81`).

---

## 3. PERTANYAAN — KAPAN HARUS DIJAWAB

Gabungan CLAUDE.md §11 (§11-1 s.d. §11-7) + pertanyaan baru (B1–B7).

| # | Pertanyaan | Sebelum langkah |
|---|---|---|
| §11-1 | Domain app apa? | **1** |
| B1 | Akun cPanel khusus proyek ini? Penyedia hostingnya apa? | **1** |
| B2 | Upload file ke hosting pakai apa: File Manager cPanel, FTP, atau Git cPanel? | **1** |
| §11-2 | Warna aksen & mode (dark saja / dark + light)? | 2 |
| B3 | 2FA: kode rahasia diketik manual ke aplikasi authenticator (tanpa QR, jadi tanpa library)? HP hilang → pulihkan lewat skrip CLI di Terminal cPanel? | 4 |
| B4 | Email lupa password: `mail()` bawaan hosting (paling simpel) atau SMTP? | 4 |
| §11-3 | Nama folder `modules/` & `templates/` disetujui? Sekalian: JS per halaman (`domains.js`, `pages.js`, …) daripada satu `app.js` raksasa? | 7 |
| B5 | Tambah domain layanan ke cPanel: manual dulu (saran) atau nanti otomatis lewat API cPanel? | 9 |
| B6 | Engine MindMap lama mau dilampirkan untuk dipakai ulang? | 10 |
| §11-4 | Template LP apa saja? Form lead dikirim ke mana? | 11 |
| §11-5 | Teks LP dibantu AI? API mana? | 11 |
| §11-6 | Pixel vendor apa? Perlu di AMP? Satu pixel dipakai di beberapa domain? | 12 / 16 |
| §11-7 | Ada VPS Indonesia untuk cek DNS Nawala, atau TrustPositif saja dulu? | 14 |
| B7 | Lampirkan `cron.php` & `bot.php` lama (rahasia dihapus dulu) | 14 |

---

## 4. PETA LANGKAH

```
FASE A · FONDASI     1 hosting → 2 bootstrap → 3 keamanan → 4 login+2FA → 5 shell → 6 komponen
FASE B · FITUR INTI  7 domain → 8 registrar → 9 route+serve → 10 mindmap → 11 LP → 12 AMP
FASE C · LANJUTAN    13 DNS → 14 monitor+bot → 15 statistik → 16 tracking
FASE D · AUDIT       17
```

- Path tanpa awalan = di dalam `public_html/`.
- Daftar file = perkiraan; versi final ditetapkan di rencana tiap langkah.
- Tiap file = satu respons.

---

### Langkah 1 — Hosting, `.htaccess`, Cloudflare ⬅ SEKARANG

**Tanya dulu:** §11-1 · B1 · B2

**Kamu kerjakan di cPanel (tanpa kode)**

1. Domain app harus memakai `public_html` yang sama dengan domain layanan. Paling aman:
   domain app = domain utama akun cPanel. Kalau pakai subdomain, document root-nya
   diarahkan ke `public_html`.
2. *MultiPHP Manager* / *Select PHP Version* → PHP 8.1 atau lebih baru.
3. *MySQL Databases* → buat database + user, hak penuh **ke database itu saja**. Simpan di
   password manager, bukan di chat.
4. *File Manager* → buat `/home/USER/private/` berisi `cache/ logs/ migrations/ scripts/`
   (izin 700).
5. Cek batas jumlah Alias di paket hosting (±23 domain dari SFLINK + domain baru).

**Kamu kerjakan di Cloudflare (domain app)**

1. Record DNS domain app di-proxy (awan oranye).
2. SSL/TLS **Full (Strict)** + *Origin Certificate* dipasang di cPanel
   (*SSL/TLS → Manage SSL Sites*).
3. Always Use HTTPS ON · TLS minimum 1.2 · Rocket Loader OFF · sisanya PRD 02 §3.1.
4. Cache Rule: **seluruh hostname app → Bypass cache**. Lebih simpel dari daftar path di
   PRD 02 §3.3, karena semua halaman app memang private.
5. WAF & rate limit POST `/login` sesuai PRD 02 §3.2. HSTS di Cloudflare **nanti**, setelah
   HTTPS 100% jalan.

**File (urutan penting)**

| # | File | Catatan |
|---|---|---|
| 1 | `_diag.php` | PRD 02 §1 + cek tambahan: cURL & `curl_multi`, GD + WebP, `exec` (untuk backup), `dns_get_record`, path PHP CLI, tes koneksi ke Telegram/Dynadot/Cloudflare/TrustPositif. Dibuka sekali → hasilnya ditempel ke chat (tidak memuat rahasia) → **dihapus**. Wajib **sebelum** `.htaccess`, karena `.htaccess` memblokir `_diag.php` |
| 2 | `.htaccess` | urutan aturan di bawah |
| 3 | `.user.ini` | PRD 02 §4.3; `USER` diisi dari hasil `_diag.php` |
| 4 | `uploads/.htaccess` | PHP dimatikan di folder upload (PRD 01 §4) |
| 5 | `.htaccess` satu baris `Require all denied` | sama untuk `config/` `modules/` `templates/` `partials/` |

Urutan aturan `.htaccess`:

```
1  HTTPS 301                               (kecuali /.well-known/ → validasi SSL)
2  buang www 301
3  host ≠ domain app → serve.php           (kecuali /uploads/media/ · /.well-known/ · /serve.php)
4  /x.php → 301 /x                         (kecuali /api/ dan /hooks/ — temuan 2)
5  /x/ → 301 /x
6  lewati aset & file fisik
7  /robots.txt → robots.php                (app: Allow, tanpa Sitemap)
8  URL bersih: login logout dashboard domains routes pages monitor registrars settings admin
   /login/(forgot|reset|totp|confirm) → auth/…
—  dihapus dari PRD: /id/ /en/ /zh/, laman public, sitemap app, verify/link/google-*
—  tetap dari PRD: blokir file sensitif · LimitExcept GET POST HEAD · header keamanan · ErrorDocument
```

**Selesai kalau** (di PowerShell Windows pakai `curl.exe`, bukan `curl`)

- [ ] Hasil `_diag.php` hijau, lalu file itu sudah dihapus
- [ ] `curl.exe -I http://DOMAIN_APP/` → `301` ke `https://`
- [ ] `curl.exe -I https://DOMAIN_APP/config/` → `403`

**Rujukan PRD:** 02 §1, §3, §4.3–§4.4 · 01 §2, §4

---

### Langkah 2 — Bootstrap & konfigurasi

**Tanya dulu:** §11-2

**Penyederhanaan** ✏️ — app cuma bahasa id dan semua noindex, jadi tanpa
`config/lang_loader.php`, folder `lang/`, `config/seo.php`, dan `config/membership.php`.
Teks UI langsung bahasa Indonesia. SEO domain layanan nanti di `modules/serve/ServedSeo.php`.

**File:**
`private/migrations/001_settings.sql` (versi pribadi: tanpa batas paket,
`registration_open = 0`, kunci tracking) · `private/migrations/002_auth.sql` (`users`
+ `totp_secret_enc`, `sessions`, `login_attempts`, `security_events`, `password_resets`;
`auth_tokens` hanya kalau "Ingat saya" tetap dipakai) · `private/config.example.php`
(template; kamu salin jadi `config.php` lalu isi sendiri) · `bootstrap.php`
(+ `APP_SERVE_MODE`) · `config/db.php` · `config/settings.php` · `config/template.php` ·
`config/url.php` · `config/guard.php` (versi awal: header keamanan) · `config/tracking.php` ·
`config/tracking_providers.php` · `partials/head.php` · `partials/tracking.php` ·
`assets/css/tokens.css` · `assets/css/base.css` · `robots.php` · `404.php` (temuan 3) ·
`index.php` (sementara: halaman kosong)

**Selesai kalau:** halaman kosong tampil tanpa error · ada header
`X-Robots-Tag: noindex` · ada penanda `<!-- tracking:start -->`

**Rujukan PRD:** 01 §5–§11, §13–§14 · 03 §2–§3 · 05 §6–§7

---

### Langkah 3 — Session DB, password, rate limit, CSRF

**Saran** ✏️: tabel `weak_passwords` (100 ribu baris) dilewati — pemiliknya cuma kamu dan
sudah ada 2FA.

**File:** `config/guard.php` (lengkap: session di DB, password, rate limit, CSRF, IP asli
lewat Cloudflare, log keamanan, CSP mulai *Report-Only* = hanya melapor) ·
`api/_bootstrap.php` · `private/scripts/cleanup.php` · `private/scripts/refresh_cf_ips.php`
+ jadwal cron cPanel

**Selesai kalau:** uji PRD 02 §16 yang sudah bisa diuji lolos (sisanya diulang di langkah
4, 5, dan 17)

**Rujukan PRD:** 02 §3.4, §5–§10, §13, §16, §20

---

### Langkah 4 — Akun pemilik, login, 2FA

**Tanya dulu:** B3 · B4 · temuan 4 & 5

**File:** `private/scripts/create_owner.php` (CLI: buat akun + password, kode rahasia 2FA
ditampilkan sekali; ada opsi reset 2FA) · `login.php` · `auth/totp.php` ·
`auth/forgot.php` · `auth/reset.php` · `auth/confirm.php` (password + kode 2FA ulang untuk
aksi sensitif, berlaku < 15 menit) · `logout.php` (POST) · `index.php` (302 ke
`/dashboard` atau `/login`) · fungsi kirim email

**Selesai kalau:** masuk dengan 2FA · tanpa kode → ditolak · email lupa password sampai ·
setelah reset tetap diminta kode 2FA · logout lewat POST

**Rujukan PRD:** 02 §5.5–§5.6, §6.1–§6.4, §7, §19.4, §19.7–§19.8

---

### Langkah 5 — Shell dashboard

**File:** `partials/sidebar.php` (Ringkasan · Domain · Route · Halaman · Monitor · Registrar
· Pengaturan · Admin · Keluar) · `partials/topbar.php` · `partials/flash.php` ·
`assets/css/dashboard.css` · `assets/js/theme.js` · `assets/js/shell.js` · `dashboard.php`
(kerangka)

**Selesai kalau:** rapi di 375px–1440px (sidebar penuh → rel ikon → drawer di HP)

**Rujukan PRD:** 03 §3, §5–§6

---

### Langkah 6 — Kerangka dashboard (komponen)

**File:** `assets/css/components.css` · `assets/js/ui.js` (modal/bottom sheet, toast,
konfirmasi, dropdown) · `assets/js/api.js` (fetch + CSRF) · `dashboard.php` (toolbar, grid,
empty state)

**Selesai kalau:** 4 keadaan ada — loading, kosong, error, terisi

**Rujukan PRD:** 03 §7–§8, §10, §14

---

### Langkah 7 — Domain & grup

**Tanya dulu:** §11-3

**File:** migrasi (`groups`, `registrar_accounts`, `domains` — `registrar_accounts` dibuat
di sini karena dirujuk `domains`) · `api/groups.php` · `api/domains.php` · `domains.php` ·
JS halaman domain · `cleanup.php` (+ kosongkan sampah > 30 hari) ·
`private/scripts/backup_db.php` (temuan 8)

**Selesai kalau:** tambah/ubah/hapus, favorit, sampah, pulihkan, cari/urut/filter jalan ·
badge status tampil · backup malam pertama terbentuk dan bisa dibuka

**Rujukan PRD:** 01 §12–§13 · 02 §9, §14.2 · 03 §12

---

### Langkah 8 — Registrar Dynadot & Cloudflare

**Siapkan:** API key Dynadot & token Cloudflare — diisi di dashboard, bukan di chat

**File:** `config/http.php` (pembungkus cURL, dipakai ulang semua modul) ·
`modules/registrar/RegistrarInterface.php` · `Dynadot.php` · `Cloudflare.php` ·
`api/registrars.php` · `registrars.php` · `private/scripts/cron_sync_domains.php`

**Selesai kalau:** Tes koneksi hijau · Sinkron menarik domain + tanggal kedaluwarsa · API
key hanya tampil 4 karakter terakhir, tidak ada di log

---

### Langkah 9 — `serve.php` + route + editor list

**Tanya dulu:** B5 · siapkan 1 domain uji (sudah Alias + SSL)

**File:** migrasi (`routes`, `click_logs`; `pages` & `media_files` ikut dibuat lebih awal
karena dirujuk `routes`) · `modules/serve/HostResolver.php` · `RouteResolver.php` ·
`RedirectGuard.php` (cek loop & rantai) · `modules/stats/ClickLogger.php` · `serve.php` ·
`api/routes.php` (+ Terapkan draft, Tes link) · `routes.php` (view=list) ·
`assets/js/routes_list.js`

**Selesai kalau:** redirect di domain uji jalan · `curl.exe -I` sesuai matriks CLAUDE.md
§2.3 · route draft tidak terlihat publik · loop ditolak · tidak ada `Set-Cookie` dari kita

---

### Langkah 10 — Editor mindmap

**Tanya dulu:** B6

**File:** `assets/js/routes_map.js` (SVG vanilla: geser, zoom/pinch, tarik node, bottom
sheet) · `routes.php` (view=map) · `api/routes.php` (simpan `pos_x`/`pos_y`) · tambahan
`dashboard.css`

**Selesai kalau:** edit di map = berubah di list, dan sebaliknya · target sentuh ≥ 44px

---

### Langkah 11 — Generator LP + SEO domain layanan

**Tanya dulu:** §11-4 · §11-5 · temuan 6

**File:** `modules/serve/LpRenderer.php` · `modules/serve/ServedSeo.php` (meta, canonical,
JSON-LD, robots.txt, sitemap.xml per domain) · `templates/lp/lp_01.php` · `lp_02.php` ·
`lp_03.php` · `api/media.php` (upload: re-encode GD, nama acak, maks 5MB, tanpa GIF) ·
`api/pages.php` · `pages.php` · JS halaman LP · `serve.php` (+ lp, robots, sitemap) ·
`config/guard.php` (+ CSP zona layanan)

**Selesai kalau:** LP tampil di domain uji · tepat satu `<h1>` · gambar ber-`width`/`height`
· robots.txt & sitemap.xml tiap domain benar dan berbeda · Rich Results Test tanpa error

**Rujukan PRD:** 02 §11 · 04 §5–§7

---

### Langkah 12 — AMP

**Tanya dulu:** §11-6 (pixel di AMP?)

**File:** migrasi `amp_pairs` · `modules/serve/AmpRenderer.php` · `templates/amp/amp_01.php`
· `partials/trk_amp.php` · `config/tracking.php` (jaring pengaman melewati AMP) ·
`config/guard.php` (CSP AMP) · `api/amp.php` · `pages.php` (panel cek AMP)

**Selesai kalau:** lolos AMP Test · LP ↔ AMP saling menunjuk · AMP tanpa script sisipan

---

### Langkah 13 — Editor DNS

**File:** migrasi `dns_changes` · modul registrar (+ fungsi DNS & nameserver) ·
`api/dns.php` · editor DNS di halaman domain + JS-nya

**Selesai kalau:** ubah record dari dashboard (pratinjau sebelum/sesudah → password + kode
2FA) · tercatat di `dns_changes` · Batalkan jalan

---

### Langkah 14 — Monitor + bot Telegram + impor SFLINK

**Tanya dulu:** §11-7 · B7 · siapkan token bot **baru** dari @BotFather (kamu isi sendiri
di `config.php`)

**File:** migrasi (`domain_checks`, `telegram_targets`, `telegram_link_codes`,
`bot_states`) · `modules/monitor/sources/TrustPositif.php` · `sources/DnsCheck.php` ·
`Checker.php` · `StatusTracker.php` · `Telegram.php` · `BotCommands.php` ·
`hooks/telegram.php` · `api/monitor.php` · `api/telegram.php` · `monitor.php` ·
`private/scripts/cron_monitor.php` (+ `flock`) · `private/scripts/set_webhook.php`
(daftar webhook sekali) · `private/scripts/import_sflink.php`

**Selesai kalau:** notif masuk saat status berubah (bukan tiap cek) · webhook tanpa secret
header ditolak · ±23 domain lama masuk · 940 baris user kembar jadi satu target

---

### Langkah 15 — Statistik klik

**File:** migrasi `click_daily` · `private/scripts/cron_stats_rollup.php` · `api/stats.php`
· grafik SVG vanilla di `dashboard.php` & `routes.php`

**Selesai kalau:** grafik per domain/route tampil · log mentah > 30 hari terhapus

---

### Langkah 16 — Pengaturan + tracking per domain

**Tanya dulu:** §11-6 · temuan 7

**File:** migrasi `tracking_tags` (+ `domain_id`) · `config/tracking.php` (snapshot &
filter per domain) · `api/tracking.php` · `partials/admin_tracking.php` ·
`assets/js/admin_tracking.js` · `admin.php` (tab tracking + log keamanan) · `settings.php`
(password, 2FA, sesi aktif)

**Selesai kalau:** pixel muncul di LP domain itu saja · zona app tetap tanpa tracker

**Rujukan PRD:** 05 §4–§12

---

### Langkah 17 — Audit

CLAUDE.md §10 + PRD 02 §16 + PRD 05 §12 + SSL Labs ≥ A + securityheaders.com ≥ A +
Lighthouse mobile LP ≥ 90 + uji pulihkan backup + CSP ditegakkan penuh.

**Selesai kalau:** semua centang
