• Features
  • Pricing
Get Started

Next.jsAstroReact RouterNuxtSvelteKitManual

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-react
pnpm add @paragraphcms/client @paragraphcms/parser-react
yarn add @paragraphcms/client @paragraphcms/parser-react
bun add @paragraphcms/client @paragraphcms/parser-react

Create 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.

lib/paragraph.server.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 });

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.

lib/blog.server.ts
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.

lib/public-documents.server.ts
import { client, blogPath, publicUrl, site } from "./paragraph.server";

function escapeXml(value: string) {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&apos;");
}

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.

server/public-documents.ts
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.

SvelteKit

Build a localized, server-rendered SvelteKit blog with Paragraph CMS and manually generated public documents.

Examples

Starter projects and advanced integration examples for common Paragraph CMS stacks.

On this page

Install the packagesCreate the server-only client and route helpersLoad content for SSR routesGenerate public documents manuallyExpose the documents from server routesConfigure caching when you deploy