WordPress CMS returns 404 errors when Next.js frontend requests page data

I’m working on a project where I use WordPress as a headless CMS and Next.js for the frontend. The setup fetches page content from WordPress and displays it as JSON data on the frontend.

The main problem: Some pages randomly return 404 errors, but this only happens in production. My local development environment and staging server work perfectly fine.

Background context: My WordPress server sometimes goes down, so I added a caching system that saves JSON files locally. When the CMS is unavailable, the site serves content from these cached files instead of showing server errors.

What I’ve noticed: When I navigate through the site, Next.js makes requests to URLs that include a build ID. If this ID keeps changing, could that be causing the 404 issues? The pattern looks like this: /_next/data/[BUILD_ID]/page-name.json

Here’s my server-side code that handles the data fetching:

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const cachePath = path.join(process.cwd(), 'cache/content');
  if (!fs.existsSync(cachePath)) {
    fs.mkdirSync(cachePath, { recursive: true });
  }

  // Get navigation and site data
  const mainNav: iNavigation = await apiCall('/menus/v1/menus/main');
  const socialLinks: iNavigation = await apiCall('/menus/v1/menus/social');
  const siteFooter: iNavigation = await apiCall('/menus/v1/menus/site-footer');
  const siteEmails: iEmails = await apiCall('/acf/v2/options/contact-info');
  const allPages = await apiCall('/wp/v2/pages', { params: { per_page: 1 } });

  const navigation = {
    main: processNavigation(mainNav),
    footer: processNavigation(siteFooter),
    social: socialLinks ? socialLinks.items?.map(({ url }) => ({ url })) : {},
  };

  const latestUpdate = allPages?.[0]?.modified;
  const emailContacts = siteEmails?.contact_info;

  const navCachePath = path.join(cachePath, `navigation.json`);
  const emailCachePath = path.join(cachePath, `emails.json`);
  const updateCachePath = path.join(cachePath, `update-time.json`);

  let navData, emailData, updateData;

  if (latestUpdate && emailContacts && navigation.main && navigation.footer) {
    fs.writeFileSync(navCachePath, JSON.stringify(navigation, null, 2));
    fs.writeFileSync(emailCachePath, JSON.stringify(emailContacts, null, 2));
    fs.writeFileSync(updateCachePath, JSON.stringify(latestUpdate, null, 2));
  }

  navData = fs.readFileSync(navCachePath, 'utf-8');
  emailData = fs.readFileSync(emailCachePath, 'utf-8');
  updateData = fs.readFileSync(updateCachePath, 'utf-8');

  // Return 404 for invalid routes
  if (!params?.route || !Array.isArray(params.route)) {
    return {
      redirect: {
        destination: '/404',
        permanent: false,
      },
    };
  }

  let pageContent = null;
  let finalUrl = '';
  let hasError = false;

  // Process each URL segment
  for (let index = 0; index < params.route.length; index++) {
    const segment = params.route[index];
    const segmentCachePath = path.join(cachePath, `${segment}.json`);
    const contentData = await fetchPageContent(segment);

    if (contentData) {
      fs.writeFileSync(segmentCachePath, JSON.stringify(contentData, null, 2));
      pageContent = {
        ...contentData,
        meta_description: contentData.meta_description ?? null,
      };
      finalUrl += `/${segment}`;
    } else if (fs.existsSync(segmentCachePath)) {
      try {
        const cachedContent = fs.readFileSync(segmentCachePath, 'utf-8');
        const parsedData = JSON.parse(cachedContent);
        pageContent = {
          ...parsedData,
          meta_description: parsedData.meta_description ?? null,
        };
        finalUrl += `/${segment}`;
      } catch (err) {
        console.error(`Cache read failed for ${segment}:`, err);
      }
    } else {
      if (index === params.route.length - 1) {
        hasError = true;
      } else {
        continue;
      }
    }
  }

  // Show 404 if no content found
  if (hasError || !pageContent) {
    return {
      props: {
        pageData: {
          status: 404,
          page_title: 'Content Not Found',
          error_message: 'Sorry, the page you are looking for does not exist.',
        },
      },
    };
  }

  // Redirect if URL structure is wrong
  const requestedPath = '/' + params.route.join('/');
  if (finalUrl && requestedPath !== finalUrl) {
    return {
      redirect: {
        destination: finalUrl,
        permanent: false,
      },
    };
  }

  return {
    props: {
      pageData: pageContent,
      navigation: JSON.parse(navData),
      emails: JSON.parse(emailData),
      lastModified: JSON.parse(updateData),
    },
  };
};

Any ideas why this works locally but fails in production? Could the changing build ID be the root cause?

yeah, same issue here. set a custom buildId in next.config.js instead of letting next.js auto-generate them. try generateBuildId: async () => { return 'my-build-id' } or use your git commit hash. also check if cache files survive deployments - most hosts wipe everything on redeploy, which breaks fallback.

I’ve hit this same issue before. Check your caching setup - it’s probably getting nuked during deployment. Most hosting platforms wipe the entire filesystem when you deploy, so your cache directory disappears completely. That explains why pages work right after deployment but break later when WordPress goes down. You’re getting hit with a double whammy: fresh deployment (no cache) plus WordPress server issues. Your code tries WordPress, fails, then looks for cache files that aren’t there anymore. Switch to Redis or something persistent, or at least restore your cache directory from backup after each deploy. And add some fallback logic so you show a generic page instead of 404s when both live data and cache are toast.

The changing build ID is your problem. Next.js generates a new build ID every time it rebuilds in production, which breaks all the existing /_next/data/[BUILD_ID]/ URLs. That’s why it works locally but not in production - development doesn’t rebuild as much.

I ran into this same scenario with a headless Drupal site. Here’s what happens: users have cached pages that reference the old build ID, but your server has already deployed a new build. When they navigate, their browser requests data with the stale build ID and receives a 404.

Consider using fallback: 'blocking' in your getStaticPaths if applicable, or switch to getServerSideProps for critical pages. Additionally, check if your hosting provider rebuilds automatically, as some platforms can trigger a rebuild on every git push or API change, leading to constant build ID changes. I also recommend adding logging to track when builds occur versus when 404s arise to confirm if this is indeed the root cause.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.