• Pricing
Get Started

Next.jsAstroReact RouterNuxtSvelteKitManual

Quickstart

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

Set Environment Variables

Add your Paragraph CMS API key to your environment configuration:

.env
PARAGRAPH_API_KEY=your_api_key

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

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 });

Create Example Components

Create two very simple components for your UI layer:

components/blog/blog.tsx
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>
  );
}
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>
  );
}
components/blog/blog.vue
<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>
components/blog/post.vue
<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>
components/blog/blog.svelte
<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>
components/blog/post.svelte
<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.

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

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

SvelteKit

SSR with SvelteKit server loads for a simple Paragraph CMS blog.

Next.js

Build a localized Next.js blog with Paragraph CMS from scratch, including sitemap.xml, robots.txt, llms.txt, and RSS output.

On this page

Install the PackageSet Environment VariablesCreate a ParagraphCMS ClientCreate Example ComponentsBuild /blogBuild /blog/[slug]