Manual
Framework-agnostic SSR, SSG, prerendering, and on-demand rendering patterns for integrating Paragraph CMS manually.
This quickstart uses the shared Paragraph CMS client directly and walks through the core setup for fetching and rendering Paragraph CMS content in any JavaScript or TypeScript application, using a pattern that works for SSR, SSG, prerendering, and on-demand rendering as long as the API key stays on the server.
Paragraph CMS ships with official renderers for React, Vue, and Svelte ecosystems:
@paragraphcms/parser-react- for React frameworks and applications@paragraphcms/parser-vue- for Vue frameworks and applications@paragraphcms/parser-svelte- for Svelte frameworks and applications
The core @paragraphcms/client works in any JavaScript runtime.
Prefer a framework-specific version? Use the Next.js, Astro, Nuxt, React Router, or SvelteKit quickstart when you want file-level routing examples.
Avoid client-side content fetching
Paragraph CMS is designed for SSG, prerendering, SSR, and cached on-demand rendering. Do not fetch published article content from the browser or client components. Keep content requests on the server and serve cached or pre-rendered output, otherwise repeated client-side requests can run into rate limits.
Install the Package
Add the Paragraph CMS client and the renderer for your UI layer. The
commands below use the React renderer; in Vue or Svelte apps, replace
@paragraphcms/parser-react with @paragraphcms/parser-vue or
@paragraphcms/parser-svelte:
npm install @paragraphcms/client @paragraphcms/parser-reactpnpm add @paragraphcms/client @paragraphcms/parser-reactyarn add @paragraphcms/client @paragraphcms/parser-reactbun add @paragraphcms/client @paragraphcms/parser-reactSet Environment Variables
Add your Paragraph CMS API key to your environment configuration:
PARAGRAPH_API_KEY=your_api_keyCreate a ParagraphCMS Client
Initialize a shared client instance. The example below uses process.env;
if your runtime exposes server-side environment variables differently,
read the API key from the equivalent server-only source:
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 });Create Example Components
Create two very simple components for your UI layer:
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>
);
}<script setup lang="ts">
import type { PageSummaryWithSlug } from "@paragraphcms/client";
defineProps<{
posts: PageSummaryWithSlug[];
}>();
</script>
<template>
<main>
<h1>Blog</h1>
<ul>
<li v-for="post in posts" :key="post.id">
<a :href="`/blog/${post.slug}`">{{ post.title }}</a>
</li>
</ul>
</main>
</template><script setup lang="ts">
import type { PageWithSlug } from "@paragraphcms/client";
import { ParagraphContent } from "@paragraphcms/parser-vue";
defineProps<{
page: PageWithSlug;
}>();
</script>
<template>
<main>
<h1>{{ page.title }}</h1>
<ParagraphContent :content="page.content" />
</main>
</template><script lang="ts">
import type { PageSummaryWithSlug } from "@paragraphcms/client";
export let posts: PageSummaryWithSlug[];
</script>
<main>
<h1>Blog</h1>
<ul>
{#each posts as post}
<li>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
{/each}
</ul>
</main><script lang="ts">
import type { PageWithSlug } from "@paragraphcms/client";
import { renderParagraphContentHtml } from "@paragraphcms/parser-svelte";
export let page: PageWithSlug;
$: content = renderParagraphContentHtml({ content: page.content });
</script>
<main>
<h1>{page.title}</h1>
{@html content ?? ""}
</main>Build /blog
Build the /blog index route, server function, or build step with
client.pages.list(). Setting requiredSlug: true narrows the result to
PageSummaryWithSlug[], so post.slug is safe to use directly.
import type { PageSummaryWithSlug } from "@paragraphcms/client";
import { client } from "../paragraph.config";
export async function getBlogPosts(): Promise<PageSummaryWithSlug[]> {
const { data: posts, error } = await client.pages.list({
requiredSlug: true,
});
if (error) {
throw error;
}
return posts;
}Call getBlogPosts() from your route handler, loader, server component,
or build script and pass the result into the Blog component for your UI
layer.
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, server function, or build step with
client.page.getBySlug().
import type { PageWithSlug } from "@paragraphcms/client";
import { client } from "../paragraph.config";
export async function getBlogPost(slug: string): Promise<PageWithSlug> {
const { data: page, error } = await client.page.getBySlug(slug);
if (error) {
throw error;
}
return page;
}This works the same way in SSR routes, SSG or prerender entry generation, and on-demand rendering flows where the slug comes from the URL.
Once this is wired, the rest of your application can be built around the rendered Paragraph CMS content.