• Home
BuildWithMatija
Get In Touch
Part 7·Design to Code: Building Block Systems with Payload CMS
  1. Home
  2. Blog
  3. Payload
  4. Migrate to Payload CMS: Seamless Swap from Dummy Data

Migrate to Payload CMS: Seamless Swap from Dummy Data

How to replace example data with Payload CMS queries, preserve frontend types, and add getPage/getBlock utilities for…

21st November 2025·Updated on:25th December 2025·MŽMatija Žiberna·
Payload

Need Help Making the Switch?

Moving to Next.js and Payload CMS? I offer advisory support on an hourly basis.

Book Hourly Advisory

Related Posts:

  • •Creating Collections: Reusable Data Entities — Types & Examples
  • •Design-Driven Block Systems: Complete 7-Part Guide
  • •Complete Guide: Use Existing Payload CMS Block Types

Part 7 of the Design to Code series — Following Quick Reference

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

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.

Your manual type:

export interface FeaturedIndustriesBlock {
  blockType: 'featuredIndustries';
  template: 'default';
  title?: string;
  selectedIndustries?: Industry[];
  bgColor?: 'white' | 'light' | 'dark';
}

export interface Industry {
  id: number;
  title: string;
  slug?: string;
  description?: string;
  icon?: string;
  link?: CTA;
}

Should match your Payload collection/block config:

// payload/blocks/featured-industries.ts
{
  slug: 'featured-industries',
  fields: [
    { name: 'title', type: 'text' },
    { name: 'selectedIndustries', type: 'relationship', relationTo: 'industries' },
    { name: 'bgColor', type: 'select', options: ['white', 'light', 'dark'] },
  ]
}

// payload/collections/industries.ts
{
  slug: 'industries',
  fields: [
    { name: 'title', type: 'text', required: true },
    { name: 'slug', type: 'text' },
    { name: 'description', type: 'textarea' },
    { name: 'icon', type: 'text' },
    { name: 'link', type: 'relationship', relationTo: 'ctas' },
  ]
}

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.

// This component works with BOTH example data and Payload data
// No changes needed!

export function FeaturedIndustriesTemplate1({ data }: { data: FeaturedIndustriesBlock }) {
  const { selectedIndustries = [] } = data;

  return (
    <section>
      {selectedIndustries.map(industry => (
        <Card key={industry.id}>
          <h3>{industry.title}</h3>
          {/* ... render industry data ... */}
        </Card>
      ))}
    </section>
  );
}

This component works with:

// Before: example data
const data = {
  blockType: 'featuredIndustries',
  selectedIndustries: industriesData,
};

// After: Payload data
const data = await getBlock('featured-industries', id);

// Same type, same component, works in both cases!

Step 3: Update Data Fetching

Replace your example data with Payload queries.

Before:

// src/app/page.tsx
import homePageData from './data';

export default function HomePage() {
  return (
    <main>
      {homePageData.layout.map((block, i) => (
        <BlockRenderer key={i} block={block} />
      ))}
    </main>
  );
}

After:

// src/app/page.tsx
import { getPage } from '@/lib/payload';
import { BlockRenderer } from '@/components/block-renderer';

export default async function HomePage() {
  // Query Payload CMS instead of importing example data
  const homePageData = await getPage('home');

  return (
    <main>
      {homePageData.layout.map((block, i) => (
        <BlockRenderer key={i} block={block} />
      ))}
    </main>
  );
}

That's it. Everything else stays the same.

Step 4: Create Payload Utilities

Build helper functions to query Payload data:

File: src/lib/payload.ts

import type { Page, FeaturedIndustriesBlock } from '@payload-types';

const PAYLOAD_API = process.env.NEXT_PUBLIC_PAYLOAD_API_URL;

/**
 * Get a complete page with all blocks
 */
export async function getPage(slug: string): Promise<Page> {
  const response = await fetch(
    `${PAYLOAD_API}/api/pages?where[slug][equals]=${slug}`,
    { next: { revalidate: 60 } }
  );

  if (!response.ok) throw new Error(`Failed to fetch page: ${slug}`);

  const data = await response.json();
  return data.docs[0];
}

/**
 * Get a single block by ID
 */
export async function getBlock(blockType: string, id: string) {
  const response = await fetch(
    `${PAYLOAD_API}/api/blocks?where[blockType][equals]=${blockType}&where[id][equals]=${id}`,
    { next: { revalidate: 60 } }
  );

  if (!response.ok) throw new Error(`Failed to fetch block: ${blockType}/${id}`);

  const data = await response.json();
  return data.docs[0];
}

/**
 * Get all industries (collection)
 */
export async function getIndustries(): Promise<Industry[]> {
  const response = await fetch(
    `${PAYLOAD_API}/api/industries`,
    { next: { revalidate: 60 } }
  );

  if (!response.ok) throw new Error('Failed to fetch industries');

  const data = await response.json();
  return data.docs;
}

/**
 * Get single industry by slug
 */
export async function getIndustry(slug: string): Promise<Industry> {
  const response = await fetch(
    `${PAYLOAD_API}/api/industries?where[slug][equals]=${slug}`,
    { next: { revalidate: 60 } }
  );

  if (!response.ok) throw new Error(`Failed to fetch industry: ${slug}`);

  const data = await response.json();
  return data.docs[0];
}

These utilities handle Payload API calls consistently. Your components don't know about Payload—they just get data.

Step 5: Update Page Data Functions

For pages that reference specific industries or other collections:

Before:

// src/app/industries/[slug]/page.tsx
import { industriesData } from '@/types/collections';

export default function IndustryPage({ params }) {
  const industry = industriesData.find(i => i.slug === params.slug);

  if (!industry) return <div>Not found</div>;

  return <IndustryDetail industry={industry} />;
}

After:

// src/app/industries/[slug]/page.tsx
import { getIndustry } from '@/lib/payload';

export default async function IndustryPage({ params }) {
  try {
    const industry = await getIndustry(params.slug);
    return <IndustryDetail industry={industry} />;
  } catch (error) {
    return <div>Not found</div>;
  }
}

Same component (IndustryDetail), different data source.

Step 6: Update BlockRenderer (Optional)

If you're using Payload's block system, BlockRenderer might query blocks from Payload instead of receiving them as props:

Before:

export function BlockRenderer({ block }: { block: Block }) {
  if (block.blockType === 'hero') {
    return <HeroTemplate1 data={block} />;
  }
  // ... etc
}

After (no change):

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:

export function BlockRenderer({ block }: { data: Block }) {
  // Map blockType to component
  const component = blockComponentMap[block.blockType]?.[block.template];

  if (!component) {
    return (
      <div className="p-6 bg-yellow-50 border border-yellow-200">
        <p className="text-yellow-800">
          Block type "{block.blockType}" / "{block.template}" not yet implemented
        </p>
      </div>
    );
  }

  return component(block);
}

This prevents the app from breaking if Payload has a block type your frontend doesn't support yet.

Migration Checklist

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:

// src/lib/payload.ts
const USE_EXAMPLES = process.env.NEXT_PUBLIC_USE_EXAMPLES === 'true';

export async function getPage(slug: string) {
  if (USE_EXAMPLES) {
    // Return example data in development
    return homePageData;
  }

  // Query Payload in production
  const response = await fetch(`${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:

// src/lib/payload.ts
export async function getPage(slug: string) {
  try {
    // Query Payload
    const response = await fetch(`${PAYLOAD_API}/api/pages?where[slug][equals]=${slug}`);
    if (!response.ok) throw new Error('Payload error');
    return await response.json();
  } catch (error) {
    // Fallback to example data
    console.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:

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

// Only query fields you need (not everything)
await fetch(`${PAYLOAD_API}/api/pages?select=slug,title,layout&where[slug][equals]=home`)

Pagination:

// Limit results if querying collections
await fetch(`${PAYLOAD_API}/api/industries?limit=100&page=1`)

Key Points to Remember

  1. Your components don't change - They expect the exact type Payload provides
  2. Your types guide Payload schema - Payload should match your frontend types
  3. Data source changes, not the code - Just swap example data for Payload queries
  4. BlockRenderer stays the same - It doesn't know or care where blocks come from
  5. 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.

Series Summary:

  • Part 1: Design-Driven Development — Philosophy & Mindset
  • Part 2: Use Existing Payload Block Types — Working with existing definitions
  • Part 3: Create Custom Block Types — Building from scratch
  • Part 4: Creating Collections — Reusable data entities
  • Part 5: Icons, Components & Best Practices — Consistency patterns
  • Part 6: Quick Reference — Templates & checklists
  • Back to Hub: Design to Code — Full series overview

Next Resources:

  • Need help with specific Payload config? Check Payload docs at payloadcms.com
  • Questions about queries? See your Payload API docs
  • Performance optimization? Review Next.js caching docs

Each step builds on the previous, and switching to Payload requires almost no code changes. This is the efficiency of design-driven development.

📚 Comprehensive Payload CMS Guides

Detailed Payload guides with field configuration examples, custom components, and workflow optimization tips to speed up your CMS development process.

No spam. Unsubscribe anytime.

← Previous

Block Component Templates: Quick Reference & Checklists

Copy-paste TypeScript + React templates, example data, and practical checklists for building blocks, collections, and…

Next →

This is the last article in the series

← Back to series
📄View markdown version
0

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

Stay updated! Get our weekly digest with the latest learnings on NextJS, React, AI, and web development tips delivered straight to your inbox.

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

You might be interested in

Creating Collections: Reusable Data Entities — Types & Examples

18th November 2025

Design-Driven Block Systems: Complete 7-Part Guide
Design-Driven Block Systems: Complete 7-Part Guide

14th November 2025

Complete Guide: Use Existing Payload CMS Block Types
Complete Guide: Use Existing Payload CMS Block Types

16th November 2025

Table of Contents

  • Why Your Code Doesn't Change
  • The Big Picture
  • Step 1: Verify Your Types Match Payload
  • Step 2: Prepare Your Component
  • Step 3: Update Data Fetching
  • Step 4: Create Payload Utilities
  • Step 5: Update Page Data Functions
  • Step 6: Update BlockRenderer (Optional)
  • Step 7: Handle Dynamic Imports (If Needed)
  • Migration Checklist
  • Keeping Development with Examples
  • Rollback Strategy
  • Performance Considerations
  • Key Points to Remember
  • The Real Magic
  • You've Completed the Series!
On this page:
  • Why Your Code Doesn't Change
  • The Big Picture
  • Step 1: Verify Your Types Match Payload
  • Step 2: Prepare Your Component
  • Step 3: Update Data Fetching
Build With Matija Logo

Build with Matija

Matija Žiberna

I turn scattered business knowledge into one usable system. End-to-end system architecture, AI integration, and development.

Quick Links

Payload CMS Websites
  • Bespoke AI Applications
  • Projects
  • How I Work
  • Blog
  • Get in Touch

    Have a project in mind? Let's discuss how we can help your business grow.

    Contact me →
    © 2026BuildWithMatija•Principal-led system architecture•All rights reserved