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-sveltepnpm add @paragraphcms/client @paragraphcms/parser-svelteyarn add @paragraphcms/client @paragraphcms/parser-sveltebun add @paragraphcms/client @paragraphcms/parser-svelteCreate 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:
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:
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;<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><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.
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 };
};<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().
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 };
};<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.