An open API service indexing awesome lists of open source software.

https://github.com/delmaredigital/payload-puck

Puck visual page builder plugin for Payload CMS
https://github.com/delmaredigital/payload-puck

page-builder payload-plugin payloadcms puck typescript visual-editor

Last synced: 4 months ago
JSON representation

Puck visual page builder plugin for Payload CMS

Awesome Lists containing this project

README

          

# @delmaredigital/payload-puck

A PayloadCMS plugin for integrating [Puck](https://puckeditor.com) visual page builder. Build pages visually with drag-and-drop components while leveraging Payload's content management capabilities.


Live Demo - Try It Now
  
Starter Template - Use This


Deploy with Vercel

---

## Documentation

For additional documentation, visit: [https://deepwiki.com/delmaredigital/payload-puck](https://deepwiki.com/delmaredigital/payload-puck)

---

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Adding to Existing Projects](#adding-to-existing-projects)
- [Styling Setup](#styling-setup)
- [Core Concepts](#core-concepts)
- [Components](#components)
- [Custom Fields](#custom-fields)
- [Building Custom Components](#building-custom-components)
- [Theming](#theming)
- [Layouts](#layouts)
- [Dark Mode Support](#dark-mode-support)
- [Page-Tree Integration](#page-tree-integration)
- [Hybrid Integration](#hybrid-integration)
- [AI Integration](#ai-integration)
- [Plugin Order](#plugin-order)
- [Advanced Configuration](#advanced-configuration)
- [License](#license)

---

## Installation

### Requirements

| Dependency | Version | Purpose |
|------------|---------|---------|
| `@puckeditor/core` | >= 0.21.0 | Visual editor core |
| `payload` | >= 3.69.0 | CMS backend |
| `@payloadcms/next` | >= 3.69.0 | Payload Next.js integration |
| `next` | >= 15.4.8 | React framework |
| `react` | >= 19.2.1 | UI library |
| `@tailwindcss/typography` | >= 0.5.0 | RichText component styling |

> **Note:** Puck 0.21+ moved from `@measured/puck` to `@puckeditor/core`. This plugin requires the new package scope.

### Install

```bash
pnpm add @delmaredigital/payload-puck @puckeditor/core
```

---

## Quick Start

The plugin integrates directly into Payload's admin UI with minimal configuration. API endpoints and admin views are registered automatically.

### Step 1: Add the Plugin

```typescript
// src/payload.config.ts
import { buildConfig } from 'payload'
import { createPuckPlugin } from '@delmaredigital/payload-puck/plugin'

export default buildConfig({
plugins: [
createPuckPlugin({
pagesCollection: 'pages', // Collection slug (default: 'pages')
}),
],
// ...
})
```

This automatically:
- Creates a `pages` collection with Puck fields (or adds fields to your existing collection)
- Registers API endpoints at `/api/puck/:collection`
- Adds the Puck editor view at `/admin/puck-editor/:collection/:id`
- Adds "Edit with Puck" buttons to the admin UI

### Step 2: Provide Puck Configuration

Wrap your app with `PuckConfigProvider` to supply the Puck configuration. This makes the config available to the editor via React context.

```typescript
// app/(app)/layout.tsx (covers both admin and frontend)
import { PuckConfigProvider } from '@delmaredigital/payload-puck/client'
import { editorConfig } from '@delmaredigital/payload-puck/config/editor'

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (



{children}



)
}
```

> **Tip:** `PuckConfigProvider` also accepts `layouts` and `theme` props. See [Layouts](#layouts) and [Theming](#theming) sections.

> **Note:** For custom editor UIs (outside Payload admin), you can also pass the config directly to `PuckEditor` instead of using the context provider.

**Alternative: Payload Admin Provider (vanilla starter pattern)**

If you're using the vanilla Payload starter structure, you can register the provider via the admin config instead:

```typescript
// src/payload.config.ts
export default buildConfig({
admin: {
components: {
providers: ['@/components/admin/PuckProvider'],
},
},
// ...
})
```

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

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

export default function PuckProvider({ children }: { children: React.ReactNode }) {
return {children}
}
```

### Step 3: Create a Frontend Route

The plugin can't auto-create frontend routes (Next.js App Router is file-based), but here's copy-paste ready code:

📄 app/(frontend)/[[...slug]]/page.tsx (click to expand)

```typescript
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'

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

// Try to find by slug, or find homepage
// Filter for published pages only (_status: 'published')
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 {
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
}
```

> **Note:** The `[[...slug]]` pattern with double brackets makes the slug optional, so this handles both `/` (homepage) and `/any/path`.

### That's It!

- The plugin registers the editor view at `/admin/puck-editor/:collection/:id`
- "Edit with Puck" buttons appear in the collection list view
- The editor runs inside Payload's admin UI with full navigation
- API endpoints are handled automatically via Payload's endpoint system

### Adding to Existing Projects

> **⚠️ Important:** If you're adding Puck to a project with existing frontend routes, you must update those routes to render Puck content.

When adding Puck to an existing Payload project:

1. ✅ Add the plugin to `payload.config.ts`
2. ✅ Add `PuckConfigProvider` to your admin layout
3. ⚠️ **Update your frontend page templates** to render `puckData`

Without step 3, Puck pages will render blank because your existing routes only look for legacy block fields like `layout` or `hero`.

**Option A: Hybrid Rendering (recommended)**

Use `HybridPageRenderer` to render Puck pages. For new projects, this is all you need:

```typescript
import { HybridPageRenderer } from '@delmaredigital/payload-puck/render'
import { baseConfig } from '@delmaredigital/payload-puck/config'

export default async function Page({ params }) {
const page = await getPage(params.slug)
return
}
```

If you're migrating an existing site with legacy Payload blocks, provide a `legacyRenderer`:

```typescript
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(params.slug)

return (
}
/>
)
}
```

**Option B: Manual Detection**

Add conditional logic to check `editorVersion`:

```typescript
// Check if page was created with Puck
const isPuckPage = page.editorVersion === 'puck' && page.puckData?.content?.length > 0

if (isPuckPage) {
return
}

// Fall back to legacy rendering
return
```

**Option C: Custom Components**

If you have custom Puck components (not just the built-in ones), create a client wrapper:

```typescript
// components/PuckPageRenderer.tsx
'use client'

import { Render } from '@puckeditor/core'
import { myCustomConfig } from '@/puck/config'

export function PuckPageRenderer({ data }) {
return
}
```

Then use this wrapper in your page template instead of `PageRenderer`.

---

## Styling Setup

### Tailwind Typography (Required)

> Required only if using the RichText component.

The RichText component uses `@tailwindcss/typography`:

```bash
pnpm add @tailwindcss/typography
```

**Tailwind v4:**
```css
@import "tailwindcss";
@plugin "@tailwindcss/typography";
```

**Tailwind v3:**
```javascript
// tailwind.config.js
module.exports = {
plugins: [require('@tailwindcss/typography')],
}
```

### Package Scanning (Required)

> Required if your project uses Tailwind CSS. Ensures component classes are included in your build.

Tell Tailwind to scan the plugin's components:

**Tailwind v4:**
```css
/* Adjust path relative to your CSS file */
@source "../node_modules/@delmaredigital/payload-puck";
```

**Tailwind v3:**
```javascript
// tailwind.config.js
module.exports = {
content: [
'./src/**/*.{js,ts,jsx,tsx}',
'./node_modules/@delmaredigital/payload-puck/**/*.{js,mjs,jsx,tsx}',
],
}
```

### Theme CSS Variables (Optional)

> Optional - the plugin includes sensible defaults. Define these only to customize colors in rendered content (links, borders, etc).

The plugin uses [shadcn/ui](https://ui.shadcn.com)-style CSS variables. If you don't use shadcn/ui and want to customize colors, define these in your CSS:

```css
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
```

---

## Core Concepts

### Server vs Client Configuration

The plugin provides two configurations for React Server Components:

| Config | Import | Use Case |
|--------|--------|----------|
| `baseConfig` | `@delmaredigital/payload-puck/config` | Server-safe rendering with `PageRenderer` |
| `editorConfig` | `@delmaredigital/payload-puck/config/editor` | Client-side editing with full interactivity |

```typescript
// Server component - use baseConfig
import { baseConfig } from '@delmaredigital/payload-puck/config'

// PuckConfigProvider - use editorConfig
import { editorConfig } from '@delmaredigital/payload-puck/config/editor'

```

### Draft System

The editor uses Payload's native draft system. The plugin automatically enables drafts on the pages collection. You can also enable it manually:

```typescript
{
slug: 'pages',
versions: {
drafts: true,
},
}
```

The editor header provides:
- **Save** - Saves as draft without publishing
- **Publish** - Publishes the page (sets `_status: 'published'`)
- **Unpublish** - Reverts a published page to draft status (appears only when published)

---

## Components

### Layout

| Component | Description |
|-----------|-------------|
| **Container** | Content wrapper with max-width and background |
| **Flex** | Flexible box layout with direction and alignment |
| **Grid** | CSS Grid layout with responsive columns |
| **Section** | Two-layer: full-bleed section + constrained content area |
| **Spacer** | Vertical/horizontal spacing element |
| **Template** | Save/load reusable component arrangements |

### Typography

| Component | Description |
|-----------|-------------|
| **Heading** | H1-H6 headings with size and alignment |
| **Text** | Paragraph text with styling options |
| **RichText** | Puck's native richtext editor with enhancements: font sizes, text colors with opacity, highlights, superscript/subscript, and inline editing on canvas |

### Media & Interactive

| Component | Description |
|-----------|-------------|
| **Image** | Responsive image with alt text |
| **Button** | Styled button/link with variants |
| **Card** | Content card with optional image |
| **Divider** | Horizontal rule with styles |
| **Accordion** | Expandable content sections (first item opens by default) |

### Semantic HTML Elements

Layout components (Section, Flex, Container, Grid) support semantic HTML output for better SEO and accessibility:

| Component | Available Elements |
|-----------|-------------------|
| **Section** | `section`, `article`, `aside`, `nav`, `header`, `footer`, `main`, `div` |
| **Flex** | `div`, `nav`, `ul`, `ol`, `aside`, `section` |
| **Container** | `div`, `article`, `aside`, `section` |
| **Grid** | `div`, `ul`, `ol` |

Select the appropriate HTML element in the component's sidebar to output semantic markup.

### Responsive Controls

Layout components support per-breakpoint customization:
- **Dimensions** - Width, max-width, height constraints
- **Padding/Margin** - Spacing per breakpoint
- **Visibility** - Show/hide at specific breakpoints
- **Viewport Preview** - Mobile, Tablet, Desktop, and Full Width options

---

## Custom Fields

All fields are imported from `@delmaredigital/payload-puck/fields`.

### Field Reference

| Field | Description |
|-------|-------------|
| **MediaField** | Payload media library integration |
| **RichTextField** | Puck's native richtext with enhancements (colors, font sizes, highlights) |
| **ColorPickerField** | Color picker with opacity and presets |
| **BackgroundField** | Solid colors, gradients, images |
| **PaddingField / MarginField** | Visual spacing editors |
| **BorderField** | Border width, style, color, radius |
| **DimensionsField** | Width/height with constraints |
| **AlignmentField** | Text alignment (left, center, right) |
| **ContentAlignmentField** | Visual 3x3 grid selector for positioning (d-pad style) |
| **SizeField** | Preset sizes (sm, default, lg) with custom mode |
| **AnimationField** | Entrance animations |
| **ResponsiveVisibilityField** | Show/hide per breakpoint |
| **FolderPickerField** | Hierarchical folder selection (page-tree) |
| **PageSegmentField** | URL segment with slugification (page-tree) |
| **SlugPreviewField** | Read-only computed slug (page-tree) |

### Usage Example

```typescript
import { createMediaField, createBackgroundField, backgroundValueToCSS } from '@delmaredigital/payload-puck/fields'

const HeroConfig = {
fields: {
image: createMediaField({ label: 'Background Image' }),
background: createBackgroundField({ label: 'Overlay' }),
},
render: ({ image, background }) => (

{/* content */}

),
}
```

### CSS Helper Functions

```typescript
import {
backgroundValueToCSS,
dimensionsValueToCSS,
animationValueToCSS,
visibilityValueToCSS,
alignmentToFlexCSS,
alignmentToGridCSS,
sizeValueToCSS,
getSizeClasses,
} from '@delmaredigital/payload-puck/fields'
```

### ContentAlignmentField Example

The ContentAlignmentField provides a visual 3x3 grid selector for content positioning:

```typescript
import {
createContentAlignmentField,
alignmentToFlexCSS,
alignmentToGridCSS,
} from '@delmaredigital/payload-puck/fields'

const BannerConfig = {
fields: {
contentPosition: createContentAlignmentField({ label: 'Content Position' }),
},
render: ({ contentPosition }) => (


Positioned content


),
}
```

Helper functions:
- `alignmentToFlexCSS()` - For Flexbox containers (`justify-content` + `align-items`)
- `alignmentToGridCSS()` - For Grid containers (`justify-content` + `align-content`)
- `alignmentToPlaceSelfCSS()` - For individual grid items (`place-self`)
- `alignmentToTailwind()` - Returns Tailwind classes (`justify-* items-*`)

---

## Building Custom Components

The plugin exports individual component configs and field factories for building custom Puck configurations.

### Cherry-Picking Components

Import only the components you need:

```typescript
import {
SectionConfig,
HeadingConfig,
TextConfig,
ImageConfig,
ButtonConfig,
} from '@delmaredigital/payload-puck/components'

export const puckConfig: Config = {
components: {
Section: SectionConfig,
Heading: HeadingConfig,
Text: TextConfig,
Image: ImageConfig,
Button: ButtonConfig,
},
categories: {
layout: { components: ['Section'] },
content: { components: ['Heading', 'Text', 'Image', 'Button'] },
},
}
```

### Using Field Factories

Build custom components with pre-built fields:

```typescript
import type { ComponentConfig } from '@puckeditor/core'
import {
createMediaField,
createBackgroundField,
createPaddingField,
backgroundValueToCSS,
paddingValueToCSS,
} from '@delmaredigital/payload-puck/fields'

export const HeroConfig: ComponentConfig = {
label: 'Hero',
fields: {
image: createMediaField({ label: 'Background Image' }),
overlay: createBackgroundField({ label: 'Overlay' }),
padding: createPaddingField({ label: 'Padding' }),
},
defaultProps: {
image: null,
overlay: null,
padding: { top: 80, bottom: 80, left: 24, right: 24, unit: 'px', linked: false },
},
render: ({ image, overlay, padding }) => (

{/* Hero content */}

),
}
```

### Server vs Editor Variants

For `PageRenderer` (frontend), components need server-safe configs without React hooks:

```typescript
// Import server variants for PageRenderer
import {
SectionServerConfig,
HeadingServerConfig,
TextServerConfig,
} from '@delmaredigital/payload-puck/components'

```

For custom components, create two files:
- `MyComponent.tsx` - Full editor version with fields and interactivity
- `MyComponent.server.tsx` - Server-safe version (no hooks, no 'use client')

### Extending Built-in Configs

Use `extendConfig()` to add custom components:

```typescript
import { extendConfig, fullConfig } from '@delmaredigital/payload-puck/config/editor'
import { HeroConfig } from './components/Hero'

export const puckConfig = extendConfig({
base: fullConfig,
components: {
Hero: HeroConfig,
},
categories: {
custom: { title: 'Custom', components: ['Hero'] },
},
})
```

> **Note:** Use `fullConfig` from `/config/editor` for extending the editor. For server-side rendering, use `baseConfig` from `/config`.

### Using Custom Config with Provider

After creating your custom config, pass it to `PuckConfigProvider`:

```typescript
// components/admin/PuckProvider.tsx
'use client'
import { PuckConfigProvider } from '@delmaredigital/payload-puck/client'
import { puckConfig } from '@/puck/config.editor'
import { siteLayouts } from '@/lib/puck-layouts'

export default function PuckProvider({ children }: { children: React.ReactNode }) {
return (

{children}

)
}
```

**For Payload admin**, register the provider in your Payload config:

```typescript
// payload.config.ts
export default buildConfig({
admin: {
components: {
providers: ['@/components/admin/PuckProvider'],
},
},
// ...
})
```

This is the recommended pattern for Payload apps. The provider wraps only the admin UI, keeping your frontend layout separate.

### Available Field Factories

| Factory | Description |
|---------|-------------|
| `createMediaField()` | Payload media library picker |
| `createBackgroundField()` | Solid, gradient, or image backgrounds |
| `createColorPickerField()` | Color picker with opacity |
| `createPaddingField()` | Visual padding editor |
| `createMarginField()` | Visual margin editor |
| `createBorderField()` | Border styling |
| `createDimensionsField()` | Width/height constraints |
| `createAnimationField()` | Entrance animations |
| `createAlignmentField()` | Text alignment (left, center, right) |
| `createContentAlignmentField()` | Visual 3x3 grid positioning selector |
| `createSizeField()` | Size presets with custom mode |
| `createRichTextField()` | Puck's native richtext with colors, font sizes, highlights |
| `createResponsiveVisibilityField()` | Show/hide per breakpoint |

### CSS Helper Functions

Convert field values to CSS:

```typescript
import {
backgroundValueToCSS,
paddingValueToCSS,
marginValueToCSS,
borderValueToCSS,
dimensionsValueToCSS,
colorValueToCSS,
alignmentToFlexCSS,
alignmentToGridCSS,
sizeValueToCSS,
} from '@delmaredigital/payload-puck/fields'

const style = {
background: backgroundValueToCSS(props.background),
padding: paddingValueToCSS(props.padding),
...dimensionsValueToCSS(props.dimensions),
...alignmentToFlexCSS(props.contentAlignment),
...sizeValueToCSS(props.size),
}
```

---

## Theming

Customize button styles, color presets, and focus rings:

```typescript
import { PageRenderer } from '@delmaredigital/payload-puck/render'
import { ThemeProvider } from '@delmaredigital/payload-puck/theme'

```

Access theme values in custom components with `useTheme()`:

```typescript
import { useTheme } from '@delmaredigital/payload-puck/theme'

function CustomButton({ variant }) {
const theme = useTheme()
const classes = theme.buttonVariants[variant]?.classes
return ...
}
```

---

## Layouts

Define page layouts with headers, footers, and styling:

```typescript
// lib/puck-layouts.ts
import type { LayoutDefinition } from '@delmaredigital/payload-puck/layouts'
import { SiteHeader } from '@/components/header'
import { SiteFooter } from '@/components/footer'

export const siteLayouts: LayoutDefinition[] = [
{
value: 'default',
label: 'Default',
description: 'Standard page with header and footer',
maxWidth: '1200px',
header: SiteHeader,
footer: SiteFooter,
stickyHeaderHeight: 80,
},
{
value: 'landing',
label: 'Landing',
description: 'Full-width landing page',
fullWidth: true,
},
]
```

Pass layouts to the `PuckConfigProvider`:

```typescript

{children}

```

And use them with `PageRenderer`:

```typescript
import { LayoutWrapper } from '@delmaredigital/payload-puck/layouts'

const layout = siteLayouts.find(l => l.value === page.puckData?.root?.props?.pageLayout)

```

### Avoiding Double Headers/Footers

When your host app already provides a global header/footer via its root layout (e.g., Next.js `layout.tsx`), use `createRenderLayouts()` to strip them from Puck layouts:

```typescript
import { HybridPageRenderer, createRenderLayouts } from '@delmaredigital/payload-puck/render'
import { siteLayouts } from '@/lib/puck-layouts' // layouts with header/footer for editor

// Strip header/footer for rendering (host app layout provides them)
const renderLayouts = createRenderLayouts(siteLayouts)

export function PageRenderer({ page }) {
const layout = renderLayouts.find(l => l.value === page.puckData?.root?.props?.pageLayout)

return (



)
}
```

This pattern keeps header/footer in your editor layouts for realistic preview, but avoids double headers when rendering.

---

## Dark Mode Support

The Puck editor automatically detects PayloadCMS dark mode and applies CSS overrides to ensure visibility. It also provides a preview toggle to test how pages look in both light and dark modes.

### How It Works

1. **Editor UI**: Automatically detects dark mode via `.dark` class (PayloadCMS) or `prefers-color-scheme` (OS preference), then injects Puck CSS variable overrides
2. **Preview Iframe**: A sun/moon toggle lets you switch the preview content between light and dark modes independently from the editor UI

### Configuration

Dark mode is enabled by default. You can customize via props on `PuckEditor`:

```typescript

```

### Using Components Directly

For custom editor implementations:

```typescript
import {
DarkModeStyles,
PreviewModeToggle,
useDarkMode,
} from '@delmaredigital/payload-puck/editor'

function CustomEditor() {
const { isDarkMode, source } = useDarkMode()
const [previewDark, setPreviewDark] = useState(false)

return (
<>
{/* Inject dark mode CSS overrides when detected */}

{/* Toggle for preview iframe */}


>
)
}
```

### Detecting Theme in Puck Components

If your Puck components need to dynamically adjust JavaScript-controlled styles based on the preview theme (not just CSS), use the `usePuckPreviewTheme()` hook:

```typescript
import { usePuckPreviewTheme } from '@delmaredigital/payload-puck/editor'
import { useEffect, useState } from 'react'

function useDetectTheme() {
const puckTheme = usePuckPreviewTheme()

// For frontend (non-editor), read from DOM
const [domTheme, setDomTheme] = useState(() =>
typeof document !== 'undefined'
? document.documentElement.getAttribute('data-theme') === 'dark'
: false
)

useEffect(() => {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.attributeName === 'data-theme') {
setDomTheme(document.documentElement.getAttribute('data-theme') === 'dark')
}
}
})
observer.observe(document.documentElement, { attributes: true })
return () => observer.disconnect()
}, [])

// In editor: use context. On frontend: use DOM.
return puckTheme !== null ? puckTheme : domTheme
}
```

**Why this is needed:** CSS dark mode variants (like Tailwind's `dark:` classes) work automatically via the `data-theme` attribute. However, if you need to conditionally render different JavaScript values (like overlay colors), those won't update reactively when the preview toggle changes. The context provides reactive updates.

---

## Page-Tree Integration

When `@delmaredigital/payload-page-tree` is detected, the plugin automatically adds folder management to the Puck sidebar.

> **⚠️ Plugin Order:** When using both plugins with `autoGenerateCollection: true`, Puck must run BEFORE page-tree. See [Plugin Order](#plugin-order).

### How It Works

The plugin checks if your collection has a `pageSegment` field (page-tree's signature). When detected:

1. **Folder Picker** - Select a folder from the hierarchy
2. **Page Segment** - Edit the page's URL segment
3. **Slug Preview** - See the computed slug (folder path + segment)

### Plugin Configuration

```typescript
createPuckPlugin({
// Auto-detect (default)
pageTreeIntegration: undefined,

// Explicitly enable with custom config
pageTreeIntegration: {
folderSlug: 'payload-folders',
pageSegmentFieldName: 'pageSegment',
},

// Explicitly disable
pageTreeIntegration: false,
})
```

### Custom Editor UI

For custom editor implementations outside Payload admin, use the `hasPageTree` prop:

```typescript
import { PuckEditor } from '@delmaredigital/payload-puck/client'
import { editorConfig } from '@delmaredigital/payload-puck/config/editor'

```

### Performance

Detection is instant - it reads the in-memory collection config, no database queries.

---

## Hybrid Integration

Add Puck to existing collections with legacy blocks.

### Automatic (Recommended)

If you already have a `pages` collection, the plugin adds only the Puck-specific fields:

```typescript
// 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' }),
],
})
```

The `editorVersion` field auto-detects whether pages use legacy blocks or Puck.

### Manual with `getPuckCollectionConfig()` (Recommended)

When you need the `isHomepage` field, use `getPuckCollectionConfig()` which returns both fields AND hooks. This ensures the homepage uniqueness validation is included:

```typescript
import { getPuckCollectionConfig } from '@delmaredigital/payload-puck'

const { fields: puckFields, hooks: puckHooks } = getPuckCollectionConfig({
includeSEO: true,
includeEditorVersion: true,
includePageLayout: true,
includeIsHomepage: true, // Includes uniqueness hook automatically
})

export const Pages: CollectionConfig = {
slug: 'pages',
hooks: {
beforeChange: [
...(puckHooks.beforeChange ?? []),
// Your other beforeChange hooks...
],
afterChange: [
// Your afterChange hooks...
],
},
fields: [
{ name: 'title', type: 'text' },
{ name: 'layout', type: 'blocks', blocks: [...] },
...puckFields,
],
}
```

### Manual with `getPuckFields()` (Fields Only)

If you don't need `isHomepage` or want to configure hooks manually:

```typescript
import { getPuckFields, createIsHomepageUniqueHook } from '@delmaredigital/payload-puck'

export const Pages: CollectionConfig = {
slug: 'pages',
hooks: {
// Required if using includeIsHomepage: true
beforeChange: [createIsHomepageUniqueHook()],
},
fields: [
{ name: 'title', type: 'text' },
{ name: 'layout', type: 'blocks', blocks: [...] },
...getPuckFields({
includeSEO: true,
includeEditorVersion: true,
includePageLayout: true,
includeIsHomepage: true, // Note: requires hook above for uniqueness
}),
],
}
```

> **Note:** The `isHomepage` field allows marking one page as the homepage. The `createIsHomepageUniqueHook()` ensures only one page can be marked as homepage at a time, prompting users to swap if a homepage already exists.

### Rendering Hybrid Pages

```typescript
import { HybridPageRenderer } from '@delmaredigital/payload-puck/render'
import { LegacyBlockRenderer } from '@/components/LegacyBlockRenderer'

}
/>
```

---

## AI Integration

> **Early Preview:** While Puck's AI features are powerful, this plugin's implementation is still in early stages and under active development. Expect changes as we refine the integration.

The plugin integrates with [Puck AI](https://puckeditor.com/docs/integrating-puck/ai) to enable AI-assisted page generation. Users can describe what they want in natural language, and the AI builds complete page layouts using your components.

### Requirements

- `PUCK_API_KEY` environment variable (from [Puck Cloud](https://puckeditor.com))
- AI features require `@puckeditor/plugin-ai` and `@puckeditor/cloud-client` (bundled with the plugin)

### Quick Start

Enable AI in your plugin configuration:

```typescript
createPuckPlugin({
pagesCollection: 'pages',
ai: {
enabled: true,
context: 'We are Acme Corp, a B2B SaaS company. Use professional language.',
},
})
```

This automatically:
- Registers the AI chat endpoint at `/api/puck/ai`
- Adds the AI chat plugin to the editor
- Applies comprehensive component instructions for better generation quality

### Dynamic Business Context

Instead of hardcoding context in your config, you can manage it through Payload admin:

```typescript
createPuckPlugin({
ai: {
enabled: true,
contextCollection: true, // Creates puck-ai-context collection
},
})
```

This creates a `puck-ai-context` collection where you can add entries for:
- **Brand Guidelines** - Colors, fonts, brand voice
- **Tone of Voice** - How to communicate
- **Product Information** - What you sell/offer
- **Industry Context** - Your market and audience
- **Technical Requirements** - Specific constraints
- **Page Patterns** - Common layout structures

Context entries can be enabled/disabled and ordered. The AI receives all enabled entries sorted by order.

### Context Editor Plugin

When `contextCollection: true`, a "Context" panel appears in the Puck plugin rail. Users can view, create, edit, and toggle context entries directly in the editor without visiting Payload admin.

### Prompt Management

Store reusable prompts in Payload:

```typescript
createPuckPlugin({
ai: {
enabled: true,
promptsCollection: true, // Creates puck-ai-prompts collection
examplePrompts: [
{ label: 'Landing page', prompt: 'Create a landing page for...' },
],
},
})
```

Prompts from the collection appear in the AI chat interface. A "Prompts" panel in the plugin rail allows in-editor prompt management.

### Custom Tools

Enable the AI to query your data:

```typescript
import { z } from 'zod'

createPuckPlugin({
ai: {
enabled: true,
tools: {
getProducts: {
description: 'Get products from the database',
inputSchema: z.object({ category: z.string() }),
execute: async ({ category }, { payload }) => {
return await payload.find({
collection: 'products',
where: { category: { equals: category } },
})
},
},
},
},
})
```

Tools receive a context object with the Payload instance and authenticated user.

### AI Configuration Options

| Option | Default | Description |
|--------|---------|-------------|
| `enabled` | `false` | Enable AI features |
| `context` | `undefined` | Static system context for the AI |
| `contextCollection` | `false` | Create `puck-ai-context` collection for dynamic context |
| `promptsCollection` | `false` | Create `puck-ai-prompts` collection for reusable prompts |
| `examplePrompts` | `[]` | Static example prompts for the chat interface |
| `tools` | `undefined` | Custom tools for AI to query your system |
| `componentInstructions` | `undefined` | Override default component AI instructions |

### Component Instructions

The plugin includes comprehensive instructions for all built-in components, teaching the AI:
- Correct field names and values
- Component composition patterns
- Page structure best practices (Hero → Features → CTA flow)
- Semantic HTML usage

To customize or extend:

```typescript
createPuckPlugin({
ai: {
enabled: true,
componentInstructions: {
Heading: {
ai: { instructions: 'Use our brand voice: professional but approachable' },
fields: {
text: { ai: { instructions: 'Keep under 8 words' } },
},
},
},
},
})
```

### Standalone API Routes

For custom implementations outside the plugin:

```typescript
// app/api/puck/[...all]/route.ts
import { createPuckAiApiRoutes } from '@delmaredigital/payload-puck/ai'
import config from '@payload-config'

export const POST = createPuckAiApiRoutes({
payloadConfig: config,
auth: {
authenticate: async (request) => {
// Your auth implementation
return { user: { id: '...' } }
},
},
ai: {
context: 'Your business context...',
},
})
```

### AI Exports

```typescript
import {
// Plugins
createAiPlugin,
createPromptEditorPlugin,
createContextEditorPlugin,

// Hooks
useAiPrompts,
useAiContext,

// Config utilities
injectAiConfig,
comprehensiveComponentAiConfig,
pagePatternSystemContext,

// API routes
createPuckAiApiRoutes,
createAiGenerate,
} from '@delmaredigital/payload-puck/ai'
```

---

## Plugin Order

When using `autoGenerateCollection: true` (the default) with `@delmaredigital/payload-page-tree`, **plugin order matters**.

### The Issue

The page-tree plugin validates configured collections when it initializes. If Puck hasn't created the collection yet, page-tree won't see it and will skip adding its fields (folder relationships, slug generation, etc.).

### Correct Order

```typescript
// ✅ CORRECT: Puck creates the collection before page-tree runs
export const plugins = [
createPuckPlugin({ pagesCollection: 'pages' }), // Creates Pages first
pageTreePlugin({ collections: ['pages'] }), // Now sees Pages
]

// ❌ WRONG: page-tree runs before Pages exists
export const plugins = [
pageTreePlugin({ collections: ['pages'] }), // Pages doesn't exist!
createPuckPlugin({ pagesCollection: 'pages' }), // Creates Pages too late
]
```

### When Order Doesn't Matter

If you define your collection manually (with `autoGenerateCollection: false`), order doesn't matter because the collection already exists in your config:

```typescript
export default buildConfig({
collections: [Pages], // Collection exists before plugins run
plugins: [
pageTreePlugin({ collections: ['pages'] }),
createPuckPlugin({ pagesCollection: 'pages', autoGenerateCollection: false }),
],
})
```

See also: [payload-page-tree Plugin Order documentation](https://github.com/delmaredigital/payload-page-tree#plugin-order-critical)

---

## Advanced Configuration

### Plugin Options

| Option | Default | Description |
|--------|---------|-------------|
| `pagesCollection` | `'pages'` | Collection slug to use for pages |
| `autoGenerateCollection` | `true` | Create the collection if it doesn't exist, or add Puck fields to existing (see [Plugin Order](#plugin-order)) |
| `enableEndpoints` | `true` | Register API endpoints at `/api/puck/:collection` for the editor |
| `enableAdminView` | `true` | Register the Puck editor view in Payload admin |
| `adminViewPath` | `'/puck-editor'` | Path for the editor (full path: `/admin/puck-editor/:collection/:id`) |
| `pageTreeIntegration` | auto-detect | Integration with `@delmaredigital/payload-page-tree` |
| `layouts` | `undefined` | Layout definitions for page templates |
| `editorStylesheet` | `undefined` | Path to CSS file for editor iframe styling (e.g., `'src/app/globals.css'`) |
| `editorStylesheetCompiled` | `undefined` | Path to pre-compiled CSS for production (e.g., `'/puck-editor-styles.css'`) |
| `editorStylesheetUrls` | `[]` | Additional stylesheet URLs for the editor (e.g., Google Fonts) |
| `previewUrl` | `undefined` | URL for "View" button - string or function receiving page data |

```typescript
createPuckPlugin({
pagesCollection: 'pages',
autoGenerateCollection: true,
enableEndpoints: true,
enableAdminView: true,
adminViewPath: '/puck-editor',
pageTreeIntegration: undefined, // auto-detects

// Collection overrides (merged with generated collection)
collectionOverrides: {
admin: {
defaultColumns: ['title', 'slug', 'updatedAt'],
},
},

// Access control
access: {
read: () => true,
create: ({ req }) => !!req.user,
update: ({ req }) => !!req.user,
delete: ({ req }) => !!req.user,
},
})
```

### Preview URL (View Button)

The "View" button in the editor opens the published page in a new tab. By default, it navigates to `/{slug}` (or `/` for homepage). Use the `previewUrl` option to customize this behavior.

```typescript
// Simple static URL pattern
createPuckPlugin({
previewUrl: '/preview',
})

// Dynamic prefix based on page data
createPuckPlugin({
previewUrl: (page) => `/${page.slug || ''}`,
})

// Organization-scoped pages (multi-tenant)
// The function receives the full page document with relationships populated
createPuckPlugin({
previewUrl: (page) => {
const orgSlug = page.organization?.slug || 'default'
// Return a function that handles homepage vs regular pages
return (slug) => slug ? `/${orgSlug}/${slug}` : `/${orgSlug}`
},
})
```

When `previewUrl` is a function, the page document is fetched with `depth: 1` so relationship fields (like `organization`) are populated with their full data.

### Editor Stylesheet (Iframe Styling)

The Puck editor renders page content in an iframe. By default, this iframe doesn't have access to your frontend's CSS (Tailwind utilities, CSS variables, fonts). The `editorStylesheet` option solves this by compiling and serving your CSS.

#### Development (Runtime Compilation)

In development, CSS is compiled at runtime for hot reload support:

```typescript
createPuckPlugin({
pagesCollection: 'pages',
editorStylesheet: 'src/app/(frontend)/globals.css',
editorStylesheetUrls: [
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'
],
})
```

**How it works:**
1. You specify your CSS file path in the plugin config
2. The plugin creates an endpoint at `/api/puck/styles`
3. On first request, the CSS is compiled with PostCSS/Tailwind and cached
4. The iframe loads this compiled CSS

#### Production (Build-Time Compilation)

Runtime compilation fails on serverless platforms (Vercel, Netlify, etc.) because source CSS files aren't deployed—only compiled `.next` output is included. Use `withPuckCSS()` to compile CSS at build time:

**Step 1: Wrap your Next.js config**

```javascript
// next.config.js
import { withPuckCSS } from '@delmaredigital/payload-puck/next'
import { withPayload } from '@payloadcms/next/withPayload'

const nextConfig = {
// your config...
}

export default withPuckCSS({
cssInput: 'src/app/(frontend)/globals.css',
})(withPayload(nextConfig))
```

**Step 2: Add the compiled path to your plugin config**

```typescript
createPuckPlugin({
pagesCollection: 'pages',
editorStylesheet: 'src/app/(frontend)/globals.css', // For dev (runtime)
editorStylesheetCompiled: '/puck-editor-styles.css', // For prod (static)
editorStylesheetUrls: [
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'
],
})
```

**How it works:**
1. During `next build`, the wrapper compiles your CSS to `public/puck-editor-styles.css`
2. In production (`NODE_ENV=production`), the plugin serves the static file
3. In development, runtime compilation continues working for hot reload

**`withPuckCSS` options:**

| Option | Default | Description |
|--------|---------|-------------|
| `cssInput` | (required) | Path to source CSS file |
| `cssOutput` | `'puck-editor-styles.css'` | Output filename in `public/` |
| `skipInDev` | `true` | Skip compilation in development |

#### Requirements

- `postcss` must be installed in your project
- For Tailwind v4: `@tailwindcss/postcss`
- For Tailwind v3: `tailwindcss`

---

### Custom API Routes (Advanced)

The built-in endpoints handle most use cases. Only disable them if you need custom authentication or middleware.

If needed, three route factories are available:

| Factory | Route Pattern | Methods |
|---------|---------------|---------|
| `createPuckApiRoutes` | `/api/puck/[collection]` | GET (list), POST (create) |
| `createPuckApiRoutesWithId` | `/api/puck/[collection]/[id]` | GET, PATCH, DELETE |
| `createPuckApiRoutesVersions` | `/api/puck/[collection]/[id]/versions` | GET, POST (restore) |

See the JSDoc in `@delmaredigital/payload-puck/api` for usage examples.

---

## Export Reference

| Export Path | Description |
|-------------|-------------|
| `@delmaredigital/payload-puck` | Plugin creation, field utilities |
| `@delmaredigital/payload-puck/plugin` | `createPuckPlugin` |
| `@delmaredigital/payload-puck/config` | `baseConfig`, `createConfig()`, `extendConfig()` |
| `@delmaredigital/payload-puck/config/editor` | `editorConfig` for editing |
| `@delmaredigital/payload-puck/client` | `PuckEditor`, `PuckConfigProvider`, page-tree utilities |
| `@delmaredigital/payload-puck/editor` | `PuckEditor`, `HeaderActions`, editor hooks |
| `@delmaredigital/payload-puck/rsc` | `PuckEditorView` for Payload admin views |
| `@delmaredigital/payload-puck/render` | `PageRenderer`, `HybridPageRenderer` |
| `@delmaredigital/payload-puck/fields` | Custom Puck fields and CSS helpers |
| `@delmaredigital/payload-puck/components` | Component configs for custom configurations |
| `@delmaredigital/payload-puck/theme` | `ThemeProvider`, theme utilities |
| `@delmaredigital/payload-puck/layouts` | Layout definitions, `LayoutWrapper` |
| `@delmaredigital/payload-puck/api` | API route factories (for custom implementations) |
| `@delmaredigital/payload-puck/ai` | AI plugins, hooks, config utilities, API routes |
| `@delmaredigital/payload-puck/next` | `withPuckCSS` Next.js config wrapper for build-time CSS |
| `@delmaredigital/payload-puck/admin/client` | `EditWithPuckButton`, `EditWithPuckCell` |

---

## License

MIT