React & CMS

How We Made CMS Content Work with React Server Components

We use CMS content with React Server Components. The fetch is easy. What breaks is caching, stale links, and revalidation, not the fetch.

SA
Samer Alsayegh
Founder
Published
4 min read
Key takeaways

Fetching CMS content in a React Server Component is the easy part. What actually breaks in production is a cached list page that doesn't know its own contents changed, client routes that silently opt out of metadata, and MDX links to entries that aren't published yet.

We render Draftbase's own marketing site and blog with Next.js App Router and React Server Components. Fetching CMS content into an RSC sounds simple: call the API in the component, done. What actually broke, twice, wasn't the fetch. It was every layer around it: what gets cached, what gets invalidated, and what quietly stops being indexable because of one file-level decision.

The Fetch Itself Was Never the Problem

An RSC can await fetch() directly in the component body, no client-side loading state, no useEffect. That part worked from day one. apps/frontend calls our own delivery API (GET /delivery/entries) from server components and gets back published content with Next's own fetch cache sitting in front of it. If you're evaluating a CMS for an RSC-based frontend, this half of the story is genuinely a non-issue: any CMS with a stable HTTP API does this part fine.

Where it gets interesting is everything downstream of that fetch succeeding.

A Cached List Page Doesn't Know When Its Own Contents Change

Our sitemap has its own route, /sitemap.xml, built by querying every published entry. We shipped it with no revalidate window and, worse, we forgot to add it to the webhook handler that revalidates a page when its content changes. A post published through the CMS went live at its own URL immediately. It stayed missing from the sitemap until the next full deploy, which is the exact window where a new post needs to be discoverable most.

The fix wasn't "cache less." It was recognizing that a page listing content needs invalidating on any member changing, not just on its own route being edited. pathsForEntry now appends /sitemap.xml to the revalidation list whenever an entry produces any path at all, and the route also carries a revalidate = 14400 backstop in case a webhook is ever missed. If you're building a CMS-backed RSC site with any kind of index or listing page, ask what invalidates it before you ask what caches it. The two questions have different answers.

Client Components Silently Opt Out of Metadata

/login, /onboarding, /billing, and a handful of other authenticated routes in our app are 'use client' from the top. They're interactive from the first paint, so that's the right call. What we didn't realize until an SEO audit flagged it: a client component can't export a metadata object. Every one of those routes rendered with the root layout's default title and description. We had seven pages all claiming to be "Draftbase," with the same generic blurb, and none of them had a real h1 either.

The fix wasn't to restructure those routes as server components with client islands, since they genuinely don't need to be indexed. It was simpler: add them to robots.ts's disallow list and stop treating "duplicate metadata" as a metadata bug when it's actually an indexing bug. If a route is authenticated and should never rank, block it at the crawler level instead of trying to hand-author metadata for a page nobody should find through search anyway.

MDX content authored in the CMS links to other entries by their intended URL, written before that target entry is published. That's normal. It's also exactly how a live 404 happens. An author writes [see our guide](/blog/some-slug) while some-slug is still a draft. The containing post gets published, and now there's a broken internal link on a live page, pointing at a route that doesn't exist yet.

Two of the three 4xx errors in one of our own site audits were exactly this. The fix runs at render time now. Every internal entry link (/blog/…, /<pillar>/…, /docs/<version>/…) inside CMS-authored MDX gets checked against actually-published targets. An unresolved one renders as inert text instead of a link. It only guards entry-backed routes, not arbitrary marketing URLs like /pricing. That's a gap we know about and haven't closed. Those links are hand-written by us, not generated from CMS state that can independently go stale.

What We'd Tell a Team Starting This Today

None of these four problems were about RSC data fetching itself. They were about treating "the page renders the right content" as the whole job. A CMS-backed site actually has three separate correctness questions. Does the page show the right thing right now? Does everything that lists or links to that page know when it changes? Does a route that should never be indexed actually get excluded, rather than just left unlinked?

The RSC fetch is the easy 20%. Revalidation paths, crawler-facing routes, and content-to-content link integrity are the part that actually breaks in production. None of it shows up in a demo where you publish one post and look at one page.

If you're building on Draftbase, our webhook-driven revalidation and the render-time link guard described above ship as defaults, not something you build yourself. See how MCP clients and servers work together if you're also wiring up AI-driven content edits on top of this. Or check pricing to see the delivery API these routes call.

Ship content that's built to be found

Draftbase generates schema, structured data, and a fast MDX editor for every post.

Frequently asked questions

Is fetching CMS content in a React Server Component hard?

No. Any CMS with a stable HTTP API works fine with a plain `await fetch()` in the component body.

What actually breaks with CMS content in Next.js App Router?

Caching and revalidation around list pages. A sitemap that doesn't know when its own contents change is a common one. Links to entries that aren't published yet are another.

Why did our sitemap miss newly published posts?

It had no revalidate window and wasn't in the webhook path list. A page listing content needs invalidating on any member changing, not just its own route.

Can a client component export page metadata in Next.js?

No. `'use client'` routes can't export a `metadata` object. They inherit the root layout's default title and description instead.

What happens when CMS content links to an unpublished entry?

The link ships live and 404s once the containing post is published. We now check internal entry links at render time and drop unresolved ones to inert text.

SA
Samer Alsayegh
Founder at Draftbase

Samer is a software engineer and entrepreneur, founder of Draftbase and Ezi Home Services, building technology that simplifies home services. Passionate about software, APIs, automation, and creating products that solve real-world problems.

react-server-componentsnextjsengineering

Related posts

Draftbase is a headless CMS built for React devs.