You've built your entire frontend with custom types and example data. Now it's time to integrate Payload CMS. Here's the good news: your code barely changes. You're just swapping the data source.
This guide shows how smooth the transition is.
Why Your Code Doesn't Change
The magic of design-driven development is that your types are your specification. When you integrated with Payload, your types became the source of truth for Payload's field configuration.
Result: Your frontend types match Payload's output exactly. When you switch from example data to Payload data, the component sees the same type structure. No refactoring needed.
The Big Picture
code
BEFORE (Dummy Data)
├─ Types: src/types/blocks/, src/types/collections/
├─ Example Data: example.ts files
├─ Components: Use example data
└─ Page: Imports example data
↓↓↓ ONE CHANGE ↓↓↓
AFTER (Payload Integration)
├─ Types: @payload-types (Payload generates them)
├─ Data: Payload CMS queries
├─ Components: Use Payload data (same structure!)
└─ Page: Queries Payload
The component code stays identical.
Step 1: Verify Your Types Match Payload
Before switching to Payload, make sure your frontend types match what Payload will generate.
The structure should be identical. If Payload has extra fields, that's fine—your components just won't use them. If Payload is missing fields, you have a mismatch to fix.
Step 2: Prepare Your Component
Your component doesn't change. It already expects the exact type Payload will provide.
typescript
// This component works with BOTH example data and Payload data// No changes needed!exportfunctionFeaturedIndustriesTemplate1({ data }: { data: FeaturedIndustriesBlock }) {
const { selectedIndustries = [] } = data;
return (
<section>
{selectedIndustries.map(industry => (
<Cardkey={industry.id}><h3>{industry.title}</h3>
{/* ... render industry data ... */}
</Card>
))}
</section>
);
}
This component works with:
typescript
// Before: example dataconst data = {
blockType: 'featuredIndustries',
selectedIndustries: industriesData,
};
// After: Payload dataconst data = awaitgetBlock('featured-industries', id);
// Same type, same component, works in both cases!
The BlockRenderer stays exactly the same. It doesn't know or care where blocks come from. It just renders them.
Step 7: Handle Dynamic Imports (If Needed)
If Payload has new block types you haven't built yet, handle them gracefully:
typescript
exportfunctionBlockRenderer({ block }: { data: Block }) {
// Map blockType to componentconst component = blockComponentMap[block.blockType]?.[block.template];
if (!component) {
return (
<divclassName="p-6 bg-yellow-50 border border-yellow-200"><pclassName="text-yellow-800">
Block type "{block.blockType}" / "{block.template}" not yet implemented
</p></div>
);
}
returncomponent(block);
}
This prevents the app from breaking if Payload has a block type your frontend doesn't support yet.
Migration Checklist
code
PAYLOAD MIGRATION CHECKLIST:
SETUP
□ Payload CMS installed and configured
□ Types exported from Payload (@payload-types)
□ Environment variables set (API URL, tokens)
□ Test Payload API connection with curl/postman
VERIFICATION
□ Frontend types match Payload schema
□ All required fields exist in Payload
□ Example data still works (verify nothing broke)
□ BlockRenderer still routes correctly
UTILITIES
□ Created src/lib/payload.ts with helper functions
□ getPage() function works
□ getBlock() function works
□ Collection query functions (getIndustries(), etc.) work
MIGRATION
□ Updated src/app/page.tsx to use getPage()
□ Updated collection pages to use get() functions
□ Removed imports of example data
□ Verified all pages render with Payload data
□ No console errors
□ No TypeScript errors
TESTING
□ Home page loads from Payload
□ All blocks render correctly
□ Detail pages load from Payload
□ Images and media display correctly
□ Links and CTAs work
□ No 404s or missing data
□ Performance is acceptable
FALLBACKS
□ Graceful error handling for missing data
□ 404 pages for not-found content
□ Empty states for no data
□ Development mode still works with example data
Keeping Development with Examples
While integrating Payload, you might want to keep development mode using example data. You can add a flag:
typescript
// src/lib/payload.tsconstUSE_EXAMPLES = process.env.NEXT_PUBLIC_USE_EXAMPLES === 'true';
exportasyncfunctiongetPage(slug: string) {
if (USE_EXAMPLES) {
// Return example data in developmentreturn homePageData;
}
// Query Payload in productionconst response = awaitfetch(`${PAYLOAD_API}/api/pages?where[slug][equals]=${slug}`);
// ... etc
}
Then set NEXT_PUBLIC_USE_EXAMPLES=true in .env.local for local development.
Rollback Strategy
If something goes wrong, you can quickly rollback:
typescript
// src/lib/payload.tsexportasyncfunctiongetPage(slug: string) {
try {
// Query Payloadconst response = awaitfetch(`${PAYLOAD_API}/api/pages?where[slug][equals]=${slug}`);
if (!response.ok) thrownewError('Payload error');
returnawait response.json();
} catch (error) {
// Fallback to example dataconsole.warn('Fallback to example data:', error);
return homePageData;
}
}
This way, if Payload is down, your site still works with example data.
Performance Considerations
Payload queries should be efficient:
Revalidation:
typescript
// Revalidate every 60 seconds (good for mostly-static sites)
{ next: { revalidate: 60 } }
// Revalidate every hour
{ next: { revalidate: 3600 } }
// Never revalidate (manually trigger with revalidatePath)
{ cache: 'no-store' }
Selective Fields:
typescript
// Only query fields you need (not everything)awaitfetch(`${PAYLOAD_API}/api/pages?select=slug,title,layout&where[slug][equals]=home`)
Pagination:
typescript
// Limit results if querying collectionsawaitfetch(`${PAYLOAD_API}/api/industries?limit=100&page=1`)
Key Points to Remember
Your components don't change - They expect the exact type Payload provides
Your types guide Payload schema - Payload should match your frontend types
Data source changes, not the code - Just swap example data for Payload queries
BlockRenderer stays the same - It doesn't know or care where blocks come from
Fallbacks prevent broken sites - Always have a graceful fallback
The Real Magic
This is where design-driven development truly shines. By letting design determine your data structure before you even built Payload, you ensured that when Payload was added, it matched perfectly.
No refactoring. No "we need to add a field." No "the CMS output doesn't match our frontend types." Just seamless integration.
You've Completed the Series!
Congratulations! You now have the complete path: design-first development → custom types → components → example data → Payload integration.