Astro
Static-first SSG and build-time prerendering in Astro for a simple Paragraph CMS blog.
This quickstart uses Astro's file-based routing and walks through the core
setup for fetching and rendering Paragraph CMS content in a static-first Astro
app, using SSG and build-time prerendering for known /blog/[slug] routes
because the slug list can be derived from the CMS with getStaticPaths().
Want a ready-made project instead? Use the
paragraphcms/astro-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 Astro app. If you are not already using React components in Astro, install the React integration too:
npm install @paragraphcms/client @paragraphcms/parser-react react react-dom
npm install -D @astrojs/reactpnpm add @paragraphcms/client @paragraphcms/parser-react react react-dom
pnpm add -D @astrojs/reactyarn add @paragraphcms/client @paragraphcms/parser-react react react-dom
yarn add -D @astrojs/reactbun add @paragraphcms/client @paragraphcms/parser-react react react-dom
bun add -d @astrojs/reactRegister the React integration in astro.config.mjs:
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
export default defineConfig({
integrations: [react()],
});Create a Paragraph CMS Client
Create paragraph.config.ts and initialize a shared client. In Astro,
server-side environment variables are read through import.meta.env:
import { Client } from "@paragraphcms/client";
const apiKey = import.meta.env.PARAGRAPH_API_KEY;
if (!apiKey) {
throw new Error("PARAGRAPH_API_KEY environment variable is not set");
}
export const client = new Client({ apiKey });Create Example Components
Create two very simple components that we will render in the next steps:
import type { PageSummaryWithSlug } from "@paragraphcms/client";
export function Blog({ posts }: { posts: PageSummaryWithSlug[] }) {
return (
<main>
<h1>Blog</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
</main>
);
}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 index route with client.pages.list(). Setting
requiredSlug: true narrows the result to PageSummaryWithSlug[], so
post.slug is safe to use directly.
---
import { Blog } from "../../components/blog/blog";
import { client } from "../../../paragraph.config";
const { data, error } = await client.pages.list({
requiredSlug: true,
});
if (error) {
throw error;
}
---
<Blog posts={data} />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 by generating all known slugs with
getStaticPaths() and resolving each page with client.page.getBySlug().
---
import { Post } from "../../components/blog/post";
import { client } from "../../../paragraph.config";
export async function getStaticPaths() {
const { data, error } = await client.pages.list({
requiredSlug: true,
});
if (error) {
throw error;
}
return data.map((page) => ({
params: {
slug: page.slug,
},
}));
}
const { data: page, error } = await client.page.getBySlug(Astro.params.slug!);
if (error) {
throw error;
}
---
<Post page={page} />This keeps the route aligned with the slugs stored in Paragraph CMS.
Once this is wired, the rest of the page can be standard Astro UI around the rendered Paragraph content.