# CLAUDE.md — SEO & Domain Hub (pribadi)

**Instruksi tetap untuk Claude Code · 10/10/26**

Website SaaS **pribadi** (satu pemilik) untuk mengelola banyak domain dari satu
dashboard: generator LP, halaman AMP (bisa beda domain), redirect lewat editor
list + mindmap, DNS multi-registrar, dan monitor Nawala/TrustPositif dengan notif
Telegram.

---

## 0. TENTANG PRD

Proyek ini mengikuti **Protokol Build SaaS v3.1** (file `00_PANDUAN.md` sampai
`05_TRACKING.md`). File PRD **tidak ada di folder proyek** — disimpan di drive lokal
pemilik.

- Kalau butuh isi section PRD (mis. "02 §11 upload"), **minta pemilik melampirkannya**.
  Jangan menebak isinya.
- Aturan PRD tetap berlaku, kecuali yang diubah di §3 file ini.
- Kalau file ini bertentangan dengan PRD, **file ini yang menang**, karena ini versi
  khusus pribadi.

---

## 1. ATURAN KERJA

1. Tunjukkan **rencana dulu**, tunggu persetujuan, baru tulis kode.
2. **Satu file per respons.** Kode lengkap, siap copy-paste.
3. Jangan ubah warna, tabel, atau format yang sudah didesain manual.
4. Jangan tambah library/dependency tanpa izin. Tanpa npm, tanpa build step.
5. Ragu = **tanya**. Cek dulu §11 (pertanyaan terbuka).
6. Jangan taruh secret di kode (§8).
7. Jawab ringkas, bahasa Indonesia santai. Istilah teknis dijelaskan singkat,
   karena pemilik masih belajar.
8. Format tanggal yang tampil: **dd/mm/yy**.

**Stack — jangan diganti:** PHP 8.1+ · MySQL/PDO · shared hosting cPanel ·
HTML + CSS + JS vanilla. Session PHP diperkuat, disimpan di MySQL (bukan
better-auth/JWT). Cron lewat cPanel (CLI). Semua HTTP keluar pakai **cURL**
(`allow_url_fopen` mati).

---

## 2. DUA ZONA

| Zona | Host | Isi |
|---|---|---|
| **App** | domain app (§11 no. 1) | login + dashboard. **Seluruhnya private & noindex** |
| **Layanan** | semua domain lain yang terdaftar di tabel `domains` | LP, AMP, redirect, `robots.txt`, `sitemap.xml` per domain |

### 2.1 Zona app — karena pribadi

- **Tidak ada laman public** (tanpa pricing/faq/about), tidak ada prefix bahasa,
  bahasa **id** saja.
- `/` → 302 ke `/dashboard` (atau `/login` kalau belum masuk).
- Semua halaman: header `X-Robots-Tag: noindex, nofollow` + meta robots noindex.
  `robots.txt` app tetap `Allow`, supaya Google bisa melihat noindex-nya.
- **Pendaftaran ditutup.** Satu akun pemilik (role master), dibuat lewat skrip CLI.
- Login email + password + **2FA TOTP wajib** (aplikasi authenticator). Akun ini
  memegang DNS semua domain, jadi satu password saja tidak cukup.
- Lupa password via email tetap ada (alur PRD 02 §19).
- Tidak ada membership, paket, atau batas pemakaian.
- Kolom `user_id` tetap ada di semua tabel dan selalu dipakai di WHERE (kebiasaan
  aman, dan siap kalau suatu hari dibuka untuk orang lain).

### 2.2 Zona layanan

Alur satu permintaan:

```
https://promo-a.com/diskon
   │
   ▼
.htaccess: host ≠ domain app → serve.php  (kecuali /uploads/media/)
   │
   ▼
serve.php  (APP_SERVE_MODE: tanpa session, tanpa cek host app)
   ├─ host ada di `domains` & aktif?   tidak → 404 polos
   ├─ /robots.txt  → dibuat per domain
   ├─ /sitemap.xml → LP indexable domain ini
   └─ (domain, path) di `routes` live
        ├─ lp       → 200 render LP
        ├─ amp      → 200 render AMP
        ├─ redirect → 301/302 + catat klik
        ├─ gone     → 410
        └─ tidak ada → route "*" kalau ada, kalau tidak 404
```

Aturan:
1. Tanpa session dan tanpa cookie.
2. Tidak pernah menautkan atau menyebut domain app.
3. Host tidak dikenal → **404 polos**, bukan 301 ke app.
4. Tujuan redirect hanya dari database, skema hanya `http`/`https`. Tidak ada `?to=`.
5. Satu bahasa per LP (`pages.lang`) → `<html lang>`.
6. CSS LP & AMP inline di template.
7. Ikon dari Iconify, ditempel sebagai `<svg>` inline. Jangan script CDN.

### 2.3 Matriks keputusan

```
route lp aktif              → 200, canonical ke dirinya (atau canonical_override)
route amp aktif             → 200 AMP, canonical → LP pasangannya
route redirect              → 301/302 sesuai isian, klik dicatat
route gone                  → 410
path tidak ada              → route "*" kalau ada, kalau tidak 404
domain nonaktif/di sampah   → 404
host tidak terdaftar        → 404 polos
route draft                 → tidak terlihat publik sampai "Terapkan"
domain app, belum login     → 302 /login (noindex)
```

---

## 3. PERUBAHAN TERHADAP PRD

| # | Aturan PRD | Diubah jadi | Alasan |
|---|---|---|---|
| 1 | `enforce_canonical_host()` me-301 semua host lain | Dilewati saat `APP_SERVE_MODE` (diset `serve.php` sebelum bootstrap) | Kalau tidak, semua domain layanan terlempar ke app |
| 2 | `.htaccess` aturan bahasa & URL berlaku untuk semua host | Aturan paling atas: host ≠ domain app → `serve.php`. Aturan prefix `/en/` `/zh/` dihapus | Domain app satu-satunya yang ditulis di `.htaccess`; ganti domain app = ubah baris ini juga |
| 3 | Laman public, hreflang, sitemap app | Dihapus untuk zona app | Pribadi, tidak perlu diindeks |
| 4 | Daftar akun + Google OAuth | Pendaftaran ditutup, OAuth tidak dipakai, 2FA TOTP wajib | Satu pemilik |
| 5 | Membership & batas paket (03 §11) | Dihapus | Pribadi |
| 6 | Jaring pengaman tracking menyisipkan `<script>` ke semua HTML | Lewati dokumen AMP (`<html ⚡` / `<html amp`); AMP pakai `partials/trk_amp.php` (amp-analytics/amp-pixel) yang tetap mencetak penanda | Script di AMP = AMP tidak valid |
| 7 | Tracker hanya global | Kolom `tracking_tags.domain_id`: NULL = app (dibiarkan kosong), terisi = domain layanan itu. Tetap hanya isi ID | Pixel iklan beda per domain |
| 8 | CSP untuk halaman app | CSP terpisah untuk AMP (izinkan `cdn.ampproject.org`, `<style amp-custom>`) | AMP butuh runtime-nya |
| 9 | Semua POST lewat CSRF di `api/_bootstrap.php` | `hooks/telegram.php` punya pembuka sendiri: hanya POST, cek header `X-Telegram-Bot-Api-Secret-Token`, chat_id harus terdaftar | Telegram tidak membawa token CSRF |

---

## 4. STRUKTUR FILE

```
/home/USER/
├── private/                         ← di luar public_html
│   ├── config.php                   ← DB, APP_KEY, token bot, secret webhook (chmod 600)
│   ├── cache/  logs/
│   ├── migrations/                  ← NNN_nama.sql
│   └── scripts/                     ← dijalankan cron cPanel via CLI, BUKAN URL
│       ├── create_owner.php         ← buat akun pemilik (sekali)
│       ├── cron_monitor.php         ← cek Nawala/TrustPositif + notif
│       ├── cron_sync_domains.php    ← tarik domain & tgl kedaluwarsa dari registrar
│       ├── cron_stats_rollup.php    ← ringkas klik, hapus log lama
│       ├── cleanup.php · backup_db.php
│       └── import_sflink.php        ← impor panel lama (§9), sekali jalan
│
└── public_html/
    ├── .htaccess · bootstrap.php
    ├── index.php                    ← 302 ke /dashboard atau /login
    ├── serve.php                    ← pintu masuk zona layanan
    ├── login.php · logout.php · auth/ (forgot, reset, totp)
    ├── dashboard.php                ← ringkasan: domain diblokir, kedaluwarsa, klik
    ├── domains.php                  ← /domains
    ├── routes.php                   ← /routes?view=list | ?view=map
    ├── pages.php                    ← /pages  (LP + AMP)
    ├── monitor.php                  ← /monitor
    ├── registrars.php               ← /registrars
    ├── settings.php · admin.php     ← admin: tracking, log keamanan
    │
    ├── hooks/telegram.php           ← SATU-SATUNYA webhook bot
    │
    ├── config/                      ← Require all denied · isi sesuai PRD 01 §2
    ├── modules/                     ← Require all denied
    │   ├── registrar/  RegistrarInterface · Dynadot · Cloudflare · Namecheap
    │   ├── serve/      HostResolver · RouteResolver · RedirectGuard
    │   │               LpRenderer · AmpRenderer · ServedSeo
    │   ├── monitor/    Checker · sources/TrustPositif · sources/DnsCheck
    │   │               StatusTracker · Telegram · BotCommands
    │   └── stats/      ClickLogger
    ├── templates/                   ← Require all denied
    │   ├── lp/   lp_01.php lp_02.php lp_03.php
    │   └── amp/  amp_01.php
    ├── partials/                    ← sesuai PRD + trk_amp.php
    ├── api/                         ← semua require _bootstrap.php · GET & POST + action
    │   domains · groups · routes · pages · amp · registrars · dns
    │   monitor · telegram · media · stats
    ├── assets/
    │   ├── css/  tokens · base · components · dashboard
    │   └── js/   theme · api · ui · shell · app · routes_list · routes_map
    └── uploads/media/               ← engine off
```

---

## 5. FITUR

### 5.1 Domain & grup
- CRUD domain dan grup (pengelompok), favorit, sampah, cari, urut, filter.
- Badge: 🟢 aman · 🔴 diblokir · ⚪ belum dicek · 🟠 gagal cek · ⏳ hampir kedaluwarsa.
- Domain hasil sinkron registrar langsung masuk daftar.

### 5.2 Registrar & DNS
- Dynadot dan Cloudflare dulu; Namecheap menyusul (IP hosting harus di-whitelist).
- API key disimpan dengan `app_encrypt()`; ke browser hanya 4 karakter terakhir.
- Tombol **Tes koneksi** dan **Sinkron domain**.
- Editor DNS: lihat, tambah, ubah, hapus record; nameserver; URL forwarding (kalau didukung).
- Setiap perubahan: pratinjau sebelum/sesudah → konfirmasi → catat di `dns_changes`
  (nilai lama disimpan untuk **Batalkan**).
- Butuh password + kode 2FA ulang (< 15 menit): ubah akun registrar, DNS, nameserver,
  hapus permanen, token Telegram.

### 5.3 Editor route (layar kerja utama)
Satu tabel `routes`, dua tampilan:

| | List | Mindmap |
|---|---|---|
| Bentuk | tabel: domain · path · aksi · tujuan · kode · status | Domain → Path → Tujuan |
| Default | HP (≤768px) | desktop |
| Interaksi | edit di baris, filter, cari | geser, zoom (pinch), tarik node ke tujuan baru, tap node → bottom sheet |

- Aksi: `lp` · `amp` · `redirect` (301/302) · `gone` (410).
- **Draft → Terapkan** dalam satu transaksi.
- Cek **loop** (A→B→A ditolak) dan **rantai** (> 2 lompatan diberi peringatan),
  termasuk tujuan yang berupa domain sendiri.
- **Tes link**: panggil URL via cURL, tampilkan kode status nyata.
- Posisi node disimpan (`pos_x`, `pos_y`).
- Mindmap: SVG vanilla, tanpa library. Engine MindMap lama pemilik boleh dipakai
  ulang kalau cocok.
- Mobile: target sentuh ≥44px, input ≥16px, uji di lebar 375px.

### 5.4 Generator LP & SEO zona layanan
- Form: judul, sub-judul, penawaran, CTA + tujuan, gambar, fitur, testimoni, FAQ →
  `pages.content_json`.
- 3 template awal. Pratinjau. Duplikat halaman.
- Upload gambar: jpg/png/webp, re-encode GD, nama acak, max 5MB, tanpa GIF.
- SEO per halaman: `seo_title`, `seo_description`, `is_indexable`,
  `canonical_override`, gambar OG. Konten utama di HTML awal, satu `<h1>`,
  gambar selalu `width`+`height`.
- JSON-LD sesuai jenis LP dan isinya sama dengan yang tampil.
- `robots.txt` per domain: ada LP indexable → `Allow` + `Sitemap`; domain redirect
  saja → `Allow: /`.
- `sitemap.xml` per domain: LP indexable saja, lastmod = `pages.updated_at`.
  AMP dan redirect tidak masuk.

### 5.5 AMP (termasuk beda domain)
- Dibuat otomatis dari data LP yang **sama**, jadi isinya selalu cocok.
- `amp_pairs`: LP mencetak `rel="amphtml"` → AMP; AMP mencetak `rel="canonical"` → LP.
- Cek di dashboard: kedua tag saling menunjuk, LP 200, LP tidak noindex, AMP tanpa
  script buatan sendiri. Validasi resmi manual di AMP Test Google.
- AMP beda domain: tampilkan **info** bahwa Search Console bisa memberi peringatan
  "AMP page domain mismatch".
- AMP bukan sinyal ranking; manfaatnya kecepatan di HP.

### 5.6 Monitor Nawala/TrustPositif + Telegram
- Sumber: TrustPositif (utama), DNS Nawala (opsional, butuh VPS Indonesia).
- `curl_multi` per kelompok 10 domain, coba ulang 2x.
- Hasil `safe` · `blocked` · `error`. `error` tidak mengubah status.
- Status berubah setelah **2 hasil berturut-turut sama**.
- Notif **hanya saat status berubah** + ringkasan harian opsional.
- Riwayat di `domain_checks` (90 hari).
- Bot: `/start` · `/link KODE` (kode dari dashboard, 10 menit, sekali pakai) ·
  `/cek domain.com` · `/status` · `/add` · `/del` (pakai **tombol**) · `/pause` · `/resume`.
  Chat yang belum di-link hanya dapat balasan umum.
- State bot di tabel `bot_states`.
- Webhook didaftarkan **sekali** ke `hooks/telegram.php` dengan `secret_token`.
- Cron: `php /home/USER/private/scripts/cron_monitor.php` tiap 15–30 menit, pakai `flock`.

### 5.7 Statistik
- `click_logs` tiap redirect & kunjungan LP (IP di-hash, negara dari header Cloudflare).
- `click_daily` ringkasan harian; log mentah dihapus setelah 30 hari.
- Grafik sederhana per domain/route.

### 5.8 Tracking
- Hook tracking PRD 05 tetap dipasang di semua halaman.
- Zona app: **tanpa tracker** (pribadi).
- Zona layanan: tracker per domain (§3 no. 7). AMP lewat `trk_amp.php`.

---

## 6. DATABASE

InnoDB · utf8mb4_unicode_ci · setiap tabel punya `id`, `created_at`, `updated_at` ·
FK dengan `ON DELETE` eksplisit · index di FK & kolom WHERE · perubahan skema lewat
file migrasi.

| Tabel | Kolom inti | Kunci & catatan |
|---|---|---|
| `users` · `sessions` · `auth_tokens` · `login_attempts` · `security_events` · `settings` | sesuai PRD | + `totp_secret_enc` di `users` |
| `groups` | user_id, name, is_favorite, is_trashed, trashed_at | |
| `domains` | user_id, group_id, registrar_account_id, name, is_active, expires_at, monitor_enabled, monitor_status, monitor_checked_at, monitor_changed_at, is_favorite, is_trashed, trashed_at | uq name · group & registrar SET NULL |
| `registrar_accounts` | user_id, provider, label, api_user, secret_enc, secret_hint, is_active, last_sync_at, last_error | |
| `dns_changes` | domain_id, user_id, action, record_type, record_name, old_value, new_value, status, provider_code | |
| `pages` | user_id, template_key, lang, title, content_json, seo_title, seo_description, og_media_id, is_indexable, canonical_override, published_at, is_trashed, trashed_at | |
| `routes` | user_id, domain_id, path, action, page_id, target_url, redirect_code, is_active, draft_json, has_draft, pos_x, pos_y | uq (domain_id, path) · domain CASCADE · page RESTRICT |
| `amp_pairs` | page_id, lp_route_id, amp_route_id, last_check_status, last_checked_at | uq (lp_route_id, amp_route_id) |
| `media_files` | user_id, file_name, width, height, bytes | |
| `domain_checks` | domain_id, source, result, detail, checked_at | 90 hari |
| `telegram_targets` | user_id, chat_id, kind, label, is_active, linked_at | uq chat_id |
| `telegram_link_codes` | user_id, code_hash, expires_at, used_at | |
| `bot_states` | chat_id, state, data_json, expires_at | uq chat_id |
| `click_logs` | domain_id, route_id, ip_hash, ua_hash, referer_host, country | 30 hari |
| `click_daily` | route_id, day, hits | uq (route_id, day) |
| `tracking_tags` | sesuai PRD 05 + domain_id NULL | |

---

## 7. URUTAN KERJA

| # | Langkah | Selesai kalau |
|---|---|---|
| 1 | Hosting, folder `private/`, `.htaccess` (+ aturan host), `.user.ini`, Cloudflare | `_diag.php` hijau, HTTPS 301 jalan |
| 2 | `settings` + `bootstrap.php` (+ `APP_SERVE_MODE`) + `config/*` + migrasi auth + `tokens.css` | halaman kosong tampil tanpa error |
| 3 | Session DB, password, rate limit, CSRF | uji PRD 02 §16 lolos |
| 4 | `create_owner.php`, `/login`, 2FA TOTP, lupa password, logout POST | bisa masuk dengan 2FA |
| 5 | Shell dashboard: sidebar, topbar, drawer HP | responsif 375px–1440px |
| 6 | Kerangka dashboard: toolbar, grid, empty state, modal, toast | 4 keadaan (loading/kosong/error/terisi) ada |
| 7 | `domains` + `groups` + `api/domains.php` + trash | CRUD jalan |
| 8 | Registrar Dynadot & Cloudflare + sinkron | domain tertarik otomatis |
| 9 | `serve.php` + `routes` + editor **list** + redirect + cek loop + draft | redirect di domain layanan jalan |
| 10 | Editor **mindmap** | edit di map = berubah di list |
| 11 | `pages` + template LP + upload + robots/sitemap per domain | LP tampil di domain layanan |
| 12 | AMP + `amp_pairs` + `trk_amp` + pengecualian jaring pengaman + CSP AMP | lolos AMP Test |
| 13 | Editor DNS + nameserver + `dns_changes` + Batalkan | ubah record dari dashboard |
| 14 | Monitor + bot + webhook + cron + impor SFLINK | notif masuk saat status berubah |
| 15 | Statistik klik + rollup | grafik tampil |
| 16 | Settings + tracking per domain | pixel muncul di LP domain itu saja |
| 17 | Audit (§10) | semua centang |

Langkah saat ini: **1**. Perbarui baris ini tiap langkah selesai.

---

## 8. KEAMANAN — YANG KHUSUS PROYEK INI

- Secret di `private/config.php`: kredensial DB, `APP_KEY`, token bot, secret webhook.
- API key registrar & secret TOTP: `app_encrypt()` dengan `APP_KEY`.
- Tidak pernah dicetak ke log atau respons API: API key, token, isi request body.
- Cron hanya via CLI; skrip di `private/scripts/` tidak bisa diakses lewat URL.
- Zona layanan tidak punya cookie, jadi tidak ada sesi yang bisa dicuri dari domain LP.
- Backup DB terenkripsi tiap malam, simpan 14 hari, uji pulihkan tiap kuartal.

---

## 9. DATA LAMA — PANEL SFLINK

Wajib dulu (kalau belum): revoke **kedua** token bot lama di @BotFather, ganti
password DB lama, hapus `bot.php.zip` dan `error_log` dari folder web lama.

`import_sflink.php` (sekali, via CLI):
- ±23 domain dari `list_domains` → `domains` dengan `monitor_enabled = 1`.
- 940 baris user kembar → satu `telegram_targets`.
- Skrip lama (`bot.php`, `cron.php`, `index.php`) tidak dipakai lagi.

---

## 10. AUDIT

```
[ ] Zona app: semua halaman noindex, /dashboard tanpa login → 302 /login
[ ] Halaman daftar akun tidak ada (404)
[ ] Login tanpa kode 2FA → ditolak
[ ] Domain layanan: curl -I sesuai matriks §2.3
[ ] Host tidak dikenal → 404 polos, tidak menyebut domain app
[ ] Domain layanan tidak mengirim Set-Cookie
[ ] Redirect loop ditolak; route draft tidak terlihat publik
[ ] robots.txt & sitemap.xml tiap domain layanan benar dan berbeda
[ ] AMP lolos AMP Test; LP ↔ AMP saling menunjuk; AMP tanpa script sisipan
[ ] Tiap LP & AMP tepat satu penanda tracking
[ ] Webhook tanpa secret header → ditolak
[ ] Perubahan DNS tercatat dan bisa dibatalkan
[ ] API key tidak muncul di respons maupun log
[ ] Tampilan dashboard rapi di 375px
```

---

## 11. PERTANYAAN TERBUKA — TANYA SEBELUM LANGKAH TERKAIT

| # | Pertanyaan | Sebelum langkah |
|---|---|---|
| 1 | Domain app apa? (saran: subdomain khusus, mis. `app.domainmu.com`) | 1 |
| 2 | Warna aksen & mode (dark saja / dark + light)? | 2 |
| 3 | Nama folder `modules/` dan `templates/` disetujui? | 7 |
| 4 | Template LP apa saja (produk, lead form, link WhatsApp)? Form lead dikirim ke mana? | 11 |
| 5 | Teks LP dibantu AI? Kalau ya, API mana? | 11 |
| 6 | Pixel di LP: vendor apa (Meta, TikTok, GA4)? Perlu juga di AMP? | 12 / 16 |
| 7 | Ada VPS Indonesia untuk cek DNS Nawala, atau TrustPositif saja dulu? | 14 |
