# SJ88 — sj88ai.com (v16.5)

> คอร์สออนไลน์ + Block-based CMS + Blog + PWA + Analytics · Deploy 2026-07-12

**Live**: https://sj88ai.com
**Repo**: https://github.com/lnwsj/sj88ai.com.git
**Backend**: `/opt/sj88auth/` (port 8094) — combined auth + content + orders + checkout + sitemap + analytics
**DB**: `sj88_content`, `sj8‌8_orders`, `sj88_account`, `sj88_kb`, `sj88_tmp`, `sj88_i18n`

---

## What's New in v16.5 (cumulative)

| Version | Feature | Status |
|---------|---------|--------|
| **v16.5** | 📊 **Analytics dashboard** + view tracking | ✅ |
| | 🔒 **Security headers docs** (CF Transform Rules guide) | ✅ |
| **v16.4** | 📝 **Blog system** (index + article reader using Block CMS) | ✅ |
| | 📱 **PWA** (manifest + service worker + offline page) | ✅ |
| **v16.3** | 🛒 **Salepages for 3 new courses** (TikTok AI, สุลาต่าน, VibeCode) | ✅ |
| **v16.2** | 📤 **Image upload** (sharp + multipart) | ✅ |
| | 📦 **/account/orders** (user's own orders) | ✅ |
| **v16.1** | 🧱 **Block-based CMS** + **Auth** + **Checkout** + **4 courses** | ✅ |

### v16.5 — Analytics + Security (current)

| Feature | Description |
|---------|-------------|
| 📊 **Analytics dashboard** | `/dashboard/analytics.html` — 5 stat cards, 2 bar charts, 3 tables (top content, recent users, recent content) |
| 👁 **View tracking** | `POST /content/view?slug=X` (public) — auto-tracks every article read |
| 🔒 **Security headers docs** | `/docs/security-headers.html` — full guide for CF Transform Rules setup (because CF strips nginx add_header) |
| 📈 **Stats API** | `GET /api/admin/stats` (auth) — full JSON payload: content/orders/users breakdowns |
| 🐛 **Bug fix** | Renamed `/admin/stats` → `/api/admin/stats` to avoid nginx proxy conflict with sj88hub |

### v16.4 — Blog + PWA

| Feature | Description |
|---------|-------------|
| 📝 **Blog index** | `/blog/` — 50+ article cards, filter pills (5 categories), search (debounce 250ms) |
| 📄 **Article reader** | `/blog/article.html?slug=X` — renders 14 block types, SEO meta, JSON-LD |
| 📱 **PWA** | `/manifest.webmanifest` + `/sw.js` + `/offline.html` — installable, offline cache |
| 🔗 **Shortcuts** | Manifest includes shortcuts to คอร์ส/บทความ/ค้นหา |

### v16.3 — Salepages × 3

| Course | Slug | Lessons | Price |
|--------|------|---------|-------|
| TikTok AI 2026 | `/courses/tiktok-ai-2026/` | 10 | ฿999 |
| TikTok สุลาต่าน 2026 | `/courses/tiktok-sulatan-2026/` | 8 | ฿999 |
| VibeCode 101 | `/courses/vibecode-101/` | 6 | ฿1,499 |

Each course has `index.html` (salepage) + `checkout.html` (3-step form, 3 pay methods) + `lessons.html`

### v16.2 — Image Upload + My Orders

| Feature | Description |
|---------|-------------|
| 📤 **Image upload** | `POST /auth/upload` — sharp + @fastify/multipart, 20MB max, auto-resize to 1600px (WebP q85) + 400px thumb (q80) |
| 🎨 **Editor dropzone** | Drag-drop image upload in editor's img block |
| 📦 **/account/orders** | New section in account.html — user's orders (filter by user_id or email) |
| 🔗 **Auto-link orders** | `/auth/checkout` now saves `user_id` if logged in |

### v16.1 — Block CMS + Auth + 4 Courses (foundation)

| Feature | Description |
|---------|-------------|
| 🧱 **Block-based CMS** | 14 block types (h1/h2/h3/p/img/video/code/list/checklist/quote/callout/divider/embed/table) stored as JSON in MySQL, rendered to static HTML with full SEO |
| 📤 **Auto publish** | `POST /content/publish` → renders to `/<slug>.html` with OG/Twitter/JSON-LD |
| 🔐 **Auth system** | HttpOnly cookies, bcrypt-12, SHA-256 refresh tokens, JWT-like rotation, `Domain=.sj88ai.com` for sub-shared auth |
| 💳 **Checkout** | 3-step form + 3 pay methods (bank/prompt/TrueMoney) → saves to `sj88_orders` |
| 📚 **4 courses** | Shopee Affiliate (19 lessons) + TikTok AI 2026 (10) + TikTok สุลาต่าน 2026 (8) + VibeCode 101 (6) |
| 🎨 **Vibe theme** | 4-pastel palette (pink/lavender/mint/peach) + Fraunces serif + Inter + IBM Plex Sans Thai |

**Stats**: 50 contents published · 30+ users · 11 orders · 12 features · 7 deployed versions

---

## Architecture

```
Browser (HTTPS via Cloudflare)
    ↓
Nginx (port 443, SSL via Let's Encrypt)
    ├─ /                  → static files (HTML/CSS/JS/images)
    ├─ /auth/*            → proxy to 127.0.0.1:8094 (Fastify auth + content API)
    ├─ /content/*         → proxy to 127.0.0.1:8094 (block CMS API)
    ├─ /api/admin/*       → proxy to 127.0.0.1:8094 (analytics + orders management)
    ├─ /api/              → proxy to 127.0.0.1:8091 (sj88hub)
    ├─ /api/kb/*          → proxy to 127.0.0.1:8092 (sj88kb)
    ├─ /api/tmp/*         → proxy to 127.0.0.1:8093 (sj88tmp)
    ├─ /admin             → proxy to 127.0.0.1:8091 (sj88hub admin UI)
    ├─ /dashboard/*       → static (analytics)
    ├─ /docs/*            → static
    ├─ /blog/*            → static
    ├─ /courses/*         → static (salepages, lessons)
    └─ /uploads/*         → static (image uploads)
```

## Backend Services (systemd)

| Service | Port | Description |
|---------|------|-------------|
| sj88auth | 8094 | Combined auth + content + orders + checkout + sitemap + analytics + upload |
| sj88hub | 8091 | Hub app + admin UI + comments |
| sj88kb | 8092 | Knowledge base |
| sj88tmp | 8093 | Staging area |

## API Endpoints (v16.5)

### Auth (sj88auth:8094 → nginx /auth/)
```
POST /auth/signup           { email, password, name }
POST /auth/login            { email, password }
POST /auth/refresh          (cookie)
POST /auth/logout
GET  /auth/me
POST /auth/update-profile   { name }
POST /auth/change-password  { old_password, new_password }
GET  /auth/sessions
POST /auth/checkout         { email, name, phone, lineId, payMethod, course, amount }
POST /auth/upload           (multipart, image only)
DELETE /auth/upload/:filename
GET  /auth/my-orders        (auth)
```

### Content (sj88auth:8094 → nginx /content/)
```
GET    /content/list?course=&status=&limit=
GET    /content/get?slug=
POST   /content/save        { slug, course_slug, title, ... blocks[] }
POST   /content/publish     { slug }
POST   /content/unpublish
DELETE /content/delete?slug=
GET    /content/preview?slug=
GET    /content/revisions?slug=
POST   /content/view?slug=  (public, increments view_count)
GET    /content/views?slug= (public)
```

### Admin (sj88auth:8094 → nginx /api/admin/)
```
GET    /api/admin/orders
PATCH  /api/admin/orders/:id { status, notes }
GET    /api/admin/stats     (auth — full analytics payload)
GET    /api/admin/recent-orders?limit=20
```

### SEO + PWA
```
GET  /sitemap.xml         (74 URLs, dynamic from DB)
GET  /manifest.webmanifest
GET  /sw.js
GET  /offline.html
```

## Database Schema

### sj88_content.content
```sql
CREATE TABLE content (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  slug VARCHAR(255) UNIQUE NOT NULL,
  course_slug VARCHAR(255) NOT NULL,
  title VARCHAR(500) NOT NULL,
  meta_title, meta_description, meta_image, cover_image, excerpt TEXT,
  status ENUM('draft','published','archived') DEFAULT 'draft',
  author, view_count, tags JSON, blocks_json LONGTEXT,
  published_at, created_at, updated_at
);
```

### sj88_content.content_revisions
```sql
CREATE TABLE content_revisions (
  id, content_id, blocks_json, meta_json, saved_by, saved_at, comment
);
```

### sj88_orders.orders
```sql
CREATE TABLE orders (
  id, reference VARCHAR(40) UNIQUE,  -- SJ88-MRHKWG76-1C91
  email, name, phone, line_id, course, amount,
  pay_method ENUM, status ENUM('pending','paid','failed','refunded'),
  slip_url, user_id, notes, created_at, updated_at
);
```

### sj88_account.users
```sql
-- users + sessions tables (bcrypt-12 + SHA-256 refresh tokens)
```

## 14 Block Types

| type | SEO | ใช้ทำ |
|---|---|---|
| h1, h2, h3 | ⭐⭐⭐⭐⭐ | หัวข้อ |
| p | ⭐⭐⭐⭐ | ย่อหน้า |
| img | ⭐⭐⭐⭐⭐ | รูปเดี่ยว (alt, width, height, lazy load) |
| video | ⭐⭐⭐⭐ | YouTube/Vimeo embed |
| code | ⭐⭐ | โค้ด |
| list | ⭐⭐⭐⭐ | ul/ol |
| checklist | ⭐⭐ | todo list |
| quote | ⭐⭐⭐⭐ | blockquote |
| callout | ⭐⭐ | info/warn/success/error |
| divider | ⭐ | เส้นแบ่ง |
| embed | ⭐ | iframe |
| table | ⭐⭐⭐⭐ | ตาราง |

## Critical Gotchas

1. **Cloudflare strips nginx add_header** — headers configured in nginx don't reach end-user; use CF Transform Rules (see /docs/security-headers.html)
2. **nginx `^~ /admin` blocks `/api/admin`** — use specific paths like `/api/admin/` not `/admin/`
3. **MySQL `dateStrings: true`** — returns dates as strings, not Date objects. Handle with `if (lastmod instanceof Date)` check
4. **Cloudflare cache** — append `?b={timestamp}` to URL when testing HTML pages
5. **Playwright full_page=True** on tall pages (5000+px) renders WITHOUT CSS — use multi-shot pattern instead
6. **Express static 500** — express.static can't find index.html → falls through to SPA fallback → 500. Add explicit route
7. **sharp install on Ubuntu 22.04** — works without native build tools via npm
8. **@fastify/multipart + auth** — register BEFORE routes, attachFieldsToBody: false
9. **Block CMS — strict validation** — only whitelisted block types render, no raw HTML except `embed`
10. **jsDelivr CF cache** — caching 30d even with `Cache-Control: max-age=300`; use ?b= for testing

## Deployment

```bash
# Build (no build step — static files)
# Edit files in /workspace/sj88auth/ then deploy:

# Deploy backend
scp server.js root@103.22.183.111:/opt/sj88auth/
ssh root@103.22.183.111 "systemctl restart sj88auth"

# Deploy frontend
scp file.html root@103.22.183.111:/var/www/sj88ai.com/public_html/

# Reload nginx if config changed
ssh root@103.22.183.111 "nginx -t && systemctl reload nginx"
```

## Testing

```bash
# Playwright + Chromium
executable_path="/root/.cache/ms-playwright/chromium-1223/chrome-linux/chrome"
args=["--no-sandbox", "--disable-gpu", "--use-gl=swiftshader", "--enable-webgl"]

# Service Worker test
const reg = await navigator.serviceWorker.getRegistration();
return reg ? reg.active?.state || 'pending' : 'not-registered';
```

## License & Status

© 2026 SJ88 Studio · All rights reserved · v16.5 · Last deploy: 2026-07-12 18:46 UTC+7
