Skip to Content
Delmare Digital
Guides

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:importmap

Import 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:

payload.config.ts
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:

collections/Pages.ts
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 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.

app/(frontend)/[[...slug]]/page.tsx
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} />
Edit this page on GitHub

On this page