76 KiB
name, description, tags, category, related_skills
| name | description | tags | category | related_skills | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| website-creator | สร้างเว็บไซต์เต็มรูปแบบด้วย Next.js + Payload CMS พร้อม Workflow สำหรับเว็บใหม่และ Migration ครอบคลุม Design System, Content Collections, Auth, SEO, PDPA Compliance และ Deploy |
|
software-development |
|
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+revalidatetags
Critical Configuration Rules
1. payload.config.ts (MongoDB — มาตรฐาน)
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:
pnpm add @payloadcms/db-mongodb@3.82.0
รัน MongoDB ด้วย Docker:
docker run -d --name mongo -p 27017:27017 mongo:7
Environment:
MONGODB_URL=mongodb://localhost:27017/my-database
PAYLOAD_SECRET=your-secret-here
2. Required Dependencies
{
"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
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
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
ขั้นตอน:
- ถามว่ามี brand guidelines หรือ reference ไหม
- ถ้าไม่มี → ถาม 2 คำถาม:
- ชอบ dark mode ไหม หรือ light mode อย่างเดียว?
- ชอบ style แบบไหน: minimal, bold, creative?
- ใช้
ckm:designเพื่อสร้าง design system - ใช้
ui-ux-pro-maxเพื่อออกแบบ wireframes - ใช้
ckm:ui-stylingเพื่อสร้าง components
Step 4: Setup Next.js + Payload CMS Project
ใช้ template ที่มีอยู่แล้ว: templates/nextjs-payload-starter/
# 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://:3000
- Payload Admin: http://localhost:3000/admin หรือ http://:3000/admin
ถ้า port 3000 มี process อื่นใช้ หรือ /admin ขึ้น 404:
# 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
ตัวอย่าง:
/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 หรือภาพประกอบ:
วิธีใช้:
# 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:
# 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:
# 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 แบบนี้:
// ❌ สิ่งที่ 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>
ต้องแปลงเป็น:
// ✅ ครอบ 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 ไหน:
// 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
// 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
// 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:
pnpm add @tailwindcss/typography
// tailwind.config.ts
plugins: [require('@tailwindcss/typography')],
ใช้ class prose กับ <RichText> component:
<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 ที่มีอยู่แล้ว:
-
Privacy Policy — ใช้
templates/privacy-policy.md- ภาษากฎหมาย PDPA ครบถ้วน
- แทนที่ placeholders ด้วยข้อมูลจริง
-
Terms of Service — ใช้
templates/terms-of-service.md -
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
ไฟล์ที่ต้องสร้าง:
src/collections/ConsentLogs.ts- Payload collection:
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
src/app/api/consent/route.ts- API endpoint (Next.js App Router):
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 })
}
-
src/components/cookie-banner.tsx- Client component ที่เก็บ localStorage + call API -
เพิ่ม ConsentLogs ใน
payload.config.ts:
import { ConsentLogs } from './collections/ConsentLogs'
// ...
collections: [Users, Media, Snacks, Orders, ConsentLogs],
- ติดตั้ง MongoDB adapter (ถ้าใช้ MongoDB):
pnpm add @payloadcms/db-mongodb@3.49.1
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)
-
Payload SEO Analyzer Plugin (
@consilioweb/seo-analyzer):- ติดตั้งและเพิ่มใน
payload.config.tsplugins array - ทำให้
/adminมี SEO dashboard, score tracking, redirect manager - ดู: SEO Analyzer Plugin Integration ด้านล่าง
- ติดตั้งและเพิ่มใน
-
Meta tags ทุกหน้า — ใช้ meta fields จาก Payload collection หรือ static
-
sitemap.xml — Next.js dynamic route หรือ
@payloadcms/plugin-seo -
robots.txt
-
Open Graph images — ใช้
picture-itสร้าง OG image template ที่ consistent -
JSON-LD structured data
-
Thai language optimization
Step 9: Preview + QA
pnpm dev --host 0.0.0.0
ก่อน QA — เรียก skills ตามลำดับ:
code-review-and-quality— Full multi-axis code review ก่อน deliveryperformance-optimization— Performance audit + fixesbrowser-testing-with-devtools— ทดสอบใน real browser
จากนั้นเรียก dogfood สำหรับ exploratory QA:
/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
/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 ที่มี
# ดูโครงสร้างไฟล์ (ไม่เอา 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 เดิมที่ต้องย้าย:
# ดูว่ามีไฟล์ 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 → สร้างตามนี้:
src/collections/Posts.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
src/collections/Pages.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
- เพิ่มใน
payload.config.ts:
import { Posts } from './collections/Posts'
import { Pages } from './collections/Pages'
// ...
collections: [Users, Media, Posts, Pages],
- Generate types + restart dev server หลังเพิ่ม collection:
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 ย้ายทีละหลายรายการ:
// 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:
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 (แนะนำ)
// 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
}
// 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 มากกว่า:
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)
pnpm add @tailwindcss/typography
// tailwind.config.ts
plugins: [require('@tailwindcss/typography')],
// ใช้ className="prose" กับ RichText หรือ dangerouslySetInnerHTML
<RichText data={post.content} className="prose prose-lg max-w-none" />
Payload Config: เปิดใช้ Lexical Editor
ตรวจสอบว่า payload.config.ts มี lexicalEditor:
import { lexicalEditor } from '@payloadcms/richtext-lexical'
export default buildConfig({
editor: lexicalEditor(),
// ...
})
กรณีมี Block Types ใน richText
ถ้า collection ใช้ blocks field (ไม่ใช่ richText):
// 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
ติดตั้ง
cd /path/to/project
pnpm add @consilioweb/seo-analyzer
เพิ่มใน payload.config.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
// 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!')
npx tsx src/scripts/seed-content.ts
วิธีที่ 2: Hardcode Content → Payload ผ่าน Migration Script
ถ้ามี content ใน React components อยู่แล้ว:
// 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-basedMedia- image uploadsPosts- 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:
# 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'
-
ConsentLogs ต้องใช้
export default// ✅ ถูกต้อง — default export export default buildConfig({ collections: [ConsentLogs] }) // ❌ ผิด — named export export const ConsentLogs = { ... } -
Template มีปัญหา: docker-compose ใช้ MongoDB แต่ payload.config.ts เดิมใช้ Vercel Postgres → ต้องแก้ payload.config.ts ให้ใช้
mongooseAdapterและเปลี่ยน package.json -
Dev mode cross-origin warning: เมื่อเข้าผ่าน IP จะมี warning
allowedDevOrigins→ ไม่ต้องแก้ มันคือ dev warning ไม่ใช่ error
docker-compose สำหรับ MongoDB:
cd portal-mini-store-template
docker compose up -d
เข้าถึงจาก external IP (เช่น 110.164.146.185):
- เพิ่ม environment ใน docker-compose.yml:
payload:
environment:
- NEXT_PUBLIC_SERVER_URL=http://110.164.146.185:3000
- MONGODB_URL=mongodb://mongo:27017/portal-mini-store
- เพิ่ม allowedDevOrigins ใน next.config.ts:
const nextConfig: NextConfig = {
allowedDevOrigins: ['110.164.146.185', '110.164.146.185:3000'],
// ...
}
วิธีสร้าง admin user ผ่าน MongoDB:
Users ที่ register ผ่านเว็บจะมี role เป็น 'user' ไม่สามารถเข้า admin ได้ ต้องเปลี่ยน role ผ่าน MongoDB:
# เข้า 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
docker compose up -d
Services:
payload— Next.js app (port 3000)postgres— PostgreSQL (port 5432)
Production (Multi-stage 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:
// 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
รูปแบบผิด:
// ❌ ไม่ทำงาน
export default function sitemap(): MetadataRoute.Sitemap { ... }
รูปแบบถูกต้อง:
// ✅
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:
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:
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)
ทางแก้:
# Kill ทุก dev server ก่อน
pkill -f "next dev"
# รอให้ port ว่าง
sleep 2
# รันใหม่
pnpm dev
ตรวจสอบว่าไม่มี server เก่าทำงานอยู่:
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 ทุกครั้ง:
# วิธีที่ 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
ทางแก้:
# 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
ทางแก้:
# ย้ายจาก 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:
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:
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 ถูกต้อง:
// ✅ ถูกต้อง
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 แทน
// ✅ ถูกต้อง
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 อย่าง:
// 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 แทน
# 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:
docker exec <postgres-container> env | grep POSTGRES
ตัวอย่าง credentials ที่พบ:
- user:
payload(ไม่ใช่postgres) - password: ดูจาก
POSTGRES_PASSWORDenv - database name: ต้องตรงกับ
POSTGRES_DB— เช่นpayload - port: Docker compose อาจ map ไป port อื่น (เช่น 5555 แทน 5432)
หา port ที่ PostgreSQL รันจริง:
docker ps --format '{{.Names}} -> {{.Ports}}' # ดู port mappings
ss -tlnp | grep 5432 # ดูว่า port 5432 ถูกใช้หรือเปล่า
Database name ต้องตรงกับ POSTGRES_DB:
# ถ้า 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
ทางแก้ - วิธีที่แน่นอน:
# 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:
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มากับ@tailwinddirectives อาจไม่มี Tailwind ติดตั้งใน package.json - ต้อง
npm install -D tailwindcss@3 postcss autoprefixer --legacy-peer-depsเสมอ - หลังติดตั้ง ต้องลบ
.nextcache และ 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
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 แทน
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
// ✅ ถูก - 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
# หา 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 ที่มีอยู่แล้ว:
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 อื่น
# ดูว่า 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/
# ลบบรรทัดนี้
# COPY --from=builder --chown=nextjs:nodejs /app/public ./public
6. Payload admin แสดง "initializing" ตลอด
สาเหตุ: ไม่ได้ generate types ก่อน
npm run generate:types
npm run generate:importmap
npm run dev
7. TypeScript Build Errors (Payload 3.x)
relationTo: 'snacks' — Type not assignable
// ❌ ผิด
relationTo: 'snacks',
// ✅ ถูกต้อง
relationTo: 'snacks' as any,
editor: { lexical: {} } — Property does not exist
// ❌ ผิด
editor: { lexical: {} },
// ✅ ถูกต้อง
editor: {} as any,
Access functions — FieldAccess type mismatch
// ❌ ผิด
admin: ({ req: { user } }) => user?.role === 'admin',
// ✅ ถูกต้อง
admin: (({ req: { user } }) => user?.role === 'admin') as any,
changeFrequency — string not assignable to union type
// ❌ ผิด
changeFrequency: 'weekly',
// ✅ ถูกต้อง
changeFrequency: 'weekly' as const,
payload.auth() — signature changed in Payload 3.x
// ❌ ผิด — 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
// ❌ ผิด
access: adminsOrOwner,
// ✅ ถูกต้อง
access: adminsOrSelf,
Where clause in access function
// ❌ ผิด — 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 ต่างหาก:
// ❌ ผิด — 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:
- เปิด browser ไปที่
http://localhost:3002/admin - หน้าแรกจะแสดงฟอร์ม "Create your first user" — Email, New Password, Confirm Password
- กรอกข้อมูลแล้วกด Create
- หลังจากนั้น admin UI จะเข้าสู่ Dashboard
10. Payload 3.x — API Authentication Flow
Login → รับ JWT token → ใช้ token ใน Authorization: Bearer *** header
# 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:
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
วิธีที่ใช้ได้:
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 รันได้จริง:
# ดู 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
วิธีวินิจฉัย:
# ดูว่า 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 แล้วลองใหม่:
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):
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:
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
# 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
-
Plan-First Approach
- แสดงแผนก่อนลงมือเสมอ
- รอ user approve ก่อน
-
Next.js Latest
- ใช้ Next.js เวอร์ชั่นล่าสุด
- Payload CMS สำหรับ content management
-
Thai-First
- ภาษาไทยเป็นหลัก
- Thai fonts (Kanit, Noto Sans Thai)
- Thai typography CSS
- Thai structured data (LocalBusiness, Organization)
-
Preview Before Deploy
- Preview ผ่าน local dev server ที่
0.0.0.0 - ให้ user ตรวจสอบก่อน deploy
- Preview ผ่าน local dev server ที่