Manual
Build a production-ready server-rendered Paragraph CMS integration with localization, feeds, and generated public documents.
This guide shows the architecture behind a production Paragraph CMS integration without assuming a particular web framework. Server-side rendering (SSR) is the recommended delivery model for Paragraph CMS: keep the API key and every CMS request in server-only code, then configure caching in the guide for your deployment platform.
Use this page when your stack is not one of the framework-specific guides or when you want to adapt the same structure to an existing application.
Keep published content server-side
Do not fetch Paragraph CMS content from browser code or client-side data hooks. Render it in your server route, loader, or controller instead.
Install the packages
Install the client and the renderer that matches your UI layer. The
commands below use React; use @paragraphcms/parser-vue or
@paragraphcms/parser-svelte in a Vue or Svelte application.
npm install @paragraphcms/client @paragraphcms/parser-reactpnpm add @paragraphcms/client @paragraphcms/parser-reactyarn add @paragraphcms/client @paragraphcms/parser-reactbun add @paragraphcms/client @paragraphcms/parser-reactCreate the server-only client and route helpers
Put the API key, public site URL, and URL-building helpers in one module
that is never imported by browser code. This example uses /blog for the
default locale and /{locale}/blog for translated content.
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 });
export const site = {
name: "Example blog",
url: "https://example.com",
};
export function blogPath(
locale: string,
defaultLocale: string,
slug?: string,
) {
const prefix = locale === defaultLocale ? "" : `/${locale}`;
const suffix = slug ? `/${slug}` : "";
return `${prefix}/blog${suffix}`;
}
export function publicUrl(pathname: string) {
return new URL(pathname, site.url).toString();
}Load content for SSR routes
Keep the CMS calls in small server functions. They can be used by a route handler, a framework loader, or a server component.
import type { PageSummaryWithSlug, PageWithSlug } from "@paragraphcms/client";
import { client } from "./paragraph.server";
export async function getDefaultLocale() {
const { data, error } = await client.locales.getDefaultLocale();
if (error) {
throw error;
}
return data;
}
export async function getBlogPosts(locale: string): Promise<PageSummaryWithSlug[]> {
const { data, error } = await client.pages.list({
collection: "blog",
language: locale,
requiredSlug: true,
});
if (error) {
throw error;
}
return data;
}
export async function getBlogPost(slug: string): Promise<PageWithSlug> {
const { data, error } = await client.page.getBySlug(slug);
if (error) {
throw error;
}
return data;
}A default-locale post route first loads the default locale, then resolves
the requested slug. A translated route additionally checks that
page.language matches the locale from the URL before rendering it.
Generate public documents manually
Keep public-document generation in server-only helpers too. The helper below deliberately loads locales and page lists in sequence, which makes request volume predictable and keeps the error boundary close to each CMS call.
import { client, blogPath, publicUrl, site } from "./paragraph.server";
function escapeXml(value: string) {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
export async function getPublicUrls() {
const { data: defaultLocale, error: defaultLocaleError } =
await client.locales.getDefaultLocale();
if (defaultLocaleError) {
throw defaultLocaleError;
}
const { data: locales, error: localesError } = await client.locales.list();
if (localesError) {
throw localesError;
}
const urls = [publicUrl("/"), publicUrl("/blog")];
for (const locale of locales) {
const { data: posts, error } = await client.pages.list({
collection: "blog",
language: locale.code,
requiredSlug: true,
});
if (error) {
throw error;
}
if (locale.code !== defaultLocale) {
urls.push(publicUrl(blogPath(locale.code, defaultLocale)));
}
for (const post of posts) {
urls.push(publicUrl(blogPath(locale.code, defaultLocale, post.slug)));
}
}
return urls;
}
export function buildRobotsTxt() {
return `User-agent: *\nAllow: /\nSitemap: ${publicUrl("/sitemap.xml")}\n`;
}
export function buildSitemapXml(urls: string[]) {
const entries = urls
.map((url) => ` <url><loc>${escapeXml(url)}</loc></url>`)
.join("\n");
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries}\n</urlset>`;
}
export function buildLlmsTxt(urls: string[]) {
return `# ${site.name}\n\n## Pages\n${urls.map((url) => `- ${url}`).join("\n")}\n`;
}Expose the documents from server routes
Map the helpers to the equivalent server routes in your framework. Each route responds at request time and does not expose the Paragraph API key.
import {
buildLlmsTxt,
buildRobotsTxt,
buildSitemapXml,
getPublicUrls,
} from "../lib/public-documents.server";
export function robotsTxt() {
return new Response(buildRobotsTxt(), {
headers: { "content-type": "text/plain; charset=utf-8" },
});
}
export async function sitemapXml() {
const urls = await getPublicUrls();
return new Response(buildSitemapXml(urls), {
headers: { "content-type": "application/xml; charset=utf-8" },
});
}
export async function llmsTxt() {
const urls = await getPublicUrls();
return new Response(buildLlmsTxt(urls), {
headers: { "content-type": "text/plain; charset=utf-8" },
});
}Reuse getBlogPosts() to build an RSS route if your application needs a
feed. Escape titles, descriptions, and URLs before interpolating them into
XML, just as buildSitemapXml() does.
Configure caching when you deploy
SSR is the desired Paragraph CMS integration. The next step is to apply a cache policy to public, published routes at the deployment layer. Keep preview, draft, authenticated, and webhook routes out of that shared cache.