Files
opencode-skill/skills/website-creator/SKILL.md
Kunthawat Greethong b26c8199a5 Update skills: add website-creator, mql-developer, ecommerce-astro
Changes:
- Add FAL_KEY and GEMINI_API_KEY to .env.example
- Update picture-it to use ~/.config/opencode/.env (unified creds)
- Remove shodh-memory skill (no longer used)
- Remove alphaear-* skills (deprecated)
- Remove thai-frontend-dev skill (replaced by website-creator)
- Remove theme-factory skill
- Add mql-developer skill (MQL5 trading)
- Add ecommerce-astro skill (Astro e-commerce)
- Add website-creator skill (Next.js + Payload CMS)
- Update install script for new skills
2026-04-16 17:40:27 +07:00

78 KiB
Raw Blame History

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
nextjs
website
website-development
website-creation
migration
tailwindcss
thai
pdpa
seo
payload-cms
isr
image-generation
picture-it
software-development
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 — มาตรฐาน)

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

ขั้นตอน:

  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/

# 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:

ถ้า 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 ที่มีอยู่แล้ว:

  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:
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
  1. 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 })
}
  1. src/components/cookie-banner.tsx - Client component ที่เก็บ localStorage + call API

  2. เพิ่ม ConsentLogs ใน payload.config.ts:

import { ConsentLogs } from './collections/ConsentLogs'
// ...
collections: [Users, Media, Snacks, Orders, ConsentLogs],
  1. ติดตั้ง 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)

  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

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:

/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 → สร้างตามนี้:

  1. 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
  1. 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
  1. เพิ่มใน payload.config.ts:
import { Posts } from './collections/Posts'
import { Pages } from './collections/Pages'
// ...
collections: [Users, Media, Posts, Pages],
  1. 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-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:

# 1. สร้าง ConsentLogs collection → export default ไม่ใช่ named export!
cp ConsentLogs.ts src/collections/

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

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

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

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

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


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

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

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

  1. ต้องใช้ mongooseAdapter ไม่ใช่ mongodbAdapter

    // ✅ ถูกต้อง
    import { mongooseAdapter } from '@payloadcms/db-mongodb'
    db: mongooseAdapter({ url: process.env.MONGODB_URL })
    
    // ❌ ผิด — ไม่มี export ชื่อนี้
    import { mongodbAdapter } from '@payloadcms/db-mongodb'
    
  2. ConsentLogs ต้องใช้ export default

    // ✅ ถูกต้อง — 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:

cd portal-mini-store-template
docker compose up -d

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

  1. เพิ่ม environment ใน docker-compose.yml:
payload:
  environment:
    - NEXT_PUBLIC_SERVER_URL=http://110.164.146.185:3000
    - MONGODB_URL=mongodb://mongo:27017/portal-mini-store
  1. เพิ่ม 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: Title field structure — seoTitle vs title

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

ทางแก้: Payload Pages collection ควรมี 2 fields:

{ name: 'title', type: 'text', required: true },        // short: "AI Automation" → h1
{ name: 'seoTitle', type: 'text' },                     // full: "AI Automation | MoreminiMore" → <title>

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

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

รูปแบบผิด:

// ❌ ไม่ทำงาน
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_PASSWORD env
  • 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 มากับ @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
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:

  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

# 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

  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