@delmaredigital/payload-puck
A PayloadCMS plugin for integrating Puck visual page builder. Build pages visually with drag-and-drop components while leveraging Payload's content management capabilities.
Documentation: plugins.delmaredigital.com/docs/puck · Overview
⚠️ Upgrading to 0.10.x from 0.9? Breaking — requires
@puckeditor/core0.24. Runpnpm add @puckeditor/core@^0.24.0. The built-in components now arrange their sidebar fields in General / Style / Layout tabs, and the action bar shows a drag handle (dnd={{ enableDragHandle: false }}onPuckEditorhides it). Saved page data is unchanged. 0.10.1 only raises dependency floors. 0.10.2 is a security fix — upgrade recommended: the admin editor view now applies the collection's read access. It also makes the admin editor full-screen, and the canvas no longer inherits the admin page's CSS; list any stylesheet your preview needs ineditorStylesheets. 0.10.3 addsintegrations(bundles of components, categories and editor plugins from other packages); nothing changes if you don't use them. 0.11.0 is a security fix and breaking: anonymous callers can read published pages only (collections with drafts turned off are unaffected), so a preview that reads drafts without a signed-in user must pass one or setaccess.read; see upgrading. See the CHANGELOG.Coming from an older version? The Upgrade Guide lists what each breaking or security release needs.
Documentation
Covers installation, configuration, components, custom fields, theming, layouts, dark mode, page-tree integration, hybrid integration, AI integration, and more.
Install
pnpm add @delmaredigital/payload-puck @puckeditor/core
Requirements
| Dependency | Version |
|---|---|
node | >= 20.9.0 |
@puckeditor/core | >= 0.24.0 |
payload | >= 3.69.0 |
@payloadcms/next | >= 3.69.0 |
next | >= 15.4.8 (see security note below) |
react | >= 19.2.1 |
Using
@delmaredigital/payload-better-auth? Any published version works — its auth strategy stampscollectionon the user, which is what 0.9's access resolution needs. 0.9.0 or later is recommended: it is the first release where the recommendedpayload.auth()wiring, combined with the request headers 0.9 now forwards into Payload, is free of side effects for API-key requests. Current is0.11.3, which requires Better Auth1.7.
Note: Puck 0.21+ moved from
@measured/puckto@puckeditor/core. This plugin requires the new package scope.
Security: If your app uses Next.js middleware (or proxy.ts) to protect dynamic routes, use
next>= 15.5.16 / 16.2.5 to pick up the fix for CVE-2026-44574 (middleware bypass via dynamic route parameter injection). Turbopack users need >= 15.5.18 / 16.2.6.
Quick Start
1. Add the Plugin
// src/payload.config.ts
import { buildConfig } from 'payload'
import { createPuckPlugin } from '@delmaredigital/payload-puck/plugin'
export default buildConfig({
plugins: [
createPuckPlugin({
pagesCollection: 'pages',
}),
],
})
2. Provide Puck Configuration
// app/(app)/layout.tsx
import { PuckConfigProvider } from '@delmaredigital/payload-puck/client'
import { editorConfig } from '@delmaredigital/payload-puck/config/editor'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<PuckConfigProvider config={editorConfig}>
{children}
</PuckConfigProvider>
</body>
</html>
)
}
3. Create a Frontend Route
// 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'
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
}
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} />
}
That's it! The plugin registers the editor view, API endpoints, and "Edit with Puck" buttons automatically.
Copy starter files from GitHub or the starter files guide. See examples/README.md for the file guide and setup notes. The npm package ships only dist; examples must be copied from GitHub or the docs. Contributors can check them against the source exports with pnpm typecheck:examples.
Documentation
For everything else — components, custom fields, theming, layouts, dark mode, page-tree integration, hybrid integration, AI integration, advanced configuration, and the full export reference — see the full documentation.
Integrations
An integration bundles Puck components, categories and optional editor plugins. Host components and category fields win; integrations fill missing component keys and append category names. Config helpers ignore plugins.
// Render config
import { extendConfig, baseConfig, type PuckIntegration } from '@delmaredigital/payload-puck/config'
const integrations: readonly PuckIntegration[] = [{
name: 'dummy',
components: { DummyBlock: { render: () => <div>Dummy block</div> } },
categories: { custom: { title: 'Custom', components: ['DummyBlock'] } },
}]
export const renderConfig = extendConfig({ base: baseConfig, integrations })
'use client'
import { PuckConfigProvider, type PuckIntegration } from '@delmaredigital/payload-puck/client'
import { editorConfig } from '@delmaredigital/payload-puck/config/editor'
import type { ReactNode } from 'react'
const integrations: readonly PuckIntegration[] = [{
name: 'dummy',
components: { DummyBlock: { render: () => <div>Dummy block</div> } },
categories: { custom: { title: 'Custom', components: ['DummyBlock'] } },
}]
export function EditorProvider({ children }: { children: ReactNode }) {
return <PuckConfigProvider config={editorConfig} integrations={integrations}>{children}</PuckConfigProvider>
}
Create the integration array once at module scope: a new array per render rebuilds the editor config. PuckEditor also accepts integrations directly. Integration plugins follow prop and context plugins; named integration plugins are deduped against prop, context and earlier integration plugins. plugins={false} disables all plugins while integration components still apply.
The editor applies integrations without exclude, so to opt out of an integration's component, omit that integration from the provider or define your own component under the same key, which wins.
Running experiments on slots
Use @delmaredigital/payload-experiments to run experiments on Puck slots through an integration. Works with: payload-experiments ≥0.4.0 ↔ payload-puck ≥0.10.3.
License
MIT