nextjs-integration

star 0

This skill should be used when the user asks about "Payload with Next.js", "getPayload in server component", "Payload App Router", "Payload route groups", "Payload live preview Next.js", "revalidate Payload page", "Payload server actions", "Payload draft mode", "Payload Next.js cache", or needs to wire PayloadCMS into a Next.js v14/v15 frontend.

Agents-Store By Agents-Store schedule Updated 5/17/2026

name: nextjs-integration description: This skill should be used when the user asks about "Payload with Next.js", "getPayload in server component", "Payload App Router", "Payload route groups", "Payload live preview Next.js", "revalidate Payload page", "Payload server actions", "Payload draft mode", "Payload Next.js cache", or needs to wire PayloadCMS into a Next.js v14/v15 frontend.

PayloadCMS — Next.js Integration

PayloadCMS v3 is Next.js native. The admin panel ships as Next.js App Router routes, and your frontend lives in the same project. This skill covers data fetching in Server Components, cache invalidation, draft mode, live preview, server actions, and route groups.

Project Layout (created by create-payload-app)

src/
├── app/
│   ├── (frontend)/                  # Your public site
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   ├── posts/
│   │   │   ├── page.tsx
│   │   │   └── [slug]/page.tsx
│   │   └── api/                     # Public API routes (optional)
│   │       └── webhook/route.ts
│   └── (payload)/                   # Payload admin + REST + GraphQL
│       ├── admin/
│       │   └── [[...segments]]/page.tsx
│       ├── api/
│       │   └── [...slug]/route.ts
│       ├── api/graphql/route.ts
│       ├── layout.tsx
│       └── custom.scss
├── collections/
├── payload.config.ts
└── payload-types.ts                  # Generated by `pnpm generate:types`

Two route groups keep frontend and admin layouts separate. Don't move (payload) — Payload's import map and Tailwind config assume that exact path.

Reading Data in Server Components

The Local API is the recommended path — zero network overhead, full type safety:

// app/(frontend)/posts/page.tsx
import { getPayload } from 'payload'
import config from '@payload-config'
import Link from 'next/link'

export default async function PostsList() {
  const payload = await getPayload({ config })

  const { docs: posts } = await payload.find({
    collection: 'posts',
    where: { _status: { equals: 'published' } },
    sort: '-publishedAt',
    limit: 20,
    depth: 1,
  })

  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>
          <Link href={`/posts/${p.slug}`}>{p.title}</Link>
        </li>
      ))}
    </ul>
  )
}

@payload-config is the path alias the scaffolder adds to tsconfig.json (compilerOptions.paths). It resolves to src/payload.config.ts.

Generated Types

After every collection / global change:

pnpm generate:types

Import generated types in app code:

import type { Post, User, Page } from '@/payload-types'

This makes the Local API responses fully typed: payload.find({ collection: 'posts' }) returns PaginatedDocs<Post>.

Single Document by Slug

// app/(frontend)/posts/[slug]/page.tsx
import { getPayload } from 'payload'
import config from '@payload-config'
import { notFound } from 'next/navigation'
import { draftMode } from 'next/headers'

export default async function PostPage({ params }: { params: { slug: string } }) {
  const { isEnabled: isDraft } = await draftMode()
  const payload = await getPayload({ config })

  const { docs } = await payload.find({
    collection: 'posts',
    where: { slug: { equals: params.slug } },
    draft: isDraft,
    limit: 1,
    depth: 2,
  })

  const post = docs[0]
  if (!post) return notFound()

  return (
    <article>
      <h1>{post.title}</h1>
      {/* render content via RichText (see lexical-editor skill) */}
    </article>
  )
}

Static Generation + ISR

For mostly-static pages, statically generate at build and revalidate via hooks:

// app/(frontend)/posts/[slug]/page.tsx
export const revalidate = 3600     // 1 hour

export async function generateStaticParams() {
  const payload = await getPayload({ config })
  const { docs } = await payload.find({
    collection: 'posts',
    where: { _status: { equals: 'published' } },
    limit: 1000,
    select: { slug: true },
  })
  return docs.map((p) => ({ slug: p.slug }))
}

Revalidation on Save

Wire Payload's afterChange and afterDelete hooks to revalidatePath / revalidateTag:

// src/collections/Posts.ts
import { revalidatePath, revalidateTag } from 'next/cache'

export const Posts: CollectionConfig = {
  slug: 'posts',
  // …
  hooks: {
    afterChange: [
      ({ doc, previousDoc, req: { context } }) => {
        if (context?.disableRevalidate) return doc
        if (doc._status === 'published') {
          revalidatePath(`/posts/${doc.slug}`)
          revalidateTag('posts')
        }
        // If slug changed, also kill the old path
        if (previousDoc?.slug && previousDoc.slug !== doc.slug) {
          revalidatePath(`/posts/${previousDoc.slug}`)
        }
        return doc
      },
    ],
    afterDelete: [
      ({ doc }) => {
        revalidatePath(`/posts/${doc.slug}`)
        revalidateTag('posts')
      },
    ],
  },
}

To bulk-write without firing revalidation:

await payload.update({ collection: 'posts', where: {/*…*/}, data: {/*…*/}, context: { disableRevalidate: true }, req })

Draft Mode

Lets editors preview unpublished content from the live frontend.

  1. Add a preview URL on the collection:
admin: {
  preview: (doc) =>
    `${process.env.NEXT_PUBLIC_SITE_URL}/api/preview?slug=${doc.slug}&secret=${process.env.PREVIEW_SECRET}&path=/posts/${doc.slug}`,
}
  1. Wire a Next.js route handler to enable draft mode:
// app/(frontend)/api/preview/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url)
  const secret = searchParams.get('secret')
  const path = searchParams.get('path') || '/'

  if (secret !== process.env.PREVIEW_SECRET) {
    return new Response('Invalid token', { status: 401 })
  }

  const dm = await draftMode()
  dm.enable()
  redirect(path)
}
  1. Exit:
// app/(frontend)/api/exit-preview/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'

export async function GET() {
  ;(await draftMode()).disable()
  redirect('/')
}
  1. Read drafts in your page via draftMode() + draft: true (see Single Document above).

Live Preview (real-time)

admin: {
  livePreview: {
    url: ({ data }) =>
      `${process.env.NEXT_PUBLIC_SITE_URL}/posts/${data.slug}?live-preview=true`,
    breakpoints: [
      { name: 'mobile', width: 375, height: 667, label: 'Mobile' },
      { name: 'desktop', width: 1440, height: 900, label: 'Desktop' },
    ],
  },
}

On the frontend, use Payload's hook to receive live-edit messages from the iframe parent:

// app/(frontend)/posts/[slug]/page-live-preview.client.tsx
'use client'
import { RefreshRouteOnSave } from '@payloadcms/live-preview-react'

export function LivePreview() {
  return <RefreshRouteOnSave />
}

Add it to the page only when in live-preview mode.

Server Actions

Server actions can call the Local API directly:

// app/(frontend)/comments/new/page.tsx
'use server'

import { getPayload } from 'payload'
import config from '@payload-config'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function submitComment(formData: FormData) {
  const payload = await getPayload({ config })

  await payload.create({
    collection: 'comments',
    data: {
      author: formData.get('author') as string,
      body: formData.get('body') as string,
    },
  })

  revalidatePath('/comments')
  redirect('/comments/thanks')
}

For server actions handling authenticated user requests, propagate auth via payload.auth({ headers: request.headers }) and use overrideAccess: false.

Caching Strategy

  • Local API does NOT auto-cache. Wrap fetches with next.cache or unstable_cache:
    import { unstable_cache } from 'next/cache'
    
    export const getPublishedPosts = unstable_cache(
      async () => {
        const payload = await getPayload({ config })
        return payload.find({ collection: 'posts', where: { _status: { equals: 'published' } } })
      },
      ['posts', 'published'],
      { tags: ['posts'], revalidate: 60 },
    )
    
  • Invalidate cached fetches with revalidateTag('posts') from Payload hooks.
  • Don't wrap admin / REST endpoints — they manage their own caching.

Custom Endpoints

Add a custom server-side endpoint to the Payload api/:

// payload.config.ts
endpoints: [
  {
    path: '/health',
    method: 'get',
    handler: () => Response.json({ ok: true, ts: new Date().toISOString() }),
  },
],

Accessible at /api/health. See the collections skill for collection-scoped endpoints.

Auth in API Routes

// app/(frontend)/api/me/route.ts
import { getPayload } from 'payload'
import config from '@payload-config'

export async function GET(req: Request) {
  const payload = await getPayload({ config })
  const { user } = await payload.auth({ headers: req.headers })

  if (!user) return new Response('Unauthorized', { status: 401 })
  return Response.json({ user })
}

Deployment Notes

  • Vercel — works out of the box. Use @payloadcms/db-vercel-postgres and @payloadcms/storage-vercel-blob.
  • Self-host (Node)pnpm build && pnpm start. Static assets from public/ go behind your reverse proxy (Caddy / nginx).
  • Payload Cloud — managed hosting from the Payload team. One-click deploy from GitHub.
  • Edge runtime — admin and Payload routes require Node runtime. Don't set export const runtime = 'edge' on (payload) routes.

See Also

  • The setup skill — initial project creation.
  • The queries skill — Local API method reference.
  • The hooks skill — afterChange / afterDelete patterns.
  • The lexical-editor skill — rendering richText in server components.
  • The jobs-queue skill — scheduling jobs via Vercel cron.
Install via CLI
npx skills add https://github.com/Agents-Store/claude-plugins --skill nextjs-integration
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
Agents-Store
Agents-Store Explore all skills →