2245 lines
76 KiB
Markdown
2245 lines
76 KiB
Markdown
---
|
||
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 (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: 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
|
||
|