Loading...
┌─────────────────────────────────────────────┐
│ BLOCK DEFINITION (config.ts) │
├─────────────────────────────────────────────┤
│ │
│ export const MyBlock: Block = { │
│ slug: "my-block" ← Unique ID │
│ interfaceName: "Props" ← Type name │
│ fields: [ ← Data shape │
│ { name: "title", type: "text" } │
│ { name: "image", type: "upload" } │
│ ] │
│ } │
│ │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ PAYLOAD ADMIN UI │
├─────────────────────────────────────────────┤
│ (Editor adds/edits block data) │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ DATABASE (MongoDB) │
├─────────────────────────────────────────────┤
│ { blockType: "my-block", title: "...", ... }
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ API RESPONSE │
├─────────────────────────────────────────────┤
│ { blockType: "my-block", title: "...", ... }
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ RenderBlocks.tsx │
├─────────────────────────────────────────────┤
│ Maps blockType → Component │
│ Passes data as props │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Component.tsx │
├─────────────────────────────────────────────┤
│ │
│ export const MyBlockComponent = ({ │
│ title, image, ... │
│ }) => ( │
│ <div>Rendered HTML</div> │
│ ) │
│ │
└─────────────────────────────────────────────┘
📦 blocks/
│
├─ 📄 RenderBlocks.tsx ← Block routing/mapping
│
├─ 📂 CallToAction/ ← Example block
│ ├─ 📄 config.ts ← Payload definition
│ └─ 📄 Component.tsx ← React component
│
├─ 📂 Banner/
│ ├─ 📄 config.ts
│ └─ 📄 Component.tsx
│
├─ 📂 ShopBlocks/ ← Grouped blocks
│ ├─ 📄 index.ts ← Barrel export
│ ├─ 📂 ShopHero/
│ │ ├─ 📄 config.ts
│ │ └─ 📄 Component.tsx
│ ├─ 📂 ShopFilters/
│ ├─ 📂 ShopProductGrid/
│ └─ 📂 ShopSearch/
│
├─ 📂 ProductBlocks/
│ ├─ 📂 ProductDetails/
│ ├─ 📂 ProductGallery/
│ └─ 📂 RelatedProducts/
│
└─ 📂 [Other blocks]/
Step 1: CREATE
↓
├─ config.ts (Block definition)
└─ Component.tsx (React component)
Step 2: REGISTER
↓
└─ RenderBlocks.tsx:
const blockComponents = {
"my-block": MyBlockComponent ← Add here
}
Step 3: INTEGRATE
↓
├─ contentTabField.ts (for Pages)
│ blocks: [MyBlock, ...]
│
└─ globals/Shop/config.ts (for Shop)
blocks: [MyBlock, ...]
Step 4: GENERATE TYPES
↓
└─ bun run gen → payload-types.ts
export const MyBlock: Block = {
// 🔴 REQUIRED
slug: "unique-slug", // Used in blockType
// 🟡 OPTIONAL but recommended
interfaceName: "MyBlockProps", // For TypeScript
dbName: "my_block", // DB collection name
// 🟡 UI labels
labels: {
singular: "My Block",
plural: "My Blocks",
},
// 🔴 REQUIRED
fields: [/* array of fields */],
// 🟡 Grouping
admin: {
group: "Content", // Group in admin UI
},
};
// Text
{ name: "title", type: "text" }
{ name: "description", type: "textarea" }
// Rich text (with editor)
{
name: "content",
type: "richText",
editor: lexicalEditor()
}
// Upload
{
name: "image",
type: "upload",
relationTo: "media"
}
// Select
{
name: "style",
type: "select",
options: [
{ label: "Option 1", value: "opt1" },
{ label: "Option 2", value: "opt2" },
]
}
// Checkbox
{
name: "isActive",
type: "checkbox",
defaultValue: true
}
// Number
{
name: "count",
type: "number",
min: 0,
max: 100
}
// Array (repeating fields)
{
name: "items",
type: "array",
fields: [
{ name: "title", type: "text" },
{ name: "link", type: "text" },
]
}
// Group (nested fields)
{
name: "settings",
type: "group",
fields: [
{ name: "option1", type: "text" },
{ name: "option2", type: "number" },
]
}
// Relationship
{
name: "relatedPosts",
type: "relationship",
relationTo: "posts"
}
┌──────────────────────────────┐
│ PAGES Collection │
├──────────────────────────────┤
│ ├─ Metadata Tab │
│ ├─ Content Tab │
│ │ └─ Blocks Layout ← Uses │
│ │ contentTabField │
│ ├─ SEO Tab │
│ └─ Overlays Tab │
│ └─ Blocks ← Custom set │
└──────────────────────────────┘
┌──────────────────────────────┐
│ SHOP GLOBAL (Settings) │
├──────────────────────────────┤
│ ├─ Shop Home Tab │
│ │ └─ Blocks Layout ← Custom │
│ │ (ShopHero, Filters, │
│ │ ProductGrid, etc.) │
│ └─ Product Pages Tab │
│ └─ Blocks Layout ← Custom │
│ (ProductDetails, │
│ Gallery, etc.) │
└──────────────────────────────┘
┌──────────────────────────────┐
│ PRODUCTS Collection │
├──────────────────────────────┤
│ ├─ Metadata Tab │
│ ├─ Content Tab │
│ │ └─ Blocks Layout ← Uses │
│ │ contentTabField │
│ └─ SEO Tab │
└──────────────────────────────┘
import type { FC } from "react";
import type { MyBlockProps } from "@/payload-types";
interface MyBlockComponentProps extends MyBlockProps {
disableInnerContainer?: boolean;
tenantSlug?: string;
}
export const MyBlockComponent: FC<MyBlockComponentProps> = ({
id, // ← Always from block data
title, // ← From config fields
content,
disableInnerContainer, // ← From RenderBlocks
tenantSlug,
// ... other props
}) => {
// Handle missing data
if (!title) return null;
return (
<div
key={`my-block-${id}`} // ← Include block ID in key
className={cn("my-styles", {
"no-container": disableInnerContainer,
})}
>
{/* Render content */}
</div>
);
};
{
name: "showAdvanced",
type: "checkbox",
defaultValue: false,
label: "Show Advanced Options",
}
{
name: "advancedOption1",
type: "text",
label: "Advanced Setting 1",
admin: {
// Only show when showAdvanced is true
condition: (_, siblingData) => siblingData?.showAdvanced,
},
}
{
name: "advancedOption2",
type: "text",
label: "Advanced Setting 2",
admin: {
condition: (_, siblingData) => siblingData?.showAdvanced,
},
}
// 1. Import block config and component
import { MyBlock } from "@/blocks/MyBlock/config";
import { MyBlockComponent } from "@/blocks/MyBlock/Component";
// 2. Add to blockComponents object
const blockComponents = {
"my-block": MyBlockComponent, // ← slug as key
// ... other blocks
};
// 3. When rendering:
// For each block in the page/product data:
// - Get blockType from block data
// - Look up component in blockComponents
// - Pass block data as props
// - Handle unknown block types gracefully
// ❌ WRONG
import { MyBlockComponent } from "@/blocks/MyBlock/Component";
// But forgot to add to blockComponents...
// ✅ CORRECT
const blockComponents = {
"my-block": MyBlockComponent,
};
// ❌ WRONG
return <div>Block content</div>;
// ✅ CORRECT
return <div key={`my-block-${id}`}>Block content</div>;
// ❌ WRONG - Different slugs!
export const MyBlock: Block = {
slug: "my-block", // config says "my-block"
// ...
};
const blockComponents = {
"my_block": MyBlockComponent, // but mapped as "my_block"
};
// ✅ CORRECT
export const MyBlock: Block = {
slug: "my-block",
};
const blockComponents = {
"my-block": MyBlockComponent, // matches!
};
// ❌ WRONG - Will crash if title missing
export const MyBlockComponent: FC<MyBlockProps> = ({ title }) => {
return <h1>{title}</h1>; // Error if title undefined
};
// ✅ CORRECT
export const MyBlockComponent: FC<MyBlockProps> = ({ title }) => {
if (!title) return null;
return <h1>{title}</h1>;
};
// ❌ WRONG - siblingData structure unclear
admin: {
condition: (_, siblingData) => siblingData.showAdvanced,
}
// ✅ CORRECT - Clear intent
admin: {
condition: (_, siblingData) => siblingData?.showAdvanced === true,
}
bun run lint:check - no errorsbun run gen - types generatedTell me:
I'll handle the rest! 🚀