• Features
  • Pricing
Get Started

Next.jsAstroReact RouterNuxtSvelteKitManual

Quickstart

React Router

SSR with React Router framework-mode loaders for a simple Paragraph CMS blog.

This quickstart uses React Router framework mode and walks through the core setup for fetching and rendering Paragraph CMS content in loader-based route modules, with SSR as the baseline because loaders run on the server and keep the API key out of the browser bundle.

Want a ready-made project instead? Use the paragraphcms/react-router-starter, which ships with the same integration pattern already wired into a blog.

Install the Package

Add the Paragraph CMS client and React renderer to your React Router app:

npm install @paragraphcms/client @paragraphcms/parser-react
pnpm add @paragraphcms/client @paragraphcms/parser-react
yarn add @paragraphcms/client @paragraphcms/parser-react
bun add @paragraphcms/client @paragraphcms/parser-react

Enable SSR

Keep server rendering enabled in react-router.config.ts so your Paragraph CMS API key stays on the server:

react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
  // Config options...
  // Server-side render by default, to enable SPA mode set this to `false`
  ssr: true,
} satisfies Config;

Set Environment Variables

Create a .env file in the project root:

.env
PARAGRAPH_API_KEY=your_api_key

Prerender Known Blog Routes

If you want the blog index and known posts prerendered at build time, add prerender() in react-router.config.ts and fetch the slugs from Paragraph CMS.

react-router.config.ts runs in the build config layer, not inside your route loaders, so load the env explicitly with Vite's loadEnv() and create the client inside prerender().

react-router.config.ts
import { Client } from "@paragraphcms/client";
import type { Config } from "@react-router/dev/config";
import { loadEnv } from "vite";

export default {
  async prerender() {
    const env = loadEnv("", process.cwd(), "");
    const apiKey = env.PARAGRAPH_API_KEY || process.env.PARAGRAPH_API_KEY;

    if (!apiKey) {
      throw new Error("PARAGRAPH_API_KEY environment variable is not set");
    }

    const client = new Client({ apiKey });
    const { data, error } = await client.pages.list({ requiredSlug: true });

    if (error) {
      throw error;
    }

    return ["/blog", ...data.map((page) => `/blog/${page.slug}`)];
  },
  ssr: true,
} satisfies Config;

This keeps the prerendered /blog/:slug paths aligned with the pages currently stored in Paragraph CMS.

Create a Paragraph CMS Client

Create paragraph.config.ts and initialize a shared client. In React Router framework mode, route loaders run on the server, so reading from process.env keeps the API key out of the browser bundle.

paragraph.config.ts
import { Client } from "@paragraphcms/client";

const apiKey = process.env.PARAGRAPH_API_KEY;

if (!apiKey) {
  throw new Error("PARAGRAPH_API_KEY environment variable is not set");
}

export const client = new Client({ apiKey });

Declare the Blog Routes

Register your routes in app/routes.ts:

app/routes.ts
import { type RouteConfig, route } from "@react-router/dev/routes";

export default [
  route("blog", "routes/blog.tsx"),
  route("blog/:slug", "routes/blog.$slug.tsx"),
] satisfies RouteConfig;

Create Example Components

Create two very simple components that we will render in the next steps:

app/components/blog/blog.tsx
import type { PageSummaryWithSlug } from "@paragraphcms/client";
import { Link } from "react-router";

export function Blog({ posts }: { posts: PageSummaryWithSlug[] }) {
  return (
    <main>
      <h1>Blog</h1>

      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <Link to={`/blog/${post.slug}`}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </main>
  );
}
app/components/blog/post.tsx
import type { PageWithSlug } from "@paragraphcms/client";
import { ParagraphContent } from "@paragraphcms/parser-react";

export function Post({ page }: { page: PageWithSlug }) {
  return (
    <main>
      <h1>{page.title}</h1>
      <ParagraphContent content={page.content} />
    </main>
  );
}

Build /blog

Build the /blog route with client.pages.list(). Setting requiredSlug: true narrows the result to PageSummaryWithSlug[], so post.slug is safe to use directly.

app/routes/blog.tsx
import { useLoaderData } from "react-router";
import { Blog } from "../components/blog/blog";
import { client } from "../../paragraph.config";

export async function loader() {
  const { data, error } = await client.pages.list({
    requiredSlug: true,
  });

  if (error) {
    throw error;
  }

  return { posts: data };
}

export default function BlogRoute() {
  const { posts } = useLoaderData<typeof loader>();

  return <Blog posts={posts} />;
}

By default, client.pages.list() returns published pages only. If you want to include unpublished pages too or limit the query to a single collection, pass additional parameters under the same call:

client.pages.list({
  requiredSlug: true,
  published: false,
  collection: "blog",
});

Build /blog/:slug

Build the /blog/:slug route with a loader that resolves the route param through client.page.getBySlug().

app/routes/blog.$slug.tsx
import { useLoaderData } from "react-router";
import type { Route } from "./+types/blog.$slug";
import { Post } from "../components/blog/post";
import { client } from "../../paragraph.config";

export async function loader({ params }: Route.LoaderArgs) {
  const { data, error } = await client.page.getBySlug(params.slug!);

  if (error) {
    throw error;
  }

  return { data };
}

export default function BlogPostRoute() {
  const { data } = useLoaderData<typeof loader>();

  return <Post page={data} />;
}

This keeps the route aligned with the slugs stored in Paragraph CMS while letting React Router load each post on the server.

Once this is wired, the rest of the page can be standard React Router UI around the rendered Paragraph content.

Astro

Static-first SSG and build-time prerendering in Astro for a simple Paragraph CMS blog.

Nuxt

SSR plus Nitro prerendering in Nuxt for a simple Paragraph CMS blog.

On this page

Install the PackageEnable SSRSet Environment VariablesPrerender Known Blog RoutesCreate a Paragraph CMS ClientDeclare the Blog RoutesCreate Example ComponentsBuild /blogBuild /blog/:slug