Existing projects
Add Puck to a Payload project that already has pages, routes and legacy blocks, and render both kinds of page.
You can add Puck to a Payload project that already has a pages collection, frontend routes and legacy Payload blocks. Old pages keep rendering from their blocks; new pages are built in Puck.
If your project has existing frontend routes, you must update them to render Puck content. Without that, Puck pages render blank, because your routes only look for legacy block fields like layout or hero.
Add the plugin
Add createPuckPlugin() to payload.config.ts, as in the quick start.
Add the provider
Add PuckConfigProvider to your admin layout, as in the quick start.
Update your frontend templates
Render puckData, with one of the rendering options below.
Regenerate Payload types
The plugin injects fields into your collection (puckData, editorVersion, and others such as pageLayout, isHomepage and conversionTracking), so regenerate Payload's types. You don't need to hand-write a collection definition.
payload generate:types
# Also regenerate the import map for the admin components the plugin registers:
payload generate:importmapImport the generated Page type from your payload-types instead of defining your own.
What the plugin adds
If you already have a pages collection, the plugin adds only the Puck fields it doesn't find:
export default buildConfig({
collections: [
{
slug: 'pages',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'layout', type: 'blocks', blocks: [HeroBlock, CTABlock] },
],
},
],
plugins: [
createPuckPlugin({ pagesCollection: 'pages' }),
],
})It adds puckData, editorVersion, pageLayout, isHomepage (with its uniqueness hook), the meta SEO group and the conversionTracking group, each only if your collection doesn't already have a field by that name, plus the "Edit with Puck" button. It turns on drafts if your collection has no versions setting. The editorVersion field records whether each page uses legacy blocks or Puck.
Manual setup
If you'd rather own the collection, set autoGenerateCollection: false and add the fields yourself.
Returns the fields and the hooks, so you get the isHomepage uniqueness hook without wiring it:
import type { CollectionConfig } from 'payload'
import { getPuckCollectionConfig } from '@delmaredigital/payload-puck/plugin'
const { fields: puckFields, hooks: puckHooks } = getPuckCollectionConfig({
includeSEO: true,
includeEditorVersion: true,
includePageLayout: true,
includeIsHomepage: true,
})
export const Pages: CollectionConfig = {
slug: 'pages',
hooks: {
beforeChange: [
...(puckHooks?.beforeChange ?? []),
],
},
fields: [
{ name: 'title', type: 'text' },
{ name: 'layout', type: 'blocks', blocks: [/* ... */] },
...puckFields,
],
}getPuckFields() is also exported from the package root.
Prop
Type
With autoGenerateCollection: false and page-tree, plugin order no longer matters, because the collection exists before either plugin runs.
Rendering hybrid pages
HybridPageRenderer (recommended)
HybridPageRenderer picks the renderer for each page: Puck when editorVersion is 'puck' and puckData has content, otherwise your legacy renderer if blocks exist, otherwise a fallback.
import { HybridPageRenderer } from '@delmaredigital/payload-puck/render'
import { baseConfig } from '@delmaredigital/payload-puck/config'
import { LegacyBlockRenderer } from '@/components/LegacyBlockRenderer'
export default async function Page({ params }) {
const page = await getPage((await params).slug)
return (
<HybridPageRenderer
page={page}
config={baseConfig}
legacyRenderer={(blocks) => <LegacyBlockRenderer blocks={blocks} />}
/>
)
}legacyRenderer is optional: leave it out on a project with only Puck pages. HybridPageRenderer also takes legacyBlocksField (default 'layout'), fallback (default <div>No content available</div>), and the PageRenderer props config, layouts, wrapper and className. Type your blocks with the generic, as in HybridPageRenderer<NonNullable<Page['layout']>>.
If Payload's generated types for JSON fields are too loose for page, convert with toHybridPageData(page), also from /render. It throws if editorVersion holds anything other than 'legacy', 'puck' or nothing.
Manual detection
// Check whether the page was built with Puck
const isPuckPage = page.editorVersion === 'puck' && page.puckData?.content?.length > 0
if (isPuckPage) {
return <PageRenderer config={baseConfig} data={page.puckData} />
}
// Fall back to legacy rendering
return <LegacyBlockRenderer blocks={page.layout} />
