Skip to Content
Delmare Digital

Quick start

Add the plugin, provide the Puck configuration and create a frontend route.

The plugin plugs into Payload's admin with minimal configuration. API endpoints and the admin editor view are registered for you; you add a config provider and a frontend route.

Add the plugin

src/payload.config.ts
import { fileURLToPath } from 'node:url'
import { buildConfig } from 'payload'
import { postgresAdapter } from '@payloadcms/db-postgres'
import { createPuckPlugin } from '@delmaredigital/payload-puck/plugin'

export default buildConfig({
  secret: process.env.PAYLOAD_SECRET!,
  db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URL! } }),
  collections: [{ slug: 'users', auth: true, fields: [] }],
  admin: {
    user: 'users',
    importMap: { baseDir: fileURLToPath(new URL('.', import.meta.url)) },
    components: { providers: ['/components/admin/PuckProvider'] },
  },
  plugins: [
    createPuckPlugin({
      pagesCollection: 'pages', // Collection slug (default: 'pages')
    }),
  ],
})

This:

  • Creates a pages collection with Puck fields, or adds the fields to your existing pages collection.
  • Registers API endpoints at /api/puck/:collection.
  • Adds the Puck editor view at /admin/puck-editor/:collection/:id.
  • Adds "Visual Editor" buttons to the admin UI.

Provide the Puck configuration

The Payload config above registers a client provider for the admin. Create that file so the editor can read the configuration through React context:

src/components/admin/PuckProvider.tsx
'use client'

import type { ReactNode } from 'react'
import { PuckConfigProvider } from '@delmaredigital/payload-puck/client'
import { editorConfig } from '@delmaredigital/payload-puck/config/editor'

export default function PuckProvider({ children }: { children: ReactNode }) {
  return <PuckConfigProvider config={editorConfig}>{children}</PuckConfigProvider>
}

PuckConfigProvider also accepts layouts, theme, plugins, integrations, editorStylesheets and editorCss. See Layouts, Theming and the provider props.

Then regenerate Payload's types and import map, so the injected fields are typed and the admin components resolve:

payload generate:types
payload generate:importmap

Create a frontend route

Next.js App Router routes are files, so the plugin can't create this one for you. Copy it:

src/app/(frontend)/[[...slug]]/page.tsx
import { getPayload } from 'payload'
import config from '@payload-config'
import { PageRenderer } from '@delmaredigital/payload-puck/render'
import { baseConfig } from '@delmaredigital/payload-puck/config'
import { notFound } from 'next/navigation'
import type { Metadata } from 'next'
import type { Data } from '@puckeditor/core'

// Fetch page by slug (or homepage if no slug)
async function getPage(slug?: string[]) {
  const payload = await getPayload({ config })
  const slugPath = slug?.join('/') || ''

  const { docs } = await payload.find({
    collection: 'pages',
    where: {
      and: [
        { _status: { equals: 'published' } },
        slugPath
          ? { slug: { equals: slugPath } }
          : { isHomepage: { equals: true } },
      ],
    },
    limit: 1,
  })

  return docs[0] || null
}

// Generate metadata from page SEO fields
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug?: string[] }>
}): Promise<Metadata> {
  const { slug } = await params
  const page = await getPage(slug)

  if (!page) return {}

  return {
    title: page.meta?.title || page.title,
    description: page.meta?.description,
  }
}

// Render the page
export default async function Page({
  params,
}: {
  params: Promise<{ slug?: string[] }>
}) {
  const { slug } = await params
  const page = await getPage(slug)

  if (!page) notFound()

  return <PageRenderer config={baseConfig} data={page.puckData as Data} />
}

The double brackets in [[...slug]] make the segment optional, so one route handles both / (the homepage) and /any/path.

That's it

Open a page in the admin and choose "Visual Editor". The editor opens full-screen at /admin/puck-editor/:collection/:id, still under /admin, so Payload's session and access rules apply, and its Back button returns to the document. Saves go through the plugin's endpoints, registered with Payload's endpoint system.

Next

Edit this page on GitHub

On this page