Kamaankamaan

Headless CMS for React: Fetch and Render Blog Content in a React App

Fetch and render blog content in a React app three ways: client-side, server-side with React Server Components, and static generation. See the code, the SEO trade-offs, how to render the article body safely, and how a st

Junaid Khalid
Junaid Khalid
August 8, 2026 · 14 min read

You picked React for the front end and now the blog is the awkward part. Marketing wants to publish without waiting on a deploy, the posts have to render inside the app you already ship, and nobody wants to bolt on WordPress or hand-build an admin panel. A headless CMS handles the storage-and-editing half of that problem. The half almost nobody explains well is the React half: how you actually fetch the content, which rendering strategy to pick, how to render the article body without breaking your page, and why the obvious client-side approach quietly costs you search rankings. This guide walks through all of it, with the code.

Key takeaways

  • A headless CMS stores and serves your content as JSON. Your React app fetches it and renders it. The CMS never touches your markup.
  • You have three ways to render: client-side with useEffect and fetch, server-side with React Server Components, and static generation at build time. Each has a different SEO and freshness trade-off.
  • Client-side-only rendering is the most common mistake. Content that loads after the initial HTML can be missed or delayed by crawlers, so a blog that needs to rank should use server-side rendering or static generation.
  • The body usually arrives as an HTML string or structured rich text. How you render it (sanitized HTML versus a serializer) matters more than the fetch itself.
  • A framework-agnostic REST API returns standard JSON over one endpoint, so the same content renders in Next.js, a Vite React app, or Astro, and one CMS can feed every product blog you run.

What a headless CMS actually gives a React app

A headless CMS splits the writing surface from the presentation layer. Editors write and schedule posts in the CMS, which exposes that content over an API as data, usually JSON. Your React app calls the API, gets back structured objects, and renders them however your design system wants. Nothing about the CMS dictates your markup, your routing, or your styling.

That separation is why React and headless pair well. React turns data into UI. A headless CMS turns editor input into data. You are wiring a data source to a renderer, which is what React does every day with any other API: no theme to fight, no PHP template to override, no plugin ecosystem to patch.

The shape of the work is small. You fetch a list of posts for the index page, and a single post by slug for the detail page, then render the title, the metadata, and the body. Everything past that (pagination, tags, related posts, search) is more of the same pattern. For the full picture of wiring a blog into a product, see the guide on how to add a blog to your SaaS product.

The three ways to fetch and render CMS content in React

There is no single correct way to render CMS content in React. There are three, and the right one depends on whether the page needs to rank in search and how fresh the content must be.

Client-side rendering (CSR). The React app ships with empty containers, then fetches content in the browser after the page loads. This is the classic useEffect plus fetch pattern, and it is the default in a plain Vite or Create React App project.

import { useEffect, useState } from "react";

function BlogList() {
  const [posts, setPosts] = useState([]);
  const [status, setStatus] = useState("loading");

  useEffect(() => {
    fetch(`${import.meta.env.VITE_CMS_URL}/articles`)
      .then((res) => res.json())
      .then((data) => {
        setPosts(data.articles);
        setStatus("ready");
      })
      .catch(() => setStatus("error"));
  }, []);

  if (status === "loading") return <p>Loading posts...</p>;
  if (status === "error") return <p>Could not load posts.</p>;

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>
          <a href={`/blog/${post.slug}`}>{post.title}</a>
        </li>
      ))}
    </ul>
  );
}

Server-side rendering (SSR) with React Server Components. In a Next.js App Router project, a Server Component can be an async function that awaits the fetch and renders on the server. The reader (and the crawler) gets fully-formed HTML on the first response. No loading spinner, no empty shell.

// app/blog/page.jsx (a React Server Component)
export default async function BlogIndex() {
  const res = await fetch(`${process.env.CMS_URL}/articles`, {
    next: { revalidate: 600 },
  });
  const { articles } = await res.json();

  return (
    <ul>
      {articles.map((post) => (
        <li key={post.slug}>
          <a href={`/blog/${post.slug}`}>{post.title}</a>
        </li>
      ))}
    </ul>
  );
}

Static generation (SSG and ISR). For a blog, this is usually the best of the three. You fetch every post at build time and pre-render each page to static HTML. With incremental static regeneration you also set a revalidation window, so new posts appear without a full rebuild. Pages are served from a CDN as plain files, which is fast and cheap.

// app/blog/[slug]/page.jsx
export async function generateStaticParams() {
  const res = await fetch(`${process.env.CMS_URL}/articles`);
  const { articles } = await res.json();
  return articles.map((post) => ({ slug: post.slug }));
}

export default async function Article({ params }) {
  const res = await fetch(`${process.env.CMS_URL}/articles/${params.slug}`);
  const post = await res.json();

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.body_html }} />
    </article>
  );
}

The response shapes above (articles, post.slug, post.body_html) are a typical example, not any one vendor's fixed schema. The pattern holds regardless of the exact field names your CMS returns.

Strategy How it renders SEO Content freshness Best for
Client-side (CSR) Browser fetches after load Weakest: content is not in the first HTML Instant on every visit Dashboards, gated app views, not public blogs
Server-side (SSR) Server fetches per request Strong: full HTML on first response Always current Personalized or fast-changing pages
Static + ISR (SSG) Pre-rendered at build, revalidated Strongest: static HTML, fastest load Near-current via revalidation Public blogs and marketing content

Rendering the article body without breaking your page

Fetching the JSON is the easy part. The part that trips people up is the body. A headless CMS delivers article content in one of three forms, and each renders differently in React.

An HTML string. The body arrives as ready-to-render HTML. You inject it with dangerouslySetInnerHTML, as in the static example above. The word "dangerously" is earned: if any of that HTML can come from an untrusted source, sanitize it first with a library like DOMPurify. For editor-written content the risk is low, but sanitizing costs nothing and prevents a bad day.

Markdown. The body is a Markdown string. Render it with a component like react-markdown rather than injecting raw HTML. You get React elements you can style and remap, so an image in the Markdown can become your own optimized image component.

Structured rich text. The body is a tree of typed nodes (a heading node, a paragraph node, an image node). This is the most work up front and the most control later: you write a serializer that maps each node type to a React component. Sanity's Portable Text and similar formats use this model, so your headings, callouts, and embeds are real components in your design system, not opaque HTML.

Pick the format that matches how much control you need. For a straightforward product blog, an HTML or Markdown body renders in a few lines and looks like the rest of your site. For more depth, the blog API guide covers fetching and rendering headless content field by field in any framework.

The SEO trap in client-side-only React blogs

Here is the mistake that costs teams months of ranking: they build the blog as a pure client-side React app, ship it, and wait for traffic that never comes. When the CMS content loads only after the JavaScript runs in the browser, the first HTML a crawler receives is an empty shell. Search engines do render JavaScript, but rendering is deferred and inconsistent, and social preview scrapers often skip it. Your post can be invisible at the moment that matters.

The fix is to move rendering to the server or to build time. Both put the real content into the first HTML response, which is what crawlers, link previews, and slow devices all want. This is why the headless CMS Next.js integration approach uses Server Components and static generation rather than client fetching for anything public.

Going headless does not hurt SEO on its own, as long as your React app renders on the server or at build time. The deeper trade-offs are in the guide on headless CMS SEO. The one-line rule: if a page needs to rank, do not render its content only in the browser.

Choosing a React headless CMS: what actually matters

Most "best React CMS" lists rank tools by star count. For a blog, three things decide whether the integration is a weekend or a slog: how you fetch (a plain REST call versus a proprietary query language), whether multilingual is built in or bolted on, and what it costs once you run more than one product.

A standard JSON REST API is the lowest-friction option because it works with a normal fetch() in any React setup, no SDK required. Some platforms lean on schema-as-code (you define your content model in code before you can store a post) or a proprietary query language, which is control you may not need for a blog. Others are self-hosted, which means the server, scaling, and security patches are yours to run.

Kamaan's REST API Delivery takes the plain-JSON path on purpose. Kamaan's REST API delivers your blog content in standard JSON to any framework: Next.js, Nuxt, SvelteKit, Astro, React, or anything else your dev team uses. There is no schema-as-code step and no server to run, so the same three patterns above are all you need. The table below compares the delivery model and the multi-product cost; verify any competitor price on its own pricing page before you commit, since these move.

Dimension Kamaan Contentful Sanity Strapi
React delivery Standard JSON REST API REST and GraphQL GROQ or GraphQL REST and GraphQL
Setup ceremony No schema-as-code, no server Content modeling per space Schema-as-code required Self-host and maintain
Native multilingual Publish once, auto-delivered per plan Add-on and modeling Plugin and modeling Plugin and modeling
Multi-product pricing Flat tiers, one account Per space, first paid tier around $300/mo Per seat and usage Free to self-host, you run the box
Who runs the server Kamaan (managed) Contentful Sanity You

The gap that matters if you run several products: Contentful bills per space, so three product blogs mean three spaces and three subscriptions. Kamaan is one account and one dashboard for every blog you run, so the same REST endpoint can feed several React apps without a new bill each time.

Multilingual React blogs: fetch by locale, get hreflang right

If the blog needs to reach more than one market, multilingual is where a naive React setup falls apart. The manual path (copy the post into a translation tool, paste it back, duplicate the route per language, hand-maintain hreflang tags) breaks at scale, and the hreflang is usually wrong.

The cleaner pattern is to fetch content by locale. Your React route carries the language (for example /es/blog/[slug]), and the fetch passes that locale to the CMS, which returns the translated version. With Kamaan's Auto-Multilingual Delivery, you publish once in English and it is delivered in every language on your plan, each at its own URL, with the hreflang tags emitted correctly server-side. Your React app fetches by locale and renders. The international SEO plumbing is not your front end's problem.

Publish once, and it is delivered in every language on your plan. On the Growth tier that is three languages, and the Unlimited tier reaches 99 or more. The React work stays identical: one fetch, one render, parameterized by locale.

How this looks in a real workflow

A developer at a three-product SaaS studio wires one Kamaan REST endpoint into three separate React front ends: a Next.js marketing site, a Vite dashboard blog, and an Astro docs site. Each app uses the same standard JSON response and its own components, so the design stays native to each product. One Growth account at $49 a month covers all three blogs from one dashboard, with no per-space bill stacking up as the studio ships more products. When an editor publishes, every front end picks up the post on its next revalidation.

A solo founder ships a Next.js blog using Server Components and incremental static regeneration. They write in English and use Auto-Multilingual Delivery to reach three languages on the Growth tier, each locale fetched by its route and pre-rendered to static HTML. Because the pages are rendered ahead of time, the real content sits in the first HTML response, so search engines and link previews see the post. First-time wiring of a Next.js front end to the REST API is an honest 40 to 60 minutes; the CMS-side benchmark from publish to live is roughly 14 minutes.

FAQ

Can you use a headless CMS with React?

Yes, and it is one of the most natural pairings in modern web development. A headless CMS serves content as JSON over an API, and React renders JSON into UI. You fetch the content with a normal fetch() call and render it with your own components, whether you are on plain React, Next.js, or Vite.

What is the best way to fetch CMS content in React?

For a public blog, static generation with incremental revalidation is usually best: you fetch at build time, pre-render each page, and serve static HTML from a CDN. For personalized or fast-changing pages, use server-side rendering. Reserve pure client-side fetching for content behind a login that does not need to rank.

Does a headless CMS hurt React SEO?

Not by itself. The risk is rendering content only in the browser, where crawlers may not see it in the first HTML. Render on the server or at build time and your headless React blog is as indexable as any static site. The problem is client-side-only rendering, not headless architecture.

Do I need an SDK to use a headless CMS in React?

No, if the CMS exposes a standard JSON REST API. A normal fetch() is enough, which keeps your dependencies light and your code portable across React setups. Kamaan's REST API Delivery is plain JSON for this reason, so there is no SDK to learn or lock into.

How do I render the article body from a headless CMS in React?

It depends on the delivery format. An HTML string renders with dangerouslySetInnerHTML (sanitize it first if the source is untrusted). Markdown renders with a component like react-markdown. Structured rich text renders through a serializer that maps each node type to a React component.

Can one headless CMS serve multiple React apps?

Yes. Because the content is delivered over a standard REST API, any number of React front ends can fetch the same endpoint. With Kamaan's Multi-Site Management, one account and one dashboard can feed several product blogs, so you are not paying per space or logging into a separate CMS for each app.

Start building with Kamaan

One REST endpoint for every React app you run.

You own the front end. Kamaan owns the content and delivers it as standard JSON to Next.js, Vite React, Astro, or anything that can call a REST API, and auto-translates it into the languages on your plan on every publish. Kamaan Growth covers three product blogs for $49 a month, with auto-translate and hreflang handled for you. First month free.

Start free at kamaan.io

Frequently asked

FAQ · 6 ITEMS
Can you use a headless CMS with React?

Yes, and it is one of the most natural pairings in modern web development. A headless CMS serves content as JSON over an API, and React renders JSON into UI. You fetch the content with a normal `fetch()` call and render it with your own components, whether you are on plain React, Next.js, or Vite.

What is the best way to fetch CMS content in React?

For a public blog, static generation with incremental revalidation is usually best: you fetch at build time, pre-render each page, and serve static HTML from a CDN. For personalized or fast-changing pages, use server-side rendering. Reserve pure client-side fetching for content behind a login that does not need to rank.

Does a headless CMS hurt React SEO?

Not by itself. The risk is rendering content only in the browser, where crawlers may not see it in the first HTML. Render on the server or at build time and your headless React blog is as indexable as any static site. The problem is client-side-only rendering, not headless architecture.

Do I need an SDK to use a headless CMS in React?

No, if the CMS exposes a standard JSON REST API. A normal `fetch()` is enough, which keeps your dependencies light and your code portable across React setups. Kamaan's REST API Delivery is plain JSON for this reason, so there is no SDK to learn or lock into.

How do I render the article body from a headless CMS in React?

It depends on the delivery format. An HTML string renders with `dangerouslySetInnerHTML` (sanitize it first if the source is untrusted). Markdown renders with a component like `react-markdown`. Structured rich text renders through a serializer that maps each node type to a React component.

Can one headless CMS serve multiple React apps?

Yes. Because the content is delivered over a standard REST API, any number of React front ends can fetch the same endpoint. With Kamaan's Multi-Site Management, one account and one dashboard can feed several product blogs, so you are not paying per space or logging into a separate CMS for each app.

Junaid Khalid
Written by
Junaid Khalid

Junaid Khalid is the founder of Kamaan, a headless blog CMS that auto-publishes in five languages and lets you manage every product blog from one dashboard.