Files
opencode-skill/skills/website-creator/references/payload-nextjs-notes.md
Kunthawat Greethong ce8483e546 Remove Astro templates, fix sitemap template for Next.js
- Delete: CookieConsent.astro (old Astro component)
- Delete: consent.ts, right-to-be-forgotten.ts (Astro API routes)
- Update: route.ts is now proper Next.js route handler
- Update: sitemap-template.md - replace Astro pages structure with Next.js app/ structure
- Update: payload-nextjs-notes.md - fix MongoDB port reference
- Note: seo-multi-channel auto_publish.py is for Astro sites (kept as-is)
2026-04-17 11:03:10 +07:00

489 lines
16 KiB
Markdown

# Payload CMS + Next.js Troubleshooting
## PostgreSQL Connection Issues
### Wrong port
- Docker container `payload-db-1` exposes MongoDB on port **27017** (default)
- Fix: Use `localhost:5555` in DATABASE_URL for local development
### Wrong database name
- Payload CMS expects database `payload` (matches `POSTGRES_DB=payload`)
- **NOT** `postgres` or `payloaddb`
- Working DATABASE_URL: `postgresql://payload:payloadpass@localhost:5555/payload`
### Wrong credentials
- Docker compose uses `POSTGRES_USER=payload` / `POSTGRES_PASSWORD=payloadpass`
- NOT the default `postgres:postgres`
### Schema not creating tables
**Symptom:** Admin page shows blank/white but HTML loads fine. Tables don't exist in DB.
**Root cause:** `payload migrate` may not have run or failed silently.
**Fix:**
```bash
# 1. Stop dev server
pkill -f "next"
# 2. Run migration
cd /path/to/project
pnpm payload migrate --yes
# OR for fresh start:
pnpm payload migrate:fresh --yes
# 3. Verify tables created
PGPASSWORD=payloadpass psql -h localhost -p 5555 -U payload -d payload -c "\dt"
# 4. Restart dev server
pnpm dev
```
## Admin Page Blank/White Screen
### Causes
1. **Browser cache from old deployment** — standalone mode serves old static file hashes
- Fix: Ctrl+Shift+R (hard refresh) or open Incognito window
2. **Static files not matching the build** — running standalone with dev `.next`
- Fix: Always `pnpm build` before running `node .next/standalone/server.js`
- OR just use `pnpm dev` for development
3. **Database tables don't exist** — Payload admin can't load without schema
- Fix: Run `pnpm payload migrate` to create tables
4. **WebSocket HMR errors** — not a real issue, just hot reload failing
- This is cosmetic and doesn't affect functionality
### Verification
```bash
# Check if admin HTML loads
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/admin
# Should return 200
# Check if JS chunks load (may 404 in dev mode - OK)
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/_next/static/chunks/0pmuyajd0waqg.js
# Check DB tables
PGPASSWORD=payloadpass psql -h localhost -p 5555 -U payload -d payload -c "\dt"
# Should show: media, payload_kv, posts, users, users_sessions, etc.
```
## Payload Migration Commands
```bash
pnpm payload migrate # Run pending migrations
pnpm payload migrate:fresh # Drop all tables and recreate (DANGEROUS)
pnpm payload migrate:reset # Reset migration history
pnpm generate:types # Generate TypeScript types
pnpm generate:importmap # Regenerate import map
```
## Payload CMS 3.x Breaking Changes
- `GRAPHQL_GET` → use `GRAPHQL_PLAYGROUND_GET` from `@payloadcms/next/routes`
- Collection config imports must use `import type { CollectionConfig } from 'payload'`
- `payload push` deprecated → use `payload migrate`
- PostgreSQL adapter in separate package: `@payloadcms/db-postgres`
- Rich text editor in separate package: `@payloadcms/richtext-lexical`
## Docker Compose for PostgreSQL
```yaml
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: payload
POSTGRES_PASSWORD: payloadpass
POSTGRES_DB: payload
ports:
- '5432:5432' # Only if not already in use
```
DATABASE_URL: `postgresql://payload:***@localhost:5432/payload`
(Port depends on what's already mapped in docker-compose)
---
## Next.js 15.3.8 + React 19 SWC Bug (Critical)
### Symptom
Build หรือ dev server compile ส่ง SyntaxError แปลกๆ เช่น:
```
SyntaxError: Unexpected token (50:3)
49 | return (
> 50 | <>
| ^
```
เกิดขึ้นกับ **เฉพาะไฟล์ที่มี**:
1. Fragment shorthand `<>` (แทน `<React.Fragment>`)
2. **Thai text หรือ non-ASCII text** ใน JSX attributes/props ของ elements ภายใน fragment
ถ้าไฟล์มี `<>` แต่ไม่มี Thai text → compile ผ่าน
ถ้าไฟล์มี Thai text แต่ใช้ `<React.Fragment>` → compile ผ่าน
### Root Cause
Next.js 15.3.8 มี SWC compiler bug ที่ค้าง stale cache ของ SyntaxError ไว้แม้หลังแก้ไขไฟล์แล้ว
### Workaround (2 วิธี)
**วิธีที่ 1 — เปลี่ยนจาก `<>` เป็น `<React.Fragment>` หรือ `<Fragment>`:**
```tsx
import { Fragment } from 'react'
// แทน:
return <>
<div>...</div>
</>
// ใช้:
return <Fragment><div>...</div></Fragment>
```
**วิธีที่ 2 — เขียน component ใหม่ทั้งหมด (แนะนำ):**
ถ้า component มี fragment shorthand + Thai text เยอะ ให้เขียนใหม่โดยใช้ pattern ที่ไม่มีปัญหา:
- ใส่ `return (...)` โดยไม่มี `<>` ครอบ
- ใช้ wrapper `<div>` แทน fragment ถ้าเป็นไปได้
- ถ้าต้องใช้ fragment ใช้ `<Fragment>`
### How to Detect
```bash
# ดูว่าไฟล์มี fragment shorthand และ Thai text หรือไม่
grep -l "<>" src/app/\(frontend\)/**/*.tsx | xargs grep -l "[ก-๙]"
```
### Prevention
หลีกเลี่ยงการใช้ `<>` shorthand ใน component ที่มี Thai text — ใช้ `<div>` wrapper หรือ `<Fragment>` แทนเสมอ
---
## ConsentLogs: Default Export Required
Payload CMS บางเวอร์ชัน require ว่า collection config ที่สร้างเองต้องใช้ **default export** ไม่ใช่ named export
```ts
// ✅ ถูกต้อง
const ConsentLogs: CollectionConfig = { ... }
export default ConsentLogs
// ❌ ผิด — named export จะทำให้ Payload มองไม่เห็น collection
export const ConsentLogs = { ... }
```
ถ้า collection ไม่ปรากฏใน Payload admin → ตรวจสอบว่าใช้ `export default` ไม่ใช่ `export const`
---
## Payload Access Functions: Must Be Separate File
Payload CMS ไม่รู้จัก `access` property ที่เป็น inline function ใน collection config — ต้องแยกออกมาเป็นไฟล์
**ถูกต้อง:** `src/collections/access.ts`
```ts
import type { Access } from 'payload'
export const admins: Access = () => true
export const anyone: Access = () => true
```
**แล้ว import ใน collection:**
```ts
import { admins } from './access'
const MyCollection: CollectionConfig = {
access: { create: admins },
}
```
**ผิด:** inline function ใน collection config จะถูก strip หรือไม่ทำงาน
---
## Dev Mode: IP Access + allowedDevOrigins
เมื่อรัน dev server แล้วเข้าผ่าน IP address (เช่น `110.164.146.185:3000`) จะมี warning:
```
Access to server at IP from the development server is blocked by CORS policy.
allowedDevOrigins
```
### Fix: เพิ่ม allowedDevOrigins ใน next.config.ts
```ts
const nextConfig: NextConfig = {
allowedDevOrigins: ['110.164.146.185', '110.164.146.185:3000'],
}
```
### Docker: อย่าลืม Restart + Clear Cache หลังแก้ไข
```bash
docker exec <container> rm -rf /home/node/app/.next
docker restart <container>
# รอ warm up 10-40 วินาที แล้วค่อยเทสต์
```
---
## SWC Cache: Stale Cache หลังแก้ไข Error
ถ้าแก้ไข syntax error แล้ว dev server ยังแสดง error เดิม → SWC cache ค้าง
**วิธีแก้:**
```bash
# ลบ .next cache
rm -rf .next
# ถ้าใช้ Docker
docker exec <container> rm -rf /home/node/app/.next
docker restart <container>
```
**สาเหตุ:** Next.js 15 SWC compiler cache ระดับ binary ค้างอยู่ใน `.next/cache/swc`
---
## sitemap.xml Route (Next.js App Router)
`MetadataRoute.Sitemap` as a **default export function** fails with 500/timeout in Next.js App Router. The correct pattern:
```ts
// ✅ ถูกต้อง — ใช้ GET handler + new Response()
export async function GET(): Promise<Response> {
const pages = [/* ... */]
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages.map(p => ` <url><loc>${p.url}</loc>...</url>`).join('\n')}
</urlset>`
return new Response(xml, {
headers: { 'Content-Type': 'application/xml' },
})
}
// ❌ ผิด — MetadataRoute.Sitemap as default export
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// ...returns array — causes 500 in some Next.js versions
}
```
Payload first request ช้ามาก (7-35s) ทำให้ sitemap timeout — ใช้ fallback static data:
```ts
const STATIC_PAGES = [
{ url: 'https://example.com/', priority: 1.0, changefreq: 'weekly' },
// ...
]
export async function GET(): Promise<Response> {
let pages: string[] = []
try {
const payload = await getPayload({ config })
const { docs } = await payload.find({ collection: 'pages', limit: 100 })
pages = docs.map(d => d.slug as string)
} catch {
// Payload unavailable — use static fallback
}
// ...build XML
}
```
---
## Critical: `devBundleServerPackages: false` + `.next` Cache Clear = Total Failure
**Symptom:** หลังลบ `.next` cache แล้ว restart dev server — ทุกหน้ารวม `/` เป็น **500 error** พร้อม:
```
Error: Failed to load external module payload-e448a27c99c096d3
Cannot find package 'payload-e448a27c99c096d3'
```
**Root Cause:** `withPayload(nextConfig, { devBundleServerPackages: false })` บอก Payload ว่าไม่ต้อง bundle Payload packages ลงใน `.next` แต่ Turbopack ยังอ้างถึง bundled chunk names เดิมจาก cache ที่ถูกลบไปแล้ว
**Fix:** ลบ `{ devBundleServerPackages: false }` ออก — ใช้แค่ `withPayload(nextConfig)`
```ts
// ✅ ถูกต้อง
export default withPayload(nextConfig)
// ❌ ลบออก — ทำให้ล้มเหลวหลัง clear .next cache
export default withPayload(nextConfig, { devBundleServerPackages: false })
```
**Prevention:** ถ้าต้อง clear `.next` cache เพราะ cache มีปัญหา ให้ลบ `devBundleServerPackages: false` ก่อน restart dev server
---
## robots.txt Route (Next.js App Router)
`MetadataRoute.Robots` as default export function causes `TypeError: NextResponse.text is not a function` error. Must use explicit GET:
```ts
// ✅ ถูกต้อง
export async function GET() {
return new Response('User-agent: *\nAllow: /\nDisallow: /admin\n', {
headers: { 'Content-Type': 'text/plain' },
})
}
// ❌ ผิด — MetadataRoute.Robots default export
export default function robots(): Promise<MetadataRoute.Robots> {
return Promise.resolve({ rules: { userAgent: '*', allow: '/' } })
}
```
---
## robots.txt Route (Next.js App Router)
**Two patterns that cause 500:**
1. `MetadataRoute.Robots` as default export — บาง version ทำให้ `TypeError: NextResponse.text is not a function`
2. **Cached file conflict** — ถ้ามี file `app/robots.txt` (ไม่ใช่ route.ts) หรือ cached file ใน `.next/dev/server/app/` อยู่ จะทำให้ route.ts handler ถูก ignore แล้ว return empty response
```ts
// ✅ ถูกต้อง
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.text(
`User-agent: *
Allow: /
Disallow: /admin
Disallow: /api/
Sitemap: https://www.example.com/sitemap.xml
`,
{ headers: { 'Content-Type': 'text/plain; charset=utf-8' } }
)
}
```
**ถ้า robots.txt เป็น 500 หรือว่างเปล่า:** ตรวจสอบว่าไม่มี `robots.txt` file ตรง (แทน route.ts) และลบ `.next` cache:
```bash
rm -rf .next
```
---
## sitemap.xml: Array Return = 500 Error
**Symptom:** `GET /sitemap.xml` returns 500 — log บอกว่าได้ `Array` แทน `Response`
**Root Cause:** Route handler ส่ง array ไปแทน Response object (เช่น `return [...pages, ...posts]`)
```ts
// ❌ ผิด — array ไม่ใช่ Response
export async function GET() {
const pages = await getPages()
return pages // ← 500 error
}
// ✅ ถูกต้อง
export async function GET() {
const pages = await getPages()
const xml = buildSitemapXml(pages)
return new Response(xml, {
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
})
}
```
---
## `/sitemap` Page Conflicts with `/sitemap.xml` Route
ถ้ามีทั้ง `app/sitemap/page.tsx` และ `app/sitemap.xml/route.ts` — Next.js จะ route ไปที่ page.tsx ก่อน ทำให้ `/sitemap.xml` เป็น **404**
**Fix:** ลบ `app/sitemap/` directory ถ้ามี sitemap.xml route:
```bash
rm -rf app/sitemap/
```
ตรวจสอบ: `ls app/` อย่างน้อยต้องมีไฟล์ `.xml` ไม่ใช่ directory ที่ชื่อเดียวกัน
---
## Bulk Insert Posts ใน MongoDB (Direct via mongosh)
เมื่อ Payload REST API (`POST /api/posts`) ตอบ `500: Something went wrong` เวลา insert richText/Lexical field โดยตรง สามารถใช้ **direct MongoDB insert** แทนได้
### วิธีทำ
```bash
# เขียน script เป็นไฟล์ .cjs (CommonJS)
# รันโดยตรงจาก host (ไม่ต้องเข้า container)
node seed-mongo.cjs
```
### หา MongoDB URL
```bash
grep MONGODB_URL .env
# ถ้าใช้ Docker: mongodb://localhost:27017/portal-mini-store
# ถ้าใช้ Atlas: mongodb+srv://user:pass@cluster.mongodb.net/dbname
```
### Payload SDK Seed Fails ด้วย spawn Error
ถ้า seed script ที่ใช้ Payload SDK (`getPayload()`) ขึ้น error เช่น `spawn is not defined` หรือ `node not found` — นั่นคือ Payload SDK ภายในมีการ `spawn('node')` ซึ่งล้มเหลวในบาง environment
**วิธีแก้: ใช้ MongoDB driver โดยตรง (CommonJS)**
```js
// seed-mongo.cjs — CommonJS เท่านั้น (require, not import)
const { MongoClient } = require('mongodb')
async function main() {
const client = new MongoClient(process.env.MONGODB_URL)
await client.connect()
const db = client.db()
// insert posts
const posts = [/* ... */]
for (const post of posts) {
const result = await db.collection('posts').insertOne({
...post,
createdAt: new Date(),
updatedAt: new Date(),
})
console.log('Inserted:', post.title, result.insertedId)
}
await client.close()
}
main().catch(console.error)
```
### Lexical Content Format ขั้นต่ำ
```js
content: {
root: {
type: 'root',
children: [
{
type: 'paragraph',
children: [{ type: 'text', text: 'your excerpt or content here' }]
}
]
}
}
```
### หา Mongo Container Name
```bash
docker ps --format '{{.Names}}' # ดู container names
# ถ้าใช้ docker-compose จะเป็น <project>-mongo หรือ <project>-db
```
### ตรวจสอบว่า Posts ถูก Insert แล้วผ่าน Payload API
```bash
docker exec <app-container> node -e "
fetch('http://localhost:3000/api/posts?limit=15')
.then(r => r.json())
.then(d => { console.log('Total:', d.totalDocs); d.docs.forEach(p => console.log(' -', p.title)); })
"
```
### ข้อควรระวัง
- Insert ตรงๆ ผ่าน MongoDB จะ bypass Payload access control
- ถ้ามี auth token ต้องใช้ Payload API แทน
- richText field ต้องเป็น Lexical JSON format (ดูด้านบน)