How Blocks Work
Blocks are Puck visual components that merchants can add and customize in the editor. Each block defines:
- Editor configuration - UI controls for customization
- Render function - How to display the block on the storefront
- Category - Where it appears in the editor sidebar
Simple Block Example
// blocks/hero.puck.tsx
import { type ComponentConfig } from '@puckeditor/core';
interface HeroProps {
title: string;
subtitle: string;
backgroundImage?: string;
}
export const category = 'Marketing';
export const config: ComponentConfig<HeroProps> = {
label: 'Hero Banner',
fields: {
title: {
type: 'text',
label: 'Title',
},
subtitle: {
type: 'textarea',
label: 'Subtitle',
},
backgroundImage: {
type: 'text',
label: 'Background Image URL (optional)',
},
},
defaultProps: {
title: 'Welcome to our store',
subtitle: 'Discover amazing products',
},
render: ({ title, subtitle, backgroundImage }) => (
<div
className="relative py-24 text-center"
style={{
backgroundImage: backgroundImage ? `url(${backgroundImage})` : undefined,
backgroundSize: 'cover',
}}
>
<h1 className="text-4xl font-bold">{title}</h1>
<p className="mt-2 text-xl text-gray-600">{subtitle}</p>
</div>
),
};Complex Block with Editor & Render Separation
For blocks that need interactive features in the editor (like product selection), split them into two files:
blocks/product-grid/
├── product-grid.edit.puck.tsx # Editor mode with interactivity
├── product-grid.render.puck.tsx # Render mode with data fetching
├── shared.ts # Shared utilities
└── index.ts # ExportsEdit Mode (*.edit.puck.tsx)
Edit mode runs in the editor UI ('use client' component):
- Runs in the editor UI
- Can have interactive features (pickers, dialogs)
- Stores lightweight data (IDs, not full objects)
- Has access to editor context
'use client';
import { type ComponentConfig } from '@puckeditor/core';
interface ProductGridProps {
selectedProductIds?: number[]; // Lightweight data
selectedProducts?: Product[]; // For preview
title: string;
}
export const config: ComponentConfig<ProductGridProps> = {
label: 'Product Grid',
fields: {
selectedProductIds: {
type: 'custom',
render: ({ value, onChange }) => <ProductPickerField value={value} onChange={onChange} />,
},
title: {
type: 'text',
label: 'Section Title',
},
},
defaultProps: {
selectedProductIds: [],
title: 'Featured Products',
},
render: ({ selectedProductIds, title }) => {
return <ProductGridPreview ids={selectedProductIds} title={title} />;
},
};Render Mode (*.render.puck.tsx)
Render mode runs on the published storefront (Server component):
- Runs on the published storefront
- Fetches fresh data from the API
- Uses Suspense for streaming
- No interactive features
// Server component by default
import { Suspense } from 'react';
import { type ComponentConfig } from '@puckeditor/core';
interface ProductGridProps {
selectedProductIds?: number[];
title: string;
}
export const config: ComponentConfig<ProductGridProps> = {
label: 'Product Grid',
defaultProps: {
selectedProductIds: [],
title: 'Featured Products',
},
render: ({ selectedProductIds, title }) => (
<Suspense fallback={<ProductGridSkeleton />}>
<ProductGridAsync ids={selectedProductIds} title={title} />
</Suspense>
),
};Block Field Types
The fields object in block config defines customizable properties:
fields: {
// Text input
title: {
type: 'text',
label: 'Title',
},
// Multi-line text
description: {
type: 'textarea',
label: 'Description',
},
// Select dropdown
alignment: {
type: 'radio',
label: 'Text Alignment',
options: [
{ label: 'Left', value: 'left' },
{ label: 'Center', value: 'center' },
{ label: 'Right', value: 'right' },
],
},
// Number input
columns: {
type: 'number',
label: 'Grid Columns',
},
// Custom component field
products: {
type: 'custom',
render: ({ value, onChange }) => (
<CustomProductPicker value={value} onChange={onChange} />
),
},
}Auto-Generated Puck Config
The .storefront/ directory contains auto-generated Puck editor configurations:
.storefront/puck.edit.config.tsx- Editor configuration (all blocks in edit mode).storefront/puck.render.config.tsx- Render configuration (all blocks in render mode)
⚠️ Never edit these files manually. They’re regenerated by the build process.
To regenerate them:
pnpm dev # Runs finqu storefront dev --components=./blocksThe CLI scans the blocks/ directory for files matching *.puck.tsx and generates the configs automatically.
Building Custom Blocks
Block Checklist
- Create file in
blocks/matching pattern*.puck.tsx - Export
categorystring - Export
config: ComponentConfig<Props> - Define
fieldsfor editor controls - Provide
defaultProps - Implement
renderfunction - Rebuild to generate
.storefront/configs
Best Practices
- Keep blocks focused - One responsibility per block
- Store lightweight data - Use IDs instead of full objects
- Use Suspense in render mode - For streaming and loading states
- Provide good defaults - Make blocks usable immediately
- Add helpful labels - Make editor UI self-documenting
- Test in the editor - Verify behavior at different viewports
Next Steps
- Visual Editing - Use blocks in the editor
- Templates - Use blocks in templates
- Data Fetching - Fetch data in blocks