• Features
  • Pricing
Get Started

Next.jsAstroReact RouterNuxtSvelteKitManual

SvelteKit

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

This guide extends the SvelteKit quickstart with localized blog routes and manual robots.txt, sitemap.xml, and llms.txt endpoints. Server-side rendering (SSR) is the recommended delivery model for Paragraph CMS.

Install the packages

npm install @paragraphcms/client @paragraphcms/parser-svelte

Set the API key in .env. $env/dynamic/private ensures it remains available only to server modules.

.env
PARAGRAPH_API_KEY=your_api_key

Create server-only CMS and URL helpers

Create a blog collection in Paragraph CMS, then add these helpers below src/lib/server.

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

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

Server-render default-locale pages

Server load functions run only on the server. The unprefixed routes use the workspace default locale.

src/routes/blog/+page.server.ts
import { client } from "$lib/server/paragraph";

export async function load() {
  const { data: defaultLocale, error: defaultLocaleError } =
    await client.locales.getDefaultLocale();

  if (defaultLocaleError) {
    throw defaultLocaleError;
  }

  const { data: posts, error } = await client.pages.list({
    collection: "blog",
    language: defaultLocale,
    requiredSlug: true,
  });

  if (error) {
    throw error;
  }

  return { defaultLocale, locale: defaultLocale, posts };
}
src/routes/blog/[slug]/+page.server.ts
import { error } from "@sveltejs/kit";

import { client } from "$lib/server/paragraph";

export async function load({ params }) {
  const { data: defaultLocale, error: defaultLocaleError } =
    await client.locales.getDefaultLocale();

  if (defaultLocaleError) {
    throw defaultLocaleError;
  }

  const { data: page, error: pageError } = await client.page.getBySlug(
    params.slug,
  );

  if (pageError) {
    throw pageError;
  }

  if (page.language !== defaultLocale) {
    throw error(404, "Not found");
  }

  return { defaultLocale, locale: defaultLocale, page };
}

Add translated SSR routes

Add src/routes/[locale]/blog/+page.server.ts and src/routes/[locale]/blog/[slug]/+page.server.ts. The list loader uses params.locale as the language query; both loaders reject a repeated default locale. The post loader also verifies page.language.

src/routes/[locale]/blog/+page.server.ts
import { error } from "@sveltejs/kit";

import { client } from "$lib/server/paragraph";

export async function load({ params }) {
  const { data: defaultLocale, error: defaultLocaleError } =
    await client.locales.getDefaultLocale();

  if (defaultLocaleError) {
    throw defaultLocaleError;
  }

  if (params.locale === defaultLocale) {
    throw error(404, "Not found");
  }

  const { data: posts, error: postsError } = await client.pages.list({
    collection: "blog",
    language: params.locale,
    requiredSlug: true,
  });

  if (postsError) {
    throw postsError;
  }

  return { defaultLocale, locale: params.locale, posts };
}

Render the server data

Your page components receive the server data as a prop. Use the same Blog and Post components from the quickstart.

src/routes/blog/+page.svelte
<script lang="ts">
  import Blog from "$lib/components/blog/blog.svelte";

  export let data;
</script>

<Blog
  defaultLocale={data.defaultLocale}
  locale={data.locale}
  pages={data.posts}
/>

Keep the Paragraph client inside the +page.server.ts modules rather than importing it into these Svelte components.

Build public documents manually

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

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 robotsTxt() {
  return `User-agent: *\nAllow: /\nSitemap: ${publicUrl("/sitemap.xml")}\n`;
}

export function sitemapXml(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 llmsTxt(urls: string[]) {
  return `# ${site.name}\n\n## Pages\n${urls.map((url) => `- ${url}`).join("\n")}\n`;
}

Add the public document endpoints

src/routes/robots.txt/+server.ts
import { robotsTxt } from "$lib/server/public-documents";

export function GET() {
  return new Response(robotsTxt(), {
    headers: { "content-type": "text/plain; charset=utf-8" },
  });
}
src/routes/sitemap.xml/+server.ts
import { getPublicUrls, sitemapXml } from "$lib/server/public-documents";

export async function GET() {
  const urls = await getPublicUrls();

  return new Response(sitemapXml(urls), {
    headers: { "content-type": "application/xml; charset=utf-8" },
  });
}
src/routes/llms.txt/+server.ts
import { getPublicUrls, llmsTxt } from "$lib/server/public-documents";

export async function GET() {
  const urls = await getPublicUrls();

  return new Response(llmsTxt(urls), {
    headers: { "content-type": "text/plain; charset=utf-8" },
  });
}

Deploy the SSR application

Cache only public published pages and document routes at deployment. Preview, draft, authenticated, and webhook routes must bypass shared caching.

Nuxt

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

Manual

Build a production-ready server-rendered Paragraph CMS integration with localization, feeds, and generated public documents.

On this page

Install the packagesCreate server-only CMS and URL helpersServer-render default-locale pagesAdd translated SSR routesRender the server dataBuild public documents manuallyAdd the public document endpointsDeploy the SSR application