---
name: website-creator
description: สร้างเว็บไซต์เต็มรูปแบบด้วย Next.js + Payload CMS พร้อม Workflow สำหรับเว็บใหม่และ Migration ครอบคลุม Design System, Content Collections, Auth, SEO, PDPA Compliance และ Deploy
tags: [nextjs, website, website-development, website-creation, migration, tailwindcss, thai, pdpa, seo, payload-cms, isr, image-generation, picture-it]
category: software-development
related_skills:
  - spec-driven-development
  - frontend-ui-engineering
  - api-and-interface-design
  - code-review-and-quality
  - performance-optimization
  - browser-testing-with-devtools
  - shipping-and-launch
---

# Website Creator Skill

สร้างเว็บไซต์เต็มรูปแบบด้วย Next.js + Payload CMS

## Architecture

**Payload admin บังคับใช้ Next.js App Router**
- `@payloadcms/next` ต้องการ Next.js App Router — ไม่สามารถใช้ Astro แทนได้
- วิธีแก้: Next.js เป็น framework เดียว ทั้ง Payload admin และ frontend

**Pattern อ้างอิง:** github.com/payloadcms/payload/tree/main/templates/with-postgres

### Next.js App Router Structure

```
src/app/
├── (payload)/              # Payload admin + API
│   ├── admin/[[...segments]]/  ← Payload admin panel (:3000/admin)
│   ├── api/[[...slug]]/       ← REST API (:3000/api/...)
│   ├── api/graphql/            ← GraphQL endpoint
│   ├── api/graphql-playground/
│   ├── custom.scss
│   └── layout.tsx
│
└── (frontend)/             # Frontend pages ของเรา
    ├── layout.tsx
    ├── page.tsx             ← หน้าแรก
    ├── globals.css
    └── posts/[[...slug]]/   ← dynamic post pages
```

**Database: MongoDB (Default — ใช้ mongooseAdapter)**
- PostgreSQL ถูกตัดออกแล้ว ทุก project ใช้ MongoDB เป็นมาตรฐาน
- ถ้าต้องการ PostgreSQL → ใช้ template อื่นหรือสร้างเอง

### ISR vs Static

- **Static (default):** หน้าส่วนใหญ่ pre-built HTML + cache
- **Dynamic:** ราคา/ส่วน dynamic ใช้ `revalidate` หรือ client-side fetch
- ปรับได้ใน `generateStaticParams` + `revalidate` tags

## Critical Configuration Rules

### 1. payload.config.ts (MongoDB — มาตรฐาน)

```ts
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
import { buildConfig } from 'payload'
import { fileURLToPath } from 'url'

export default buildConfig({
  admin: {
    user: Users.slug,
    importMap: { baseDir: path.resolve(dirname) },
  },
  collections: [Users, Media, Posts],
  editor: lexicalEditor(),
  secret: process.env.PAYLOAD_SECRET || '',
  typescript: { outputFile: path.resolve(dirname, 'payload-types.ts') },
  db: mongooseAdapter({
    url: process.env.MONGODB_URL || 'mongodb://localhost:27017/my-database',
  }),
})
```

**ติดตั้ง MongoDB adapter:**
```bash
pnpm add @payloadcms/db-mongodb@3.82.0
```

**รัน MongoDB ด้วย Docker:**
```bash
docker run -d --name mongo -p 27017:27017 mongo:7
```

**Environment:**
```env
MONGODB_URL=mongodb://localhost:27017/my-database
PAYLOAD_SECRET=your-secret-here
```

### 2. Required Dependencies

```json
{
  "dependencies": {
    "@payloadcms/next": "^3.82.0",
    "@payloadcms/db-postgres": "^3.82.0",
    "@payloadcms/richtext-lexical": "^3.82.0",
    "@payloadcms/ui": "^3.82.0",
    "payload": "^3.82.0",
    "next": "^16.2.3",
    "react": "^19.2.4",
    "react-dom": "^19.2.4",
    "sharp": "^0.34.2",
    "cross-env": "^7.0.3"
  },
  "devDependencies": {
    "tailwindcss": "^3.4.0",
    "postcss": "^8.4.0",
    "autoprefixer": "^10.4.0"
  }
}
```

**⚠️ ติดตั้ง Tailwind ต้องใช้ v3 (`tailwindcss@3`) ไม่ใช่ v4** — v4 มี syntax ต่างกัน (`@import "tailwindcss"` vs `@tailwind base;`) และ Payload 3.x ยังไม่รองรับ v4 อย่างเป็นทางการ
- ถ้า `npm install -D tailwindcss` ติด peer dep conflict → ใช้ `--legacy-peer-deps`
- ถ้า project มี `"type": "module"` → postcss config ต้องเป็น `postcss.config.cjs` (ไม่ใช่ `.js`)
- globals.css สำหรับ v3: ใช้ `@tailwind base; @tailwind components; @tailwind utilities;`

### 3. next.config.ts

```ts
import { withPayload } from '@payloadcms/next/withPayload'
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'standalone',
  images: {
    localPatterns: [{ pathname: '/api/media/file/**' }],
  },
}

export default withPayload(nextConfig, { devBundleServerPackages: false })
```

### 4. Collection Imports — ใช้ `import type`

```ts
import type { CollectionConfig } from 'payload'
// ห้ามใช้ `import { CollectionConfig } from 'payload'` — มันคือ type เท่านั้น
```

---

## Workflow A: สร้างเว็บใหม่

### Step 1: Pre-Project Questions

ใช้ `references/questions.md` เป็นแนวทาง:

```
1. ชื่อเว็บไซต์/บริษัท?
2. ทำอะไร? (ขายอะไร/ให้บริการอะไร)
3. กลุ่มเป้าหมายคือใคร?
4. มีเว็บอยู่แล้วหรือยัง?
5. ชอบสี/ดีไซน์แบบไหน?
6. มี logo file ไหม?
7. ต้องการหน้าอะไรบ้าง?
8. มี SMTP/email สำหรับส่งเมลไหม?
9. มี GA4/Marketing tools ไหม?
10. มี DPO หรือยัง?
```

---

### Step 2: Plan Sitemap + Content Structure

**เรียก skill: `spec-driven-development`** — เขียน SPEC.md ก่อนลงมือทุกครั้ง

แสดง sitemap ให้ user ดูก่อนเสมอ รอ approve ก่อนลงมือ:

```
/                           # Home
/about                      # About Us
/services                   # Services overview
/contact                    # Contact + Form
/blog                       # Blog listing (ถ้ามี)
/blog/[slug]               # Individual blog post
/privacy-policy            # PDPA Privacy Policy
/terms-of-service          # PDPA Terms of Service
```

---

### Step 3: Design Framework

**เรียก skills: `ckm:design` + `frontend-ui-engineering` + `ckm:ui-styling` + `ui-ux-pro-max`**

ขั้นตอน:
1. ถามว่ามี brand guidelines หรือ reference ไหม
2. ถ้าไม่มี → ถาม 2 คำถาม:
   - ชอบ dark mode ไหม หรือ light mode อย่างเดียว?
   - ชอบ style แบบไหน: minimal, bold, creative?
3. ใช้ `ckm:design` เพื่อสร้าง design system
4. ใช้ `ui-ux-pro-max` เพื่อออกแบบ wireframes
5. ใช้ `ckm:ui-styling` เพื่อสร้าง components

---

### Step 4: Setup Next.js + Payload CMS Project

**ใช้ template ที่มีอยู่แล้ว: `templates/nextjs-payload-starter/`**

```bash
# 1. Copy template ไปที่ project ใหม่
cp -r ~/.hermes/skills/website-creator/templates/nextjs-payload-starter /path/to/my-website

# 2. เข้าไปใน project
cd /path/to/my-website

# 3. ติดตั้ง dependencies
pnpm install

# 4. สร้าง .env
cp .env.example .env
# แก้ PAYLOAD_SECRET และ DATABASE_URL

# 5. Generate types
pnpm generate:types
pnpm generate:importmap

# 4. รัน dev server
```bash
# Next.js dev default: binds all interfaces, ไม่ต้องใส่ --host
pnpm dev
```

**เปิด browser:**
- Frontend: http://localhost:3000 หรือ http://<external-ip>:3000
- Payload Admin: http://localhost:3000/admin หรือ http://<external-ip>:3000/admin

**ถ้า port 3000 มี process อื่นใช้ หรือ /admin ขึ้น 404:**
```bash
# Kill ทุก Next.js process แล้วรันใหม่
pkill -9 -f "next dev"
cd /home/kunthawat/moreminimore-next && rm -rf .next && pnpm dev
```

---

### Step 5: พัฒนา Components + Pages

**เรียก skills: `spec-driven-development` + `api-and-interface-design` + `ckm:design` + `ckm:ui-styling` + `payload` + `picture-it`**

สร้างตาม sitemap ที่วางแผนไว้:
- FrontendLayout, Navigation, Footer
- Pages (Home, About, Services, Contact)
- Blog listing + detail pages
- Forms (Contact form ส่ง email จริง)
- Auth (Payload built-in auth)

**สำคัญ — แยก Design Layer กับ Content Layer:**

Design skill (ui-ux-pro-max) ออกแบบ **หน้าตา + layout + animation** — ไม่ใช่ content structure
Payload Lexical เก็บ **เนื้อหา (ข้อความ, format, links, images)** — ไม่ใช่ layout

ทั้งสองอยู่คนละ layer กัน → ต้องแยกทำ แล้วมารวมกันตอน integrate

ดู **Design + Payload Integration** ด้านล่าง สำหรับ workflow การรวม design output กับ Payload content

**ตัวอย่าง:**

```bash
/skill ckm:ui-styling
"สร้าง components สำหรับเว็บบริษัท:
 - Navigation bar (sticky, responsive)
 - Hero section
 - Service cards
 - Footer with contact info
 - Button styles
 - Form inputs"
```

---

### Step 5b: สร้างภาพประกอบด้วย `picture-it`

**เรียก skill: `picture-it`**

สำหรับทุก page/section ที่ต้องการ hero image, illustration หรือภาพประกอบ:

**วิธีใช้:**

```bash
# Load credentials ก่อน
set -a && source ~/.config/opencode/.env && set +a
export PATH="/home/kunthawat/snap/bun-js/87/.bun/bin:$PATH"

# ตรวจสอบ Thai font patch
bun ~/.hermes/skills/website-creator/creative/picture-it/scripts/thai-font-patch.ts --force
```

**Workflow สร้างภาพประกอบเว็บ:**

| Use Case | Pipeline | ค่าใช้จ่าย |
|----------|----------|-----------|
| Hero background | `generate flux-schnell` + `text` (Thai) + `grade cinematic` + `vignette` | ~$0.003 |
| Service illustration | `generate flux-schnell` + `remove-bg` (ถ้ามี product) + `compose` | ~$0.01 |
| Blog hero | `generate flux-schnell` + `edit seedream` (place logo) + `grade` | ~$0.043 |
| Social/OG image | `generate flux-schnell` + `text` + `grade` | ~$0.003 |
| Team/About photo | `generate flux-schnell` → realistic headshot style | ~$0.003 |

**ขนาดภาพแนะนำ:**

| Purpose | Size |
|---------|------|
| Hero banner | 1200×630 หรือ 1920×1080 |
| Blog hero | 1200×630 |
| OG Image (social share) | 1200×630 |
| Service card | 800×600 |
| Team member | 400×400 (circle crop) |
| Logo (ในภาพ) | 200×200 หรือเล็กกว่า |

**Workflow ตัวอย่าง — Hero Section:**

```bash
# 1. Generate background
picture-it generate \
  --prompt "modern tech workspace, clean minimal, yellow #fed400 accent lighting" \
  --size 1200x630 \
  --model flux-schnell \
  -o hero-bg.png

# 2. เพิ่ม Thai text
picture-it text \
  -i hero-bg.png \
  --title "บริการรับทำเว็บไซต์ SEO" \
  --font "Kanit" \
  --font-size 48 \
  -o hero-texted.png

# 3. Color grade
picture-it grade -i hero-texted.png --name cinematic -o hero-final.png
```

**Workflow ตัวอย่าง — Service Illustration:**

```bash
# 1. Generate scene
picture-it generate \
  --prompt "professional web development workflow, code screen, modern design tools" \
  --size 800x600 \
  --model flux-schnell \
  -o service-illustration.png

# 2. Remove background if needed
picture-it remove-bg -i service-illustration.png -o service-no-bg.png
```

**การเลือก Model:**

| Task | Model | เหตุผล |
|------|-------|--------|
| Draft/fast | `flux-schnell` | $0.003 — ใช้ได้เลยสำหรับ 90% ของ use cases |
| ต้องการ text ในภาพ | `recraft-v3` | รองรับภาษาไทยในตัว image ได้ดีกว่า |
| ต้องการแก้ไขเฉพาะจุด | `kontext` | $0.04 — สำหรับ localized edits |
| Realistic photo | `banana-pro` | $0.15 — สำหรับ team/about photos |

**การบันทึกภาพไว้ใน project:**

```
src/app/(frontend)/
├── assets/
│   ├── heroes/         ← Hero images
│   ├── services/       ← Service illustrations
│   ├── blog/           ← Blog heroes
│   └── og/              ← Open Graph images
```

**หมายเหตุ:**
- ภาพทุกภาพที่สร้างจาก picture-it ให้เก็บ source file (PNG) ไว้ใน project ด้วย
- สำหรับ OG Image ควรสร้าง template pipeline ที่ consistent ทั้งเว็บ (ใช้ `--font "Kanit"` + `--font-size` คงที่)
- ถ้าต้องการ team photos ที่ realistic ใช้ `banana-pro` และ prompt ที่ระบุชัดเจนเรื่อง nationality/ethnicity เพื่อให้ได้ผลลัพธ์ที่ตรงความต้องการ

---

### Step 5c: Design + Payload Content Integration

**Design layer กับ Content layer แยกกัน — ค่อยรวมตอน build**

#### สิ่งที่ต้องเข้าใจ

```
┌─────────────────────────────────────────────────────────┐
│  ui-ux-pro-max / ckm:design — Design Layer              │
│  • Component structure (Hero, Card, Navbar)              │
│  • Color tokens, typography, spacing                      │
│  • Animation specs (150-300ms, ease-out)                 │
│  • Layout grid, responsive breakpoints                   │
│  • Interaction states (hover, press, disabled)           │
│  Output: React + Tailwind code (component skeleton)      │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  Payload CMS — Content Layer                           │
│  • ข้อความ + format (bold, italic, link)               │
│  • Headings (H1-H6)                                     │
│  • Lists, blockquotes, code blocks                      │
│  • Images, links                                       │
│  Output: Lexical JSON (เนื้อหา)                         │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  Integration — ครอบ Payload content ด้วย Design       │
│  • Design component ครอบ RichText output               │
│  • Animation class ที่ wrapper element                  │
│  • Design tokens apply ผ่าน Tailwind prose             │
└─────────────────────────────────────────────────────────┘
```

#### ขั้นตอนที่ถูกต้อง

```
[1] Design Phase
    ui-ux-pro-max → Component structure, tokens, animations
    Output: Component skeleton (ไม่มี content)
         ↓
[2] Payload Phase
    สร้าง Collections + richText Fields
    Output: Content structure ใน Payload
         ↓
[3] Content Phase
    พิมพ์ content ใน /admin (Lexical editor)
    Output: Lexical JSON
         ↓
[4] Integration Phase
    ครอบ Payload content ด้วย Design components
```

#### ตัวอย่าง: Component Structure (Design Output)

Design skill อาจให้ component แบบนี้:

```tsx
// ❌ สิ่งที่ design skill อาจให้มา — hardcode content
<div className="hero-section">
  <h1>Welcome to Our Site</h1>  // hardcode
  <p className="hero-desc">Amazing content here...</p>  // hardcode
</div>
```

ต้องแปลงเป็น:

```tsx
// ✅ ครอบ Payload content ด้วย design component
import { RichText } from '@payloadcms/richtext-lexical'

<div className="hero-section animate-hero-in">
  <h1 className="text-5xl font-bold">{post.title}</h1>
  {post.heroContent && (
    <RichText
      data={post.heroContent}
      className="prose prose-xl max-w-3xl"
    />
  )}
</div>
```

#### Payload Collection: แบ่ง Content Fields ตาม Section

กำหนดว่า content แต่ส่วนเก็บใน field ไหน:

```ts
// src/collections/Posts.ts
const Posts: CollectionConfig = {
  slug: 'posts',
  fields: [
    { name: 'title',       type: 'text',     required: true },
    { name: 'slug',         type: 'text',     required: true },
    { name: 'heroContent',  type: 'richText' },   // content สำหรับ Hero section
    { name: 'features',     type: 'array',
      fields: [
        { name: 'heading',  type: 'text' },
        { name: 'content',  type: 'richText' },   // content ในแต่ละ feature card
      ]
    },
    { name: 'testimonial',  type: 'richText' },
    { name: 'featuredImage', type: 'upload', relationTo: 'media' },
    { name: 'status',        type: 'select', options: [...], defaultValue: 'draft' },
  ],
}
```

#### วิธี Integrate: Design Components + Payload Content

```tsx
// src/app/(frontend)/posts/[slug]/page.tsx
import { getPost } from '@/lib/payload-helpers'
import { RichText } from '@payloadcms/richtext-lexical'
import { cn } from '@/lib/utils'

// Design tokens จาก ui-ux-pro-max
const tokens = {
  hero:      'text-5xl md:text-7xl font-bold tracking-tight',
  section:   'py-20 px-6 max-w-7xl mx-auto',
  card:      'rounded-2xl border border-slate-200 p-6 shadow-sm',
  animate:   'animate-fade-in duration-300 ease-out',
}

function HeroSection({ title, content }: { title: string; content: any }) {
  return (
    <section className={cn(tokens.section, 'text-center')}>
      <h1 className={cn(tokens.hero, 'mb-6')}>{title}</h1>
      {content && (
        <div className="max-w-3xl mx-auto">
          {/* Payload content → RichText → design wrapper */}
          <RichText data={content} className="prose prose-lg" />
        </div>
      )}
    </section>
  )
}

function FeatureCard({ heading, content }: { heading: string; content: any }) {
  return (
    <div className={cn(tokens.card, tokens.animate)}>
      <h3 className="text-xl font-semibold mb-3">{heading}</h3>
      {content && <RichText data={content} className="prose prose-sm" />}
    </div>
  )
}

function FeaturesGrid({ features }: { features: any[] }) {
  return (
    <section className={cn(tokens.section, 'bg-slate-50')}>
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-7xl mx-auto">
        {features.map((f, i) => (
          <FeatureCard key={i} heading={f.heading} content={f.content} />
        ))}
      </div>
    </section>
  )
}

export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)
  if (!post) return <div>Not found</div>

  return (
    <main className="min-h-screen">
      {/* Hero: design component ครอบ Payload content */}
      <HeroSection title={post.title} content={post.heroContent} />

      {/* Features: layout จาก design, content จาก Payload */}
      {post.features?.length > 0 && (
        <FeaturesGrid features={post.features} />
      )}

      {/* Testimonial: design wrapper + Payload content */}
      {post.testimonial && (
        <section className={cn(tokens.section, 'bg-slate-900 text-white')}>
          <RichText data={post.testimonial} className="prose prose-invert" />
        </section>
      )}
    </main>
  )
}
```

#### Animation ทำยังไง

Animation apply ที่ **wrapper element** ไม่ใช่ที่ content — เพราะ Lexical JSON เก็บแค่ content structure ไม่เก็บ animation

```tsx
// Design layer: ui-ux-pro-max ให้ animation spec
const animation = {
  hero:   'animate-hero-in',
  card:   'animate-card-in',
  stagger: 'delay-100',   // stagger ระหว่าง cards
}

// Content layer: Payload content อยู่ใน richText
// Integration: รวม animation class กับ Payload output

<RichText
  data={post.content}
  className="prose"
/>

// Apply animation ที่ wrapper element
<div className="animate-hero-in">
  <RichText data={post.content} />
</div>

// หรือใช้ wrapper component
<AnimatedSection variant="hero">
  <RichText data={post.content} />
</AnimatedSection>
```

#### Tailwind Prose Setup

ติดตั้ง typography plugin เพื่อ style richText output:

```bash
pnpm add @tailwindcss/typography
```

```ts
// tailwind.config.ts
plugins: [require('@tailwindcss/typography')],
```

ใช้ class `prose` กับ `<RichText>` component:

```tsx
<RichText data={post.content} className="prose prose-lg max-w-none" />
```

#### สรุป: Design + Payload Layer

| Design Layer ทำ | Payload Layer ทำ | Integration ทำ |
|-----------------|------------------|----------------|
| Component structure | Content storage | ครอบ `RichText` ด้วย design component |
| Color/tokens | richText fields | Apply design tokens กับ Payload output |
| Typography system | Visual editor (/admin) | Style richText output ด้วย prose class |
| Animation specs | Content rendering | Wrap output ด้วย animation classes |
| Layout grid | SEO fields (via plugin) | Layout คงที่ + content จาก Payload |

**หลักการ:** Design skill สร้าง "ภาชนะ" — Payload สร้าง "เนื้อหา" — Integration ค่อยรวมกัน

---

### Step 7: PDPA Compliance

**ใช้ templates ที่มีอยู่แล้ว:**

1. **Privacy Policy** — ใช้ `templates/privacy-policy.md`
   - ภาษากฎหมาย PDPA ครบถ้วน
   - แทนที่ placeholders ด้วยข้อมูลจริง

2. **Terms of Service** — ใช้ `templates/terms-of-service.md`

3. **PDPA Topics ที่ต้องครอบคลุม:**

**3.1 Cookie Consent Popup**
- ไม่ block content
- Options: Accept All, Reject All, Customize
- ถ้า reject → ไม่ load GA4/marketing scripts

**3.2 Consent Logging (PDPA)**
- เก็บ log ลง Payload collection ชื่อ `consent-logs`
- ระบุ: action, purpose, analytics/marketing/functional flags, ip, userAgent, timestamp, previousConsent, newConsent

**ไฟล์ที่ต้องสร้าง:**

1. **`src/collections/ConsentLogs.ts`** - Payload collection:
```ts
import type { CollectionConfig } from 'payload'

const ConsentLogs: CollectionConfig = {
  slug: 'consent-logs',
  admin: { useAsTitle: 'timestamp', defaultColumns: ['timestamp', 'action', 'purpose', 'ip'] },
  access: {
    create: () => true,  // public endpoint
    read: () => true,
    update: () => false, // immutable
    delete: () => false, // immutable
  },
  fields: [
    { name: 'action', type: 'select', required: true, options: [
      { label: 'Accept', value: 'accept' },
      { label: 'Reject', value: 'reject' },
      { label: 'Update', value: 'update' },
    ]},
    { name: 'purpose', type: 'select', required: true, options: [
      { label: 'Analytics', value: 'analytics' },
      { label: 'Marketing', value: 'marketing' },
      { label: 'Functional', value: 'functional' },
      { label: 'All', value: 'all' },
    ]},
    { name: 'analytics', type: 'checkbox', defaultValue: false },
    { name: 'marketing', type: 'checkbox', defaultValue: false },
    { name: 'functional', type: 'checkbox', defaultValue: false },
    { name: 'userAgent', type: 'text', admin: { readOnly: true } },
    { name: 'ip', type: 'text', admin: { readOnly: true } },
    { name: 'timestamp', type: 'date', required: true, admin: { readOnly: true } },
    { name: 'previousConsent', type: 'json', admin: { readOnly: true } },
    { name: 'newConsent', type: 'json', admin: { readOnly: true } },
  ],
}
export default ConsentLogs
```

2. **`src/app/api/consent/route.ts`** - API endpoint (Next.js App Router):
```ts
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@/payload.config'

export async function POST(request: NextRequest) {
  const payload = await getPayload({ config: await config })
  const body = await request.json()
  const { action, purpose, analytics, marketing, functional, previousConsent } = body

  const ip = request.headers.get('x-forwarded-for')?.split(',')[0] || 'unknown'
  const userAgent = request.headers.get('user-agent') || 'unknown'

  const log = await payload.create({
    collection: 'consent-logs',
    data: {
      action, purpose,
      analytics: analytics ?? false,
      marketing: marketing ?? false,
      functional: functional ?? false,
      userAgent, ip,
      timestamp: new Date().toISOString(),
      previousConsent: previousConsent || null,
      newConsent: { analytics, marketing, functional },
    },
  })
  return NextResponse.json({ success: true, doc: log })
}
```

3. **`src/components/cookie-banner.tsx`** - Client component ที่เก็บ localStorage + call API

4. **เพิ่ม ConsentLogs ใน `payload.config.ts`:**
```ts
import { ConsentLogs } from './collections/ConsentLogs'
// ...
collections: [Users, Media, Snacks, Orders, ConsentLogs],
```

5. **ติดตั้ง MongoDB adapter (ถ้าใช้ MongoDB):**
```bash
pnpm add @payloadcms/db-mongodb@3.49.1
```
```ts
import { mongooseAdapter } from '@payloadcms/db-mongodb'
db: mongooseAdapter({
  url: process.env.MONGODB_URL || 'mongodb://localhost:27017/my-database',
})
```

**3.3 Right to be Forgotten**
- API endpoint สำหรับลบข้อมูล user

---

### Step 8: SEO Setup

**เรียก skills: `seo-analyzers` + `seo-geo` + `seo-multi-channel` + `payload` (sub-skill)**

1. **Payload SEO Analyzer Plugin** (`@consilioweb/seo-analyzer`):
   - ติดตั้งและเพิ่มใน `payload.config.ts` plugins array
   - ทำให้ `/admin` มี SEO dashboard, score tracking, redirect manager
   - ดู: **SEO Analyzer Plugin Integration** ด้านล่าง

2. **Meta tags ทุกหน้า** — ใช้ meta fields จาก Payload collection หรือ static
3. **sitemap.xml** — Next.js dynamic route หรือ `@payloadcms/plugin-seo`
4. **robots.txt**
5. **Open Graph images** — **ใช้ `picture-it` สร้าง OG image template ที่ consistent**
6. **JSON-LD structured data**
7. **Thai language optimization**

---

### Step 9: Preview + QA

```bash
pnpm dev --host 0.0.0.0
```

**ก่อน QA — เรียก skills ตามลำดับ:**

1. **`code-review-and-quality`** — Full multi-axis code review ก่อน delivery
2. **`performance-optimization`** — Performance audit + fixes
3. **`browser-testing-with-devtools`** — ทดสอบใน real browser

**จากนั้นเรียก `dogfood` สำหรับ exploratory QA:**

```bash
/skill dogfood
"ทดสอบ user journey:
 1. ดู home page → คลิก services
 2. ดู about us
 3. กรอก contact form
 4. ทดสอบ cookie consent popup
 5. ตรวจ PDPA compliance (privacy policy, terms)
 6. ตรวจ mobile responsive
 7. ตรวจ SEO meta tags ทุกหน้า"
```

ให้ user ตรวจสอบ → รอ feedback → แก้ไข → รอ approve

---

### Step 10: Deploy

**เรียก skill: `easypanel-deploy`**

```bash
/skill easypanel-deploy
"deploy ไปยัง easypanel server
 server: openclaw-vps
 project: my-website
 domain: example.com
 git repo: https://github.com/user/my-website"
```

---

## Workflow B: ปรับปรุงเว็บที่มีอยู่แล้ว (Existing Repo)

### ความหมายของ "Existing Repo"

Repo ที่มีโครงสร้าง Next.js + Payload แล้ว แต่ยังไม่สมบูรณ์ — เช่น:
- มี starter template แต่ไม่มี content
- มี design tokens แล้วแต่ยังไม่มี pages
- มีบางหน้าแต่ต้องการปรับปรุงเนื้อหา/design

**ไม่ใช่** 新规 creation — มี codebase อยู่แล้ว ปรับในที่เดิม

**รวมถึง Migration ของ Content เข้า Payload CMS** — Posts และ Pages ต้องย้ายเข้า Payload backend เพื่อให้แก้ไขจาก `/admin` ได้ทันทีหลัง login

---

### ขั้นตอน

```
[1] ตรวจสอบ repo ที่มี (terminal/find ดูโครงสร้าง)
        │
[2] สำรวจ: payload.config.ts, components, pages, design tokens
        │
[3] สำรวจ content ที่มี (Posts, Pages จากเว็บเดิม) → วางแผน Migration
        │
[4] สร้าง Payload Collections (Posts + Pages) ถ้ายังไม่มี
        │
[5] Migrate content เข้า Payload CMS
        │
[6] ตรวจสอบ /admin ว่าเห็น Posts + Pages พร้อมแก้ไขได้
        │
[7] Integrate: อัปเดต Frontend ให้อ่านจาก Payload แทน hardcode
        │   (ดู **Design + Payload Integration** ด้านบน)
        │
[8] สรุปสิ่งที่มี vs สิ่งที่ขาด → แผน Sitemap
        │
[9] ถามคำถามที่ขาด (content, portfolio, pricing, dark/light)
        │
[10] รอ approve
        ↓
จากนั้น → Workflow A Step 3 เป็นต้นไป (พัฒนา Components + Pages)
```

### [1] ตรวจสอบ Repo ที่มี

```bash
# ดูโครงสร้างไฟล์ (ไม่เอา node_modules/.next)
find /path/to/repo -maxdepth 3 -type f \
  -not -path '*/node_modules/*' \
  -not -path '*/.next/*'

# ดู payload.config.ts, tailwind.config.ts
# ดู globals.css, Navigation, Footer
# ดู src/app/ (frontend) pages
```

### [2] สำรวจสิ่งที่มี vs ขาด

| หมวด | ต้องตรวจ |
|------|---------|
| Design | colors, fonts, tailwind config, globals.css, btn/card components |
| Pages | หน้าอะไรบ้างใน `(frontend)` |
| Collections | Payload collections มีอะไร — **ต้องมี `Posts` และ `Pages` collection สำหรับแก้ไขจาก `/admin`** |
| Content Migration | Posts/Pages จากเว็บเก่าถูกย้ายเข้า Payload หรือยัง |
| Auth | login/register pages |
| PDPA | CookieBanner, ConsentLogs |
| SEO | meta, sitemap, robots |

**Content Migration (Posts & Pages → Payload CMS):**

ถ้าเว็บเดิมมี posts หรือ pages อยู่แล้ว → ต้องสร้าง Payload collections (`Posts`, `Pages`) และย้ายข้อมูลเข้า Payload backend ทันที เพื่อให้:
- Login เข้า `/admin` แล้วเห็น posts/pages พร้อมแก้ไข
- ใช้ Payload rich-text editor แก้ไข content ได้โดยตรง
- ไม่ต้องแก้ไขโค้ดเมื่อเพิ่ม/ลบ post

**วิธีตรวจสอบ content เดิม:**

### [3] วางแผน Content Migration

ค้นหา content เดิมที่ต้องย้าย:
```bash
# ดูว่ามีไฟล์ content อะไรอยู่แล้ว
find /path/to/repo/src -type f \( -name "*.md" -o -name "*.mdx" -o -name "*.json" \) | head -30

# ดู posts/pages ใน frontend directory
ls /path/to/repo/src/app/\(frontend\)/**
```

วางแผน: content อยู่ในรูปไฟล์ไหน (MD/MDX/JSON) มีกี่รายการ แต่ละรายการมี fields อะไรบ้าง

### [4] สร้าง Payload Collections (Posts + Pages)

ถ้ายังไม่มี `src/collections/Posts.ts` และ `Pages.ts` → สร้างตามนี้:

1. **`src/collections/Posts.ts`**:
```ts
import type { CollectionConfig } from 'payload'

const Posts: CollectionConfig = {
  slug: 'posts',
  admin: {
    useAsTitle: 'title',
    defaultColumns: ['title', 'slug', 'status', 'updatedAt'],
  },
  access: { read: () => true },
  fields: [
    { name: 'title', type: 'text', required: true },
    { name: 'slug', type: 'text', required: true },
    { name: 'status', type: 'select',
      options: [{ label: 'Draft', value: 'draft' }, { label: 'Published', value: 'published' }],
      defaultValue: 'draft' },
    { name: 'excerpt', type: 'text' },
    { name: 'content', type: 'richText' },
    { name: 'featuredImage', type: 'upload', relationTo: 'media' },
    { name: 'publishedAt', type: 'date' },
    { name: 'updatedAt', type: 'date', admin: { readOnly: true } },
  ],
}
export default Posts
```

2. **`src/collections/Pages.ts`**:
```ts
import type { CollectionConfig } from 'payload'

const Pages: CollectionConfig = {
  slug: 'pages',
  admin: {
    useAsTitle: 'title',
    defaultColumns: ['title', 'slug', 'status'],
  },
  access: { read: () => true },
  fields: [
    { name: 'title', type: 'text', required: true },
    { name: 'slug', type: 'text', required: true },
    { name: 'status', type: 'select',
      options: [{ label: 'Draft', value: 'draft' }, { label: 'Published', value: 'published' }],
      defaultValue: 'draft' },
    { name: 'content', type: 'richText' },
    { name: 'updatedAt', type: 'date', admin: { readOnly: true } },
  ],
}
export default Pages
```

3. **เพิ่มใน `payload.config.ts`**:
```ts
import { Posts } from './collections/Posts'
import { Pages } from './collections/Pages'
// ...
collections: [Users, Media, Posts, Pages],
```

4. **Generate types + restart dev server** หลังเพิ่ม collection:
```bash
pnpm generate:types && pnpm generate:importmap
# restart dev server
```

### [5] ย้าย Content เข้า Payload

**วิธีที่ 1 — เข้า `/admin` ด้วยตัวเอง (แนะนำ ถ้า content น้อย):**
- ล็อกอิน `/admin`
- ไปที่ Posts → กด New Post → กรอก content จากเว็บเดิม
- ไปที่ Pages → กด New Page → กรอก content จากเว็บเดิม

**วิธีที่ 2 — Seed Script (ถ้า content เยอะ):**
เขียน script ใช้ Payload SDK ย้ายทีละหลายรายการ:

```ts
// src/scripts/migrate-content.ts
import { getPayload } from 'payload'
import config from '../payload.config'

const payload = await getPayload({ config: await config })

// ตัวอย่าง: ย้าย post เดียว
await payload.create({
  collection: 'posts',
  data: {
    title: 'ชื่อบทความ',
    slug: 'slug-url-friendly',
    status: 'published',
    excerpt: 'คำอธิบายย่อ',
    content: { root: { children: [...] } }, // Lexical JSON
    publishedAt: new Date().toISOString(),
  },
})
```

รัน seed script:
```bash
npx tsx src/scripts/migrate-content.ts
```

### [6] ตรวจสอบ /admin

หลังย้ายเสร็จ → login `/admin` แล้วตรวจ:

**Posts collection:**
- [ ] เห็น `Posts` ใน sidebar ซ้าย
- [ ] กดเปิด post → เห็น title, slug, content, excerpt ถูกต้อง
- [ ] แก้ไข post จาก Payload rich-text editor ได้
- [ ] ลบ post จาก /admin → หายจาก frontend ทันที

**Pages collection:**
- [ ] เห็น `Pages` ใน sidebar ซ้าย
- [ ] กดเป็น page → เห็น title, slug, content ถูกต้อง
- [ ] แก้ไข page จาก Payload editor ได้
- [ ] ลบ page จาก /admin → หายจาก frontend ทันที

**Frontend sync:** (ดู **Design + Payload Integration** ด้านบน สำหรับรายละเอียด)
- [ ] หน้า blog listing แสดง posts จาก Payload ถูกต้อง
- [ ] หน้า static pages แสดง content จาก Payload ถูกต้อง
- [ ] `<RichText>` component แสดง Lexical JSON เป็น HTML ถูกต้อง
- [ ] Design tokens (Tailwind prose) apply กับ richText output ถูกต้อง
- [ ] Animation classes apply ที่ wrapper elements ถูกต้อง

**ถ้าผ่านทุกข้อ → Content Migration สำเร็จ!** ต่อด้วย Workflow A Step 3 (พัฒนา Components + Pages)

**รอ user approve ก่อนดำเนินต่อ**

---

## Lexical RichText Rendering in Frontend

Payload เก็บ richText เป็น **Lexical JSON** — ต้อง serialize เป็น HTML ก่อนแสดงใน Next.js

### วิธีที่ 1: serializeLexical (แนะนำ)

```tsx
// src/lib/payload-helpers.ts
import { serializeLexical } from '@payloadcms/richtext-lexical'
import { getPayload } from 'payload'
import config from '@/payload.config'

export async function getPost(slug: string) {
  const payload = await getPayload({ config })
  const { docs } = await payload.find({
    collection: 'posts',
    where: { slug: { equals: slug } },
    depth: 2,
  })
  return docs[0] ?? null
}

export async function getPage(slug: string) {
  const payload = await getPayload({ config })
  const { docs } = await payload.find({
    collection: 'pages',
    where: { slug: { equals: slug } },
    depth: 1,
  })
  return docs[0] ?? null
}
```

```tsx
// src/app/(frontend)/posts/[slug]/page.tsx
import { getPost } from '@/lib/payload-helpers'
import { RichText } from '@payloadcms/richtext-lexical'

export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)
  if (!post) return <div>Not found</div>

  return (
    <article>
      <h1>{post.title}</h1>
      {post.featuredImage && (
        <img src={post.featuredImage.url} alt={post.featuredImage.alt} />
      )}
      {post.content && (
        <RichText
          data={post.content}
          className="prose max-w-none"
        />
      )}
    </article>
  )
}
```

### วิธีที่ 2: serializeLexical (custom HTML)

ถ้าต้องการ control HTML output มากกว่า:

```tsx
import { serializeLexical } from '@payloadcms/richtext-lexical'

const html = serializeLexical({ data: post.content })

return (
  <div
    className="prose max-w-none"
    dangerouslySetInnerHTML={{ __html: html }}
  />
)
```

### RichText CSS (Tailwind + Prose)

```bash
pnpm add @tailwindcss/typography
```

```ts
// tailwind.config.ts
plugins: [require('@tailwindcss/typography')],
```

```tsx
// ใช้ className="prose" กับ RichText หรือ dangerouslySetInnerHTML
<RichText data={post.content} className="prose prose-lg max-w-none" />
```

### Payload Config: เปิดใช้ Lexical Editor

ตรวจสอบว่า `payload.config.ts` มี `lexicalEditor`:

```ts
import { lexicalEditor } from '@payloadcms/richtext-lexical'

export default buildConfig({
  editor: lexicalEditor(),
  // ...
})
```

### กรณีมี Block Types ใน richText

ถ้า collection ใช้ `blocks` field (ไม่ใช่ `richText`):

```tsx
// Blocks ต้อง render ด้วย Payload Block renderer
import { BlockRenderer } from '@payloadcms/richtext-lexical'

<BlockRenderer
  blocks={post.layout}  // array of blocks
  duck
/>
```

---

## SEO Analyzer Plugin Integration

ติดตั้ง `@consilioweb/seo-analyzer` เพื่อให้ editor เห็น SEO score ใน `/admin`

### ติดตั้ง

```bash
cd /path/to/project
pnpm add @consilioweb/seo-analyzer
```

### เพิ่มใน payload.config.ts

```ts
import { seoAnalyzerPlugin } from '@consilioweb/seo-analyzer'

export default buildConfig({
  plugins: [
    seoAnalyzerPlugin({
      collections: ['pages', 'posts'],
      locale: 'en',          // 'fr' (default) | 'en'
      siteName: 'My Website',
    }),
  ],
})
```

### Features ที่ได้ทันที

| สิ่งที่ได้ | รายละเอียด |
|-----------|-------------|
| **SEO Dashboard** | `/admin/seo` — score table ทุกหน้า |
| **Sitemap Audit** | `/admin/sitemap-audit` — orphan pages, broken links |
| **Redirect Manager** | `/admin/redirects` — 301/302 redirects |
| **SeoAnalyzer sidebar** | ใน editor ของทุก collection ที่ตั้งไว้ |
| **Meta fields** | `meta.title`, `meta.description`, `meta.image` auto-created |
| **Auto-redirect** | เปลี่ยน slug แล้วสร้าง redirect ให้อัตโนมัติ |
| **50+ SEO checks** | title, description, headings, content, images, links, readability |

### ถ้ามี `@payloadcms/plugin-seo` อยู่แล้ว

`seoAnalyzerPlugin` จะ skip auto-creation ของ meta fields โดยอัตโนมัติ — ทั้งสอง plugin อยู่ร่วมกันได้

---

## Content Seeding จาก Hardcode

เมื่อ frontend พร้อมแสดง content แล้ว ต้องย้าย hardcode content จาก pages เข้า Payload

### วิธีที่ 1: Seed via Payload Local API

```ts
// src/scripts/seed-content.ts
import { getPayload } from 'payload'
import config from '../payload.config'

const payload = await getPayload({ config })

// Seed pages
const pages = [
  { title: 'Home', slug: 'home', content: { root: { children: [] } }, status: 'published' },
  { title: 'About Us', slug: 'about', content: { root: { children: [] } }, status: 'published' },
]

for (const page of pages) {
  await payload.create({
    collection: 'pages',
    data: page,
  })
}

// Seed posts
const posts = [
  { title: 'Getting Started', slug: 'getting-started', content: { root: { children: [] } }, status: 'published' },
]

for (const post of posts) {
  await payload.create({
    collection: 'posts',
    data: post,
  })
}

console.log('Seeding complete!')
```

```bash
npx tsx src/scripts/seed-content.ts
```

### วิธีที่ 2: Hardcode Content → Payload ผ่าน Migration Script

ถ้ามี content ใน React components อยู่แล้ว:

```ts
// src/scripts/migrate-from-hardcode.ts
// ดึง content จาก hardcode objects แล้วสร้าง Payload documents
const hardcodedContent = [
  { title: 'Home', slug: 'home', body: '<p>Welcome to our site</p>' },
]

for (const item of hardcodedContent) {
  // แปลง HTML string → Lexical JSON
  const lexicalContent = htmlToLexical(item.body)

  await payload.create({
    collection: 'pages',
    data: {
      title: item.title,
      slug: item.slug,
      content: lexicalContent,
      status: 'published',
    },
  })
}
```

---

## Templates

### Next.js Payload Starter (`templates/nextjs-payload-starter/`)

Base template ที่รวมทุกอย่างพร้อม ใช้สร้างเว็บไซต์ใหม่ได้เลย (PostgreSQL):

```
src/
├── app/
│   ├── (payload)/              # Payload admin + API
│   │   ├── admin/[[...segments]]/
│   │   ├── api/[[...slug]]/
│   │   ├── api/graphql/
│   │   ├── api/graphql-playground/
│   │   ├── custom.scss
│   │   └── layout.tsx
│   └── (frontend)/             # Frontend pages
│       ├── layout.tsx
│       ├── page.tsx
│       ├── globals.css
│       └── posts/[[...slug]]/
├── collections/
│   ├── Users.ts               # User auth
│   ├── Media.ts                # Media uploads
│   └── Posts.ts                # Blog posts
├── payload.config.ts
└── index.ts
```

**Collections ที่มี:**
- `Users` - auth, email-based
- `Media` - image uploads
- `Posts` - title, slug, richText content, featuredImage, status

### portal-mini-store-template (MongoDB)

Template สำหรับ mini store ที่มี cart, checkout, orders ในตัว (MongoDB):

```
git clone https://github.com/dyad-sh/portal-mini-store-template.git
```

**Collections ที่มี:** `Users`, `Media`, `Snacks`, `Orders`
**Adapter:** `@payloadcms/db-mongodb` (MongoDB)
**Components ที่มี:** CartSidebar, SiteHeader, CartButton, AddToCartButton

**เพิ่ม PDPA consent logging:**
```bash
# 1. สร้าง ConsentLogs collection → export default ไม่ใช่ named export!
cp ConsentLogs.ts src/collections/

# 2. สร้าง API endpoint
mkdir -p src/app/api/consent

# 3. เพิ่ม collection ใน payload.config.ts
**⚠️ สำคัญมาก: portal-mini-store-template มีปัญหา**

Template นี้เมื่อ clone มาจริงๆ คือ **Astro project** (Astro 6 + Tailwind v4 + SQLite/LibSQL) ไม่ใช่ Next.js + Payload + MongoDB ตามที่อธิบายไว้ใน skill นี้!

```bash
# สิ่งที่ได้จริงๆ คือ Astro project:
# - astro.config.mjs
# - src/content.config.ts
# - @astrojs/db (ไม่ใช่ @payloadcms/db-mongodb)
# - better-sqlite3 / libsql (ไม่ใช่ MongoDB)
```

**ทางแก้:** ถ้าต้องการ Next.js + Payload + MongoDB ต้องสร้างจาก template อื่น หรือใช้ website-creator อ้างอิง pattern จาก github.com/payloadcms/payload/tree/main/templates/with-postgres โดยเปลี่ยน db adapter เป็น mongooseAdapter

---

### portal-mini-store-template (MongoDB) — ข้อมูลเดิม (พบว่าไม่ตรงความจริง)

**เปิดเข้าไปใน project:**
cd portal-mini-store-template

**⚠️ Pitfalls สำคัญมาก:**

1. **ต้องใช้ `mongooseAdapter` ไม่ใช่ `mongodbAdapter`**
   ```ts
   // ✅ ถูกต้อง
   import { mongooseAdapter } from '@payloadcms/db-mongodb'
   db: mongooseAdapter({ url: process.env.MONGODB_URL })

   // ❌ ผิด — ไม่มี export ชื่อนี้
   import { mongodbAdapter } from '@payloadcms/db-mongodb'
   ```

2. **ConsentLogs ต้องใช้ `export default`**
   ```ts
   // ✅ ถูกต้อง — default export
   export default buildConfig({ collections: [ConsentLogs] })

   // ❌ ผิด — named export
   export const ConsentLogs = { ... }
   ```

3. **Template มีปัญหา:** docker-compose ใช้ MongoDB แต่ payload.config.ts เดิมใช้ Vercel Postgres
   → ต้องแก้ payload.config.ts ให้ใช้ `mongooseAdapter` และเปลี่ยน package.json

4. **Dev mode cross-origin warning:** เมื่อเข้าผ่าน IP จะมี warning `allowedDevOrigins`
   → ไม่ต้องแก้ มันคือ dev warning ไม่ใช่ error

**docker-compose สำหรับ MongoDB:**
```bash
cd portal-mini-store-template
docker compose up -d
```

**เข้าถึงจาก external IP (เช่น 110.164.146.185):**

1. เพิ่ม environment ใน docker-compose.yml:
```yaml
payload:
  environment:
    - NEXT_PUBLIC_SERVER_URL=http://110.164.146.185:3000
    - MONGODB_URL=mongodb://mongo:27017/portal-mini-store
```

2. เพิ่ม allowedDevOrigins ใน next.config.ts:
```ts
const nextConfig: NextConfig = {
  allowedDevOrigins: ['110.164.146.185', '110.164.146.185:3000'],
  // ...
}
```

**วิธีสร้าง admin user ผ่าน MongoDB:**

Users ที่ register ผ่านเว็บจะมี role เป็น 'user' ไม่สามารถเข้า admin ได้ ต้องเปลี่ยน role ผ่าน MongoDB:

```bash
# เข้า MongoDB container
docker exec -it <mongo-container-name> mongosh

# เปลี่ยน role เป็น admin
use("portal-mini-store");
db.users.updateOne(
  { email: "your-email@example.com" },
  { $set: { role: "admin" } }
);

# clear sessions (force re-login)
db.users.updateOne(
  { email: "your-email@example.com" },
  { $set: { sessions: [] } }
);
```

---

## Docker

### Development

```bash
docker compose up -d
```

Services:
- `payload` — Next.js app (port 3000)
- `postgres` — PostgreSQL (port 5432)

### Production (Multi-stage Dockerfile)

```dockerfile
FROM node:22-alpine AS deps
RUN corepack enable && corepack prepare pnpm@9.0.0 --activate
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
RUN pnpm install --frozen-lockfile

FROM deps AS builder
COPY . .
RUN pnpm build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
RUN adduser --system --uid 1001 nextjs
RUN mkdir .next && chown nextjs:nodejs .next
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
```

**สำคัญ:** ต้องมี `output: 'standalone'` ใน next.config.ts

---

## Sub-skill Routing

| ขั้นตอน | Sub-skill | รายละเอียด |
|---------|-----------|-------------|
| **ตลอด workflow** | `spec-driven-development` | เขียน spec ก่อน implement ทุกครั้ง |
| Design Framework | `ckm:design` | Brand, tokens, design system |
| Design Framework | `frontend-ui-engineering` | UI component architecture, patterns |
| UI Components | `ckm:ui-styling` | shadcn/ui, Tailwind CSS |
| UI/UX Design | `ui-ux-pro-max` | Wireframes, mockups, 50+ design patterns |
| Brand Identity | `brand` | Logo, colors, typography, voice |
| Banner/Hero Images | `banner-design` | Social, ads, web heroes |
| Develop Components + Pages | `api-and-interface-design` | API design, component interfaces |
| Before QA | `code-review-and-quality` | Full multi-axis code review |
| Before QA | `performance-optimization` | Performance audit + fixes |
| QA Testing | `browser-testing-with-devtools` | Real browser testing (รวมเข้ากับ dogfood) |
| QA Testing | `dogfood` | Exploratory QA of web app |
| SEO Planning | `seo-context` | Per-project SEO context |
| SEO Audit | `seo-analyzers` | Thai language SEO analysis |
| GEO (AI Search) | `seo-geo` | AI Overviews, llms.txt, crawler |
| Multi-channel Content | `seo-multi-channel` | Facebook, LinkedIn, blog content |
| Deploy | `easypanel-deploy` | Easypanel hosting |
| Payload CMS | `payload` | Collections, fields, hooks, access control, queries, plugins, Lexical editor |
| SEO Plugin | `@consilioweb/seo-analyzer` | 50+ SEO checks, admin dashboard, redirect manager, sitemap audit |
| Design + Payload Integration | `payload-lexical-integration` | แยก design layer กับ content layer, integrate RichText กับ design components |

---

### Issue 1: Frontend routes 404 but / works

**อาการ:** `/about-us`, `/contact-us`, `/faq`, `/portfolio` เป็น 404 แต่ `/` ทำงานปกติ

**สาเหตุ:** ไม่มี root `src/app/layout.tsx` — ทำให้ `(payload)` route group ที่มี catch-all `[[...slug]]` route ขัดกับ `(frontend)` group สำหรับ nested routes

**ทางแก้:** สร้าง root layout:
```tsx
// src/app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return children
}
```

### Issue 2: Title field structure — seoTitle vs title

**สิ่งที่พบ:** Astro templates เก็บ full title ไว้ที่ metadata (เช่น `"AI Automation | MoreminiMore"`) แต่ h1 ใช้ short title (เช่น `"AI Automation"`)

**ทางแก้:** Payload `Pages` collection ควรมี 2 fields:
```ts
{ name: 'title', type: 'text', required: true },        // short: "AI Automation" → h1
{ name: 'seoTitle', type: 'text' },                     // full: "AI Automation | MoreminiMore" → <title>
```

Seed script ต้องแยก: ดึง full title จาก Astro → ใส่ `seoTitle`, สร้าง short title จาก slug → ใส่ `title`

### Issue 3: sitemap.xml and robots.txt routes must export GET

**รูปแบบผิด:**
```ts
// ❌ ไม่ทำงาน
export default function sitemap(): MetadataRoute.Sitemap { ... }
```

**รูปแบบถูกต้อง:**
```ts
// ✅
export async function GET(): Promise<MetadataRoute.Sitemap> { ... }
```

ทั้ง `sitemap.xml/route.ts` และ `robots.txt/route.ts` ต้องใช้ named export `GET`

### Issue 4: MongoDB seed script — mongodb package path

**ปัญหา:** `require('mongodb')` ใน `.cjs` file จะหาไม่เจอถ้ารันจาก `/tmp` แทน project root

**ทางแก้:** รัน seed script จาก project root ที่มี `node_modules/mongodb`:
```bash
node seed-mongo.cjs  # ไม่ใช่ node /tmp/seed-mongo.cjs
```

---

## Troubleshooting

### Build fails: Could not find a declaration file for module 'nodemailer'

**Error:** `Could not find a declaration file for module 'nodemailer'`

**ทางแก้:** สร้าง declaration file:

```bash
mkdir -p src/types
echo "declare module 'nodemailer'" > src/types/nodemailer.d.ts
```

### Dev server serves wrong content (old template)

**อาการ:** Dev server แสดง content ของ template เก่า แม้ว่าโค้ดปัจจุบันถูกต้องแล้ว

**สาเหตุ:** มี Next.js dev server หลายตัวทำงานพร้าว (port conflict)

**ทางแก้:**
```bash
# Kill ทุก dev server ก่อน
pkill -f "next dev"

# รอให้ port ว่าง
sleep 2

# รันใหม่
pnpm dev
```

ตรวจสอบว่าไม่มี server เก่าทำงานอยู่:
```bash
ps aux | grep "next dev" | grep -v grep
```

### Terminal cwd resets to hermes-agent directory (Next.js dev server starts in wrong place)

**อาการ:** `bun run dev` รันจาก working directory ของ hermes-agent แทน project directory ทำให้ server ทำงานผิดที่ หรือ 404 ทุก route

**สาเหตุ:** Hermes agent terminal มี cwd ที่อยู่ไม่ตรงกับ project ที่ต้องการ — `pwd` เป็น `/home/kunthawat/.hermes/hermes-agent`

**วิธีแก้ — ต้องระบุ workdir ทุกครั้ง:**
```bash
# วิธีที่ 1: ใช้ script file
cat > /path/to/project/start-dev.sh << 'EOF'
#!/bin/bash
cd /home/kunthawat/moreminimore-next
kill $(lsof -t -i:3000) 2>/dev/null || true
sleep 2
nohup bun run dev --port 3000 > /tmp/next-dev.log 2>&1 &
echo "Server PID: $!"
EOF
bash /path/to/project/start-dev.sh

# วิธีที่ 2: ใช้ workdir parameter ทุกครั้ง
terminal(command="bun run dev --port 3000", workdir="/home/kunthawat/moreminimore-next")

# ตรวจสอบ port ว่างก่อน start
ss -tlnp | grep 3000
# ถ้ามี process เก่า:
kill $(ss -tlnp | grep 3000 | grep -o 'pid=[0-9]*' | cut -d= -f2)
```

### Turbopack rebuild causes 500 on first request after restart

**อาการ:** หลัง restart dev server ทุก route คืน 500 แต่หลังจากนั้นทำงานปกติ

**สาเหตุ:** Turbopack ต้อง rebuild ทุก chunk ใหม่หลัง restart ทำให้ request แรก fail

**ทางแก้:**
```bash
# 1. Restart แล้วรอ warmup ก่อน test
bash start-dev.sh && sleep 20 && curl -sI http://localhost:3000/ | head -3

# 2. ถ้ายัง 500 หลัง warmup → restart อีกครั้ง (Turbopack ต้อง 2-3 cycles)
kill $(ss -tlnp | grep 3000 | grep -o 'pid=[0-9]*' | cut -d= -f2)
sleep 3
bash start-dev.sh && sleep 20

# 3. ถ้ายังมีปัญหา → ลบ .next cache
kill $(ss -tlnp | grep 3000 | grep -o 'pid=[0-9]*' | cut -d= -f2)
rm -rf /path/to/project/.next
bash start-dev.sh && sleep 25
```

### Hero images in src/app/(frontend)/assets/ return 404

**อาการ:** ไฟล์ภาพอยู่ที่ `src/app/(frontend)/assets/heroes/` แต่ `next/image` src `/assets/heroes/xxx.png` คืน 404

**สาเหตุ:** ไฟล์ใน `src/app/` directory ไม่ได้ถูก serve เป็น static files — เฉพาะ `public/` เท่านั้นที่ Next.js serve เป็น static

**ทางแก้:**
```bash
# ย้ายจาก src/app ไป public/
mv src/app/\(frontend\)/assets/heroes/*.png public/assets/heroes/
# แก้ next.config.ts ให้ allow localPatterns
# localPatterns: [{ pathname: '/api/media/file/**' }, { pathname: '/assets/heroes/**' }]
```

### next/image "does not match images.localPatterns" error

**อาการ:** `next/image` src `/assets/heroes/xxx.png` คืน 500 พร้อม error "does not match `images.localPatterns` configured in your `next.config.js`"

**ทางแก้:** เพิ่ม pattern ใน `next.config.ts`:
```ts
images: {
  localPatterns: [
    { pathname: '/api/media/file/**' },
    { pathname: '/assets/heroes/**' },
  ],
},
```

### Payload CMS first request very slow (7-35s) causing sitemap timeout

**อาการ:** `/sitemap.xml` timeout หรือ 500 เพราะ Payload ต้อง warm up

**ทางแก้:** Sitemap route ต้องมี timeout + fallback:
```ts
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 8000)
try {
  const payload = await getPayload({ config })
  // fetch pages/posts
} catch (e) {
  console.warn('Sitemap: Payload unavailable, using static fallback')
} finally {
  clearTimeout(timeout)
}
// ถ้า Payload fail ทั้งหมด → return static sitemap
```

### Syntax error: extra closing parenthesis in Payload queries

**Error:** `Expected ',', got '}'` หรือ `Expected a semicolon`

**สาเหตุ:** มักเกิดจาก `.find({ ... } }))` แทนที่จะเป็น `.find({ ... }))`

**วิธีแก้:** ตรวจสอบว่า `payload.find()`, `payload.create()`, `payload.update()` calls มี closing brackets ถูกต้อง:

```ts
// ✅ ถูกต้อง
const { docs } = await payload.find({
  collection: 'posts',
  where: { slug: { equals: slug } },
})

// ❌ ผิด — extra )
const { docs } = await payload.find({ collection: 'posts', where: { slug: { equals: slug } } }))
```

### Payload admin /admin shows "initializing" forever

- ตรวจสอบว่า `payload-types.ts` ถูก generate แล้ว
- ตรวจสอบ `importMap` ว่าถูก generate แล้ว: `pnpm generate:importmap`
- ตรวจสอบ DATABASE_URL ว่าถูกต้อง
- ดู logs: `tail /tmp/next-dev.log` หรือ terminal output

### Build fails: GRAPHQL_GET doesn't exist

**Error:** `Export GRAPHQL_GET doesn't exist in target module`

**ทางแก้:** ใช้ `GRAPHQL_PLAYGROUND_GET` แทน

```ts
// ✅ ถูกต้อง
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
// ❌ ผิด — ไม่มี export นี้
import { GRAPHQL_GET } from '@payloadcms/next/routes'
```

### Build fails: relation does not exist / table does not exist

**Error:** `error: relation "posts" does not exist` หรือ `42P01`

**สาเหตุ:** Next.js พยายาม pre-render pages ที่เรียก Payload ตอน build แต่ tables ยังไม่มี

**ทางแก้ - ทั้ง 2 อย่าง:**

```tsx
// 1. บอก Next.js ว่าหน้านี้ dynamic ไม่ต้อง pre-render
export const dynamic = 'force-dynamic'

// 2. wrap Payload calls ใน try/catch
export default async function HomePage() {
  let posts: any[] = []
  try {
    const payload = await getPayload({ config })
    const { docs } = await payload.find({ collection: 'posts', limit: 10 })
    posts = docs
  } catch (e) {
    console.warn('Could not fetch posts:', e)
  }
  // ... render
}
```

### Dev mode ช้ามาก (7-35 วินาที) แม้ warm up แล้ว

**สาเหตุ:** Payload ต้อง pull schema from database ทุก request ใน dev mode

**ทางแก้:** ใช้ **production standalone mode** แทน

```bash
# Build
pnpm build

# Run production (เร็วมาก ~400ms vs 7-35s)
node .next/standalone/server.js
```

**ต้องมี `output: 'standalone'` ใน next.config.ts**

### PostgreSQL connection fails / wrong credentials

ถ้าใช้ Docker: ดู credentials จริงจาก container:

```bash
docker exec <postgres-container> env | grep POSTGRES
```

**ตัวอย่าง credentials ที่พบ:**
- user: `payload` (ไม่ใช่ `postgres`)
- password: ดูจาก `POSTGRES_PASSWORD` env
- database name: ต้องตรงกับ `POSTGRES_DB` — เช่น `payload`
- port: Docker compose อาจ map ไป port อื่น (เช่น 5555 แทน 5432)

**หา port ที่ PostgreSQL รันจริง:**
```bash
docker ps --format '{{.Names}} -> {{.Ports}}'  # ดู port mappings
ss -tlnp | grep 5432  # ดูว่า port 5432 ถูกใช้หรือเปล่า
```

**Database name ต้องตรงกับ `POSTGRES_DB`:**
```bash
# ถ้า docker-compose ตั้ง POSTGRES_DB=payload
# DATABASE_URL ต้องเป็น .../payload ไม่ใช่ .../postgres
DATABASE_URL=postgresql://payload:payloadpass@localhost:5555/payload
```

### Tables ไม่ถูกสร้าง / migrate ทำงานแล้วแต่ tables ไม่มี

**สาเหตุ:** Payload ไม่ได้สร้าง tables อัตโนมัติเมื่อรัน `migrate` command — ต้องให้ Payload runtime สร้าง tables

**ทางแก้ - วิธีที่แน่นอน:**

```bash
# 1. ลบ .next cache
rm -rf .next

# 2. รัน dev server (Payload จะสร้าง tables เองเมื่อมี request)
pnpm dev

# 3. เปิด browser ไปที่ http://localhost:3000/admin
#    รอให้ schema pull เสร็จ (10-30 วินาที)

# 4. ตรวจสอบว่า tables ถูกสร้างแล้ว
docker exec <container> psql -U <user> -d <dbname> -c "\dt"
```

**หรือใช้ `migrate:fresh` ก่อน dev แต่ต้องมี migration files:**
```bash
mkdir -p src/migrations
pnpm payload migrate:fresh --yes
# ถ้ามี interactive prompt ให้พิมพ์ 'y'
```

### Migration interactive prompt causing crash

- รัน `pnpm payload migrate --yes` ก่อน `pnpm dev`
- ถ้า crash แล้ว refresh browser — มันจะ warm up ใหม่อีกครั้ง
- ถ้า `migrate:fresh` ค้าง → ลองแค่ `pnpm dev` แล้วไป `/admin` ใน browser แทน

### First request extremely slow (7-30s) then fast

- **ปกติ** — Payload ต้อง pull schema จาก DB ก่อน request แรกทุกครั้ง
- รอ warm up เสร็จก่อนทดสอบต่อ
- ดู logs ว่า server พร้อมหรือยัง: `tail /tmp/next-dev.log`

---

### Common Pitfalls Discovered from moreminimore-redesign Project

**1. Tailwind not pre-installed**
- Template ที่มี `globals.css` มากับ `@tailwind` directives อาจไม่มี Tailwind ติดตั้งใน package.json
- ต้อง `npm install -D tailwindcss@3 postcss autoprefixer --legacy-peer-deps` เสมอ
- หลังติดตั้ง ต้องลบ `.next` cache และ restart dev server

**2. postcss.config — ESM vs CommonJS**
- Project เป็น ESM (`"type": "module"` ใน package.json)
- ถ้าสร้าง `postcss.config.js` ด้วย `module.exports` → Error: `module is not defined in ES module scope`
- ต้องใช้ `postcss.config.cjs` เท่านั้น หรือใช้ ESM syntax (`export default`)

**3. Port conflict with root Next.js processes**
- `kill` ธรรมดาลบ root process ไม่ได้
- ต้องใช้: `fuser -k <port>/tcp`
- Dev server อาจติด port แม้ process ดูตายแล้ว — ตรวจสอบด้วย `fuser <port>/tcp`

**4. Content seeding via Payload CLI ช้ามาก**
- ถ้าต้อง seed content หลาย items ใช้ MongoDB CLI โดยตรงแทน Payload SDK
```bash
docker cp script.js <mongo-container>:/tmp/script.js
docker exec <mongo-container> mongosh <url> /tmp/script.js
```

**5. ตรวจสอบ dev server ก่อน assume ว่ามันทำงาน**
- ทุกครั้งที่ restart: `curl http://localhost:<port>/ | grep tailwind-classes`
- ถ้า CSS ไม่ทำงานจะเห็น `class=\"...\"` ที่ไม่มี styling ตามมา

---

## Troubleshooting & Common Issues

### 1. pnpm "specs is not iterable" error

**ปัญหา:** pnpm 10.x มี bug กับ some package configurations
**แก้:** ใช้ npm แทน

```bash
npm install --legacy-peer-deps
```

### 2. Payload named vs default import error

**ปัญหา:** `Module has no exported member 'Portfolio'. Did you mean to use 'import Portfolio from'`

**สาเหตุ:** Payload collections ต้องใช้ `export default` ไม่ใช่ `export const`

```ts
// ✅ ถูก - collection file ใช้ default export
const Portfolio: CollectionConfig = { ... }
export default Portfolio

// ❌ ผิด - named export จะมี error ตอน build
export const Portfolio = { ... }
```

### 3. MongoDB connection refused (Docker)

**ปัญหา:** `ECONNREFUSED 127.0.0.1:27017` ใน container

**แก้:** ต้องใช้ container hostname ไม่ใช่ localhost

```bash
# หา MongoDB container name และ network
docker ps --format '{{.Names}} {{.Ports}}'
docker inspect <mongo-container> --format '{{json .NetworkSettings.Networks}}'

# หา IP ของ MongoDB container
docker inspect <mongo-container> --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
```

**docker-compose.yml — ต่อไปยัง MongoDB container ที่มีอยู่แล้ว:**

```yaml
services:
  app:
    build: .
    ports:
      - "3002:3000"
    environment:
      - MONGODB_URL=mongodb://<mongo-container-name>:27017/<dbname>
    networks:
      - <network-name>

networks:
  <network-name>:
    external: true
```

### 4. Port already allocated

**ปัญหา:** `Bind for 0.0.0.0:3001 failed: port is already allocated`

**แก้:** ใช้ port อื่น

```bash
# ดูว่า port ไหนว่าง
ss -tlnp | grep <port>

# ใน docker-compose.yml
ports:
  - "3002:3000"  # host:container
```

### 5. Docker build "public not found"

**ปัญหา:** `"/app/public": not found` ใน multi-stage Dockerfile

**แก้:** ลบบรรทัดนั้นออก — Next.js standalone ไม่ต้องการ public/

```dockerfile
# ลบบรรทัดนี้
# COPY --from=builder --chown=nextjs:nodejs /app/public ./public
```

### 6. Payload admin แสดง "initializing" ตลอด

**สาเหตุ:** ไม่ได้ generate types ก่อน

```bash
npm run generate:types
npm run generate:importmap
npm run dev
```

### 7. TypeScript Build Errors (Payload 3.x)

#### `relationTo: 'snacks'` — Type not assignable

```ts
// ❌ ผิด
relationTo: 'snacks',

// ✅ ถูกต้อง
relationTo: 'snacks' as any,
```

#### `editor: { lexical: {} }` — Property does not exist

```ts
// ❌ ผิด
editor: { lexical: {} },

// ✅ ถูกต้อง
editor: {} as any,
```

#### Access functions — FieldAccess type mismatch

```ts
// ❌ ผิด
admin: ({ req: { user } }) => user?.role === 'admin',

// ✅ ถูกต้อง
admin: (({ req: { user } }) => user?.role === 'admin') as any,
```

#### `changeFrequency` — string not assignable to union type

```ts
// ❌ ผิด
changeFrequency: 'weekly',

// ✅ ถูกต้อง
changeFrequency: 'weekly' as const,
```

#### `payload.auth()` — signature changed in Payload 3.x

```ts
// ❌ ผิด — Payload 3.x ไม่รองรับ collection ที่นี่
const { user } = await payload.auth({ headers })

// ✅ ถูกต้อง — สร้าง request object เอง
import { createLocalReq } from '@/collections/Users'
const { user } = await createLocalReq({ userId, role: 'admin' })
```

#### `adminsOrOwner` doesn't exist → use `adminsOrSelf`

```ts
// ❌ ผิด
access: adminsOrOwner,

// ✅ ถูกต้อง
access: adminsOrSelf,
```

#### Where clause in access function

```ts
// ❌ ผิด — return type ไม่ตรง
update: ({ req: { user } }) => {
  if (user?.role === 'admin') return true
  return { user: { equals: user.id } }
},

// ✅ ถูกต้อง
update: (({ req: { user } }) => {
  if (user?.role === 'admin') return true
  return { user: { equals: user.id } }
}) as any,
```

### 8. `"use client"` + `generateMetadata` in same file → 500 error

**Error:** Page returns HTTP 500 or Next.js compile error.

**Root Cause:** Next.js App Router ไม่อนุญาตให้ export `generateMetadata` หรือ `generateStaticParams` จาก component ที่มี `"use client"` directive — metadata functions ต้องอยู่ใน **server component** เท่านั้น

**Fix:** แยก client-interactive parts (form, stateful UI) ออกเป็นไฟล์ component ต่างหาก:

```tsx
// ❌ ผิด — page.tsx มีทั้ง "use client" และ generateMetadata
"use client";
export async function generateMetadata() { ... }
export default function Page() { ... }

// ✅ ถูกต้อง — page.tsx เป็น server component, แยก form ออกไป
// src/components/ContactForm.tsx
"use client";
export default function ContactForm() { ... }

// src/app/contact/page.tsx
export async function generateMetadata() { ... }
import ContactForm from "@/components/ContactForm";
export default function Page() { return <ContactForm />; }
```

### 9. Payload 3.x — First-Time Admin User Setup

**ปัญหา:** Payload 3.x ไม่มีคำสั่ง `create-user` CLI เหมือน Payload 2.x

**ทางแก้ — สร้าง admin user ผ่าน Browser:**

1. เปิด browser ไปที่ `http://localhost:3002/admin`
2. หน้าแรกจะแสดงฟอร์ม "Create your first user" — Email, New Password, Confirm Password
3. กรอกข้อมูลแล้วกด Create
4. หลังจากนั้น admin UI จะเข้าสู่ Dashboard

### 10. Payload 3.x — API Authentication Flow

Login → รับ JWT token → ใช้ token ใน `Authorization: Bearer *** header`

```bash
# Login
curl -s -X POST http://localhost:3002/api/users/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password": "***"}'
# Response: {"token": "***", "exp": 1776229711, ...}

# Use token for authenticated requests
curl -s -X POST http://localhost:3002/api/products \
  -H 'Authorization: Bearer *** \
  -H 'Content-Type: application/json' \
  -d '{"title":"...","slug":"..."}'
```

### 11. Payload 3.x — Seed Content via API (Python urllib)

**ปัญหา:** `curl` subprocess มีข้อจำกัด argument list length — ไม่สามารถ POST HTML content ขนาดใหญ่ได้

```
OSError: [Errno 7] Argument list too long: 'curl'
```

**ทางแก้ — ใช้ Python urllib:**

```python
import urllib.request, urllib.error, json

TOKEN = "<login-token>"

def api_post(collection, data):
    url = f'http://localhost:3002/api/{collection}'
    body = json.dumps(data, ensure_ascii=False).encode('utf-8')
    req = urllib.request.Request(url, data=body, headers={
        'Authorization': f'Bearer {TOKEN}',
        'Content-Type': 'application/json',
    })
    resp = urllib.request.urlopen(req, timeout=15)
    return json.loads(resp.read())

# Login first
import subprocess
r = subprocess.run(
    ['curl', '-s', '-X', 'POST', 'http://localhost:3002/api/users/login',
     '-H', 'Content-Type: application/json',
     '-d', '{"email":"admin@example.com","password": "***"}'],
    capture_output=True, text=True, timeout=10
)
TOKEN = json.loads(r.stdout)['token']

# Then use urllib for large content
r = api_post('products', {"title": "...", "content": large_html_string})
if 'doc' in r:
    print(f"Success: {r['doc']['id']}")
```

### 12. Next.js Dev Server — Keep Alive in Background

**ปัญหา:** subprocess ที่รัน dev server อาจ crash หรือถูก kill เมื่อ command timeout

**วิธีที่ใช้ได้:**

```python
import subprocess, os

# Kill existing
subprocess.run(['pkill', '-9', '-f', 'next dev'], capture_output=True)

# Start with nohup
proc = subprocess.Popen(
    ['nohup', 'npm', 'run', 'dev'],
    cwd='/path/to/project',
    stdout=open('/tmp/dev.log', 'w'),
    stderr=subprocess.STDOUT,
    preexec_fn=os.setsid  # Create new process group
)

import time; time.sleep(8)

# Verify
result = subprocess.run(['ss', '-tlnp'], capture_output=True, text=True)
ports = [l for l in result.stdout.split('\n') if ':3002' in l and 'LISTEN' in l]
print("Running on 3002:", bool(ports))
```

**ตรวจสอบว่า server รันได้จริง:**

```bash
# ดู port binding — ถ้าเห็น *:3002 LISTEN = รันอยู่
ss -tlnp | grep 3002

# ถ้า port ว่าง = server crash
# ดู log: cat /tmp/dev.log
```

### 13. Dev Server Crashes Silently After Compilation

**สาเหตุ:** Next.js dev server พยายาม start แต่ process อาจ crash หลัง compile เสร็จโดยไม่แสดง error

**วิธีวินิจฉัย:**

```bash
# ดูว่า port ถูก bind หรือยัง (ถ้ายัง = server crash)
ss -tlnp | grep 3000

# ลองรันแบบ foreground เพื่อดู error
cd /path/to/project
pnpm dev
# ดูว่า process exit หลัง "Ready in Xms" หรือเปล่า
```

**ทางแก้:** ถ้า server crash ให้ลบ `.next` cache แล้วลองใหม่:

```bash
rm -rf .next
pnpm dev
```

### 14. White Screen — SSR Error Disguised as Client Hydration Failure

**Symptom:** Admin page or frontend shows white screen. `body{display:none}` style is present.

**KEY INSIGHT:** White screen usually means SSR failed, NOT React hydration broke. The `body{display:none}` is Next.js's FOUC prevention.

**Diagnosis — ALWAYS start with curl (fastest):**

```bash
curl -s http://localhost:3000/admin/create-first-user | grep -o '"statusCode":[0-9]*'
# If 500 → SSR error. Look for "err" object in __NEXT_DATA__
curl -s http://localhost:3000/admin/create-first-user | grep -o '"message":"[^"]*"' | head -3
```

**Root Cause ที่พบบ่อย: `sharp` import missing from `payload.config.ts`**

```
Failed to load external module sharp-20c6a5da84e2135f:
Cannot find package 'sharp-20c6a5da84e2135f'
```

**Fix:**

```bash
cd /path/to/project

# Option A: Remove sharp import (if not actively used)
sed -i "s/import sharp from 'sharp'//" src/payload.config.ts
sed -i "s/, sharp//" src/payload.config.ts

# Option B: Install sharp properly
npm install sharp

# Then clear cache + restart
rm -rf .next
fuser -k 3000/tcp 2>/dev/null || true
sleep 2
npm run dev &
sleep 15
```

### 15. First request extremely slow (7-30s) then fast

- **ปกติ** — Payload ต้อง pull schema จาก DB ก่อน request แรกทุกครั้ง
- รอ warm up เสร็จก่อนทดสอบต่อ
- ดู logs ว่า server พร้อมหรือยัง: `tail /tmp/next-dev.log`

### 16. Build Error Troubleshooting Workflow

```bash
# 1. Build and count errors
pnpm build 2>&1 | grep -c "Type error:"

# 2. Get first error
pnpm build 2>&1 | grep -A5 "Type error:" | head -10

# 3. Fix one at a time, restart build each time
# 4. Check for more errors after each fix
```

---

## Key Principles

1. **Plan-First Approach**
   - แสดงแผนก่อนลงมือเสมอ
   - รอ user approve ก่อน

2. **Next.js Latest**
   - ใช้ Next.js เวอร์ชั่นล่าสุด
   - Payload CMS สำหรับ content management

3. **Thai-First**
   - ภาษาไทยเป็นหลัก
   - Thai fonts (Kanit, Noto Sans Thai)
   - Thai typography CSS
   - Thai structured data (LocalBusiness, Organization)

4. **Preview Before Deploy**
   - Preview ผ่าน local dev server ที่ `0.0.0.0`
   - ให้ user ตรวจสอบก่อน deploy

