• Features
  • Pricing
Get Started

Next.jsAstroReact RouterNuxtSvelteKitManual

Quickstart

SvelteKit

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

This quickstart uses SvelteKit's file-based routing and server load functions and walks through the core setup for fetching and rendering Paragraph CMS content in a SvelteKit app, with SSR as the default so server load functions keep the API key on the server.

Want a ready-made project instead? Use the paragraphcms/sveltekit-starter, which ships with the same integration pattern already wired into a blog.

Install the Package

Add the Paragraph CMS client and Svelte rendering helpers to your SvelteKit app:

npm install @paragraphcms/client @paragraphcms/parser-svelte
pnpm add @paragraphcms/client @paragraphcms/parser-svelte
yarn add @paragraphcms/client @paragraphcms/parser-svelte
bun add @paragraphcms/client @paragraphcms/parser-svelte

Set Environment Variables

Create a .env file in the project root:

.env
PARAGRAPH_API_KEY=your_api_key

Create a Paragraph CMS Client

Create paragraph.config.ts and initialize a shared client. In SvelteKit, private server-side environment variables are read through $env/dynamic/private:

paragraph.config.ts
import { env } from "$env/dynamic/private";
import { Client } from "@paragraphcms/client";

const apiKey = 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:

If your project forces Svelte 5 runes mode globally, keep these files in legacy mode or translate the snippets to runes syntax. For example:

svelte.config.js
import adapter from "@sveltejs/adapter-auto";

const legacySvelteFiles = [
  "/src/lib/components/blog/blog.svelte",
  "/src/lib/components/blog/post.svelte",
  "/src/routes/blog/+page.svelte",
  "/src/routes/blog/[slug]/+page.svelte",
];

/** @type {import("@sveltejs/kit").Config} */
const config = {
  compilerOptions: {
    runes: ({ filename }) => {
      const normalizedFilename = filename.replace(/\\/g, "/");

      if (normalizedFilename.includes("/node_modules/")) {
        return undefined;
      }

      return !legacySvelteFiles.some((file) =>
        normalizedFilename.endsWith(file),
      );
    },
  },
  kit: {
    adapter: adapter(),
  },
};

export default config;
src/lib/components/blog/blog.svelte
<script lang="ts">
  import type { PageSummaryWithSlug } from "@paragraphcms/client";

  export let pages: PageSummaryWithSlug[];
</script>

<main>
  <h1>Blog</h1>

  <ul>
    {#each pages as page}
      <li>
        <a href={`/blog/${page.slug}`}>{page.title}</a>
      </li>
    {/each}
  </ul>
</main>
src/lib/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 with a server load function. Setting requiredSlug: true narrows the result to PageSummaryWithSlug[], so page.slug is safe to use directly.

src/routes/blog/+page.server.ts
import type { PageServerLoad } from "./$types";
import { client } from "../../../paragraph.config";

export const load: PageServerLoad = async () => {
  const { data, error } = await client.pages.list({
    requiredSlug: true,
  });

  if (error) {
    throw error;
  }

  return { pages: data };
};
src/routes/blog/+page.svelte
<script lang="ts">
  import Blog from "$lib/components/blog/blog.svelte";
  import type { PageSummaryWithSlug } from "@paragraphcms/client";

  export let data: {
    pages: PageSummaryWithSlug[];
  };
</script>

<Blog pages={data.pages} />

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 reading the slug from params and resolving the page with client.page.getBySlug().

src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from "./$types";
import { client } from "../../../../paragraph.config";

export const load: PageServerLoad = async ({ params }) => {
  const { data: page, error } = await client.page.getBySlug(params.slug);

  if (error) {
    throw error;
  }

  return { page };
};
src/routes/blog/[slug]/+page.svelte
<script lang="ts">
  import Post from "$lib/components/blog/post.svelte";
  import type { PageWithSlug } from "@paragraphcms/client";

  export let data: {
    page: PageWithSlug;
  };
</script>

<Post page={data.page} />

This keeps the route aligned with the slugs stored in Paragraph CMS while keeping the API key on the server.

Once this is wired, the rest of the page can be standard SvelteKit UI around rendered Paragraph content.

Nuxt

SSR plus Nitro prerendering in Nuxt for a simple Paragraph CMS blog.

Manual

Framework-agnostic SSR, SSG, prerendering, and on-demand rendering patterns for integrating Paragraph CMS manually.

On this page

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