Dynamically Update Page/Collection Blocks in Payload CMS Using Next.js API Routes
Managing content dynamically in a headless CMS can often be challenging, especially when you need to update multiple pages programmatically without breaking the existing structure. If you’re using Payload CMS with Next.js, this guide will show you how to build a secure API route to update landing page blocks efficiently.
Why Use a Dynamic Update API for Payload CMS?
Updating page content manually is time-consuming, especially when dealing with multiple pages that share similar blocks. By creating a Next.js API route, you can:
- Update multiple landing pages simultaneously.
- Maintain existing block IDs to avoid losing references.
- Add or update dynamic content blocks programmatically.
- Secure updates using a server-side token.
This approach is perfect for agencies or developers who manage headless CMS-powered websites with frequently changing content.
I’m using Next.js with Payload CMS, so I’ll guide you through my setup. You can follow this approach and adapt it for your project.
I created this script to make managing landing page content in Payload CMS super easy and efficient. The idea is simple: instead of manually updating every single page, you can now pass your page and block data in JSON format, and this script will handle the rest.
Location: app/api/update-pages/route.ts// Author: buggerspot.com
import { getPayload } from "payload";
import config from "@payload-config";
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
  const token = req.nextUrl.searchParams.get("token");
  if (token !== process.env.SERVER_CMS_UPDATE_TOKEN) {
    return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
  }
  try {
    const body = await req.json();
    const { pageIds, data } = body;
    if (!Array.isArray(pageIds) || pageIds.length === 0) {
      return NextResponse.json(
        { error: "Missing or invalid pageIds" },
        { status: 400 },
      );
    }
    if (!data || typeof data !== "object") {
      return NextResponse.json(
        { error: "Missing or invalid block data" },
        { status: 400 },
      );
    }
    const payload = await getPayload({ config });
    const {
      title,
      slug,
      metaDescription,
      featuredImage,
      parentCategory,
      blocks: updatedBlockList = [],
    } = data;
    if (!Array.isArray(updatedBlockList)) {
      return NextResponse.json(
        { error: "blocks must be an array" },
        { status: 400 },
      );
    }
    const results: {
      id: string | number;
      success: boolean;
      title?: string;
      error?: string;
    }[] = [];
    for (const pageId of pageIds) {
      try {
        const existing = await payload.findByID({
          collection: "pages",
          id: pageId,
        });
        if (!existing || !Array.isArray(existing.blocks)) {
          throw new Error("Landing page not found or blocks are missing.");
        }
        const existingBlocks = [...existing.blocks];
        const newBlocks: typeof existing.blocks = [];
        // Track how many of each block type we've already used
        const blockTypeCounters: Record<string, number> = {};
        for (const updatedBlock of updatedBlockList) {
          if (!updatedBlock?.blockType) continue;
          const type = updatedBlock.blockType;
          blockTypeCounters[type] = (blockTypeCounters[type] || 0) + 1;
          // Find the corresponding existing block occurrence
          let occurrence = 0;
          const index = existingBlocks.findIndex((b) => {
            if (b.blockType === type) {
              occurrence++;
              return occurrence === blockTypeCounters[type];
            }
            return false;
          });
          if (index !== -1) {
            // Update only fields, keep the existing block id intact
            const { ...fieldsToUpdate } = updatedBlock;
            newBlocks.push({
              ...existingBlocks[index],
              ...fieldsToUpdate,
            });
          } else {
            // Append new block if it doesn't exist yet
            newBlocks.push(updatedBlock);
          }
        }
        // Append remaining blocks that were not in the updatedBlockList
        const usedBlocksCount: Record<string, number> = {};
        for (const b of existingBlocks) {
          const countUsed = blockTypeCounters[b.blockType] || 0;
          const totalSameType = existingBlocks.filter(
            (eb) => eb.blockType === b.blockType,
          ).length;
          usedBlocksCount[b.blockType] =
            (usedBlocksCount[b.blockType] || 0) + 1;
          if (
            usedBlocksCount[b.blockType] > countUsed &&
            usedBlocksCount[b.blockType] <= totalSameType
          ) {
            newBlocks.push(b);
          }
        }
        const updated = await payload.update({
          collection: "landing-pages",
          id: pageId,
          data: {
            ...(title && { title }),
            ...(slug && { slug }),
            ...(metaDescription && { metaDescription }),
            ...(featuredImage && { featuredImage }),
            ...(parentCategory && { parentCategory }),
            blocks: newBlocks,
          },
        });
        results.push({ id: pageId, success: true, title: updated.title });
      } catch (err: unknown) {
        const message = err instanceof Error ? err.message : String(err);
        results.push({ id: pageId, success: false, error: message });
      }
    }
    return NextResponse.json({ updated: results });
  } catch (err: unknown) {
    const message = err instanceof Error ? err.message : String(err);
    return NextResponse.json({ error: message, status: 500 });
  }
}
Here’s a sample JSON showing the structure of my page blocks. You can use it as a reference to create a similar structure for your collection. If you’re unsure about any field, you can check the API response for each page in the Payload CMS backend{
  "title": "Buggerspot - Fix Your Blog & Website Issues",
  "slug": "about-me",
  "metaDescription": "Buggerspot provides fast and reliable bug fixes, website improvements, and maintenance services for bloggers and small businesses.",
  "featuredImage": 7,
  "blocks": [
    {
      "blockType": "Hero",
      "heading": "Fix Your Website Issues Instantly",
      "subheading": "Buggerspot helps bloggers and small businesses resolve bugs and improve website performance in no time.",
      "ctaText": "Get Started",
      "ctaLink": "/contact",
      "backgroundImage": 5
    },
    {
      "blockType": "ContentMediaBlock",
      "heading": "Why Choose Buggerspot?",
      "content": "We provide fast, reliable, and affordable solutions for all your website issues. From minor bug fixes to major performance optimization, we've got you covered.",
      "media": 2,
      "mediaPosition": "right"
    },
    {
      "blockType": "ImageGridBlock",
      "heading": "Our Services",
      "items": [
        {
          "title": "Blog Bug Fixes",
          "image": 3,
          "description": "Resolve blog errors, broken links, and display issues quickly."
        },
        {
          "title": "Website Optimization",
          "image": 4,
          "description": "Improve your site's loading speed and performance effortlessly."
        },
        {
          "title": "SEO & Meta Updates",
          "image": 9,
          "description": "Update meta descriptions, titles, and SEO settings across your site."
        }
      ]
    },
    {
      "blockType": "TestimonialsBlock",
      "heading": "What Our Clients Say",
      "testimonials": [
        {
          "name": "Alice Johnson",
          "role": "Blogger",
          "content": "Buggerspot fixed my blog issues within hours. Highly recommended!"
        },
        {
          "name": "Mark Stevens",
          "role": "Small Business Owner",
          "content": "Fast, reliable, and affordable. My website performance has improved drastically."
        }
      ]
    },
    {
      "blockType": "FaqBlock",
      "heading": "Frequently Asked Questions",
      "faqs": [
        {
          "question": "How quickly can you fix my website?",
          "answer": "Most common issues are fixed within 24 hours, depending on complexity."
        },
        {
          "question": "Do you work on all blogging platforms?",
          "answer": "Yes, we support WordPress, Blogger, and most custom CMS setups."
        },
        {
          "question": "Can you update SEO meta data across multiple pages?",
          "answer": "Absolutely! Our system allows bulk updates to meta descriptions, titles, and tags."
        }
      ]
    },
    {
      "blockType": "CtaFormBlock",
      "heading": "Ready to Fix Your Website?",
      "subheading": "Get in touch with Buggerspot today and let us handle your website issues.",
      "formId": "contact-form",
      "ctaText": "Submit"
    }
  ]
}With this API, you can import new blocks, update existing blocks, or change specific fields across one or multiple pages at once. For example, if you want to update a single section, like a hero banner or a testimonial, across your entire site, this script will do it automatically. There’s no need to open each page individually and make the changes manually.
It’s perfect for content teams or developers who frequently make updates. You can also maintain the structure of your existing blocks, so nothing breaks visually or functionally. The script ensures that only the targeted fields are updated, leaving all other content intact.
Essentially, it’s a time-saving, reliable solution for dynamic content management. Whether you’re updating text, images, or metadata, this script makes it fast, consistent, and error-free. If your site has dozens or even hundreds of landing pages, this tool will save hours of work and reduce the risk of mistakes.
