# Payload CMS + Next.js Troubleshooting ## PostgreSQL Connection Issues ### Wrong port - Docker container `astro-starter-db-1` exposes PostgreSQL on port **5555** (not 5432) - 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 `<>` (แทน ``) 2. **Thai text หรือ non-ASCII text** ใน JSX attributes/props ของ elements ภายใน fragment ถ้าไฟล์มี `<>` แต่ไม่มี Thai text → compile ผ่าน ถ้าไฟล์มี Thai text แต่ใช้ `` → compile ผ่าน ### Root Cause Next.js 15.3.8 มี SWC compiler bug ที่ค้าง stale cache ของ SyntaxError ไว้แม้หลังแก้ไขไฟล์แล้ว ### Workaround (2 วิธี) **วิธีที่ 1 — เปลี่ยนจาก `<>` เป็น `` หรือ ``:** ```tsx import { Fragment } from 'react' // แทน: return <>
...
// ใช้: return
...
``` **วิธีที่ 2 — เขียน component ใหม่ทั้งหมด (แนะนำ):** ถ้า component มี fragment shorthand + Thai text เยอะ ให้เขียนใหม่โดยใช้ pattern ที่ไม่มีปัญหา: - ใส่ `return (...)` โดยไม่มี `<>` ครอบ - ใช้ wrapper `
` แทน fragment ถ้าเป็นไปได้ - ถ้าต้องใช้ fragment ใช้ `` ### How to Detect ```bash # ดูว่าไฟล์มี fragment shorthand และ Thai text หรือไม่ grep -l "<>" src/app/\(frontend\)/**/*.tsx | xargs grep -l "[ก-๙]" ``` ### Prevention หลีกเลี่ยงการใช้ `<>` shorthand ใน component ที่มี Thai text — ใช้ `
` wrapper หรือ `` แทนเสมอ --- ## 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 rm -rf /home/node/app/.next docker restart # รอ 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 rm -rf /home/node/app/.next docker restart ``` **สาเหตุ:** 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 { const pages = [/* ... */] const xml = ` ${pages.map(p => ` ${p.url}...`).join('\n')} ` return new Response(xml, { headers: { 'Content-Type': 'application/xml' }, }) } // ❌ ผิด — MetadataRoute.Sitemap as default export export default async function sitemap(): Promise { // ...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 { 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 { 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 จะเป็น -mongo หรือ -db ``` ### ตรวจสอบว่า Posts ถูก Insert แล้วผ่าน Payload API ```bash docker exec 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 (ดูด้านบน)