SvelteKit Integration Guide

UnfoldCMS works as a headless backend for SvelteKit. Same fetch() pattern as Next.js and Astro — no SvelteKit-specific package required.

Setup

# .env
PUBLIC_CMS_URL=https://your-cms.com
UNFOLDCMS_WEBHOOK_SECRET=your-webhook-secret

Blog Index

// src/routes/blog/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ fetch }) => {
  const res = await fetch(`${process.env.PUBLIC_CMS_URL}/api/v1/posts?per_page=20`);
  if (!res.ok) throw new Error('Failed to load posts');
  const { data: posts, pagination } = await res.json();
  return { posts, pagination };
};
<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
  export let data;
</script>

<h1>Blog</h1>
<ul>
  {#each data.posts as post}
    <li>
      <a href={`/blog/${post.slug}`}>
        <h2>{post.title}</h2>
        <p>{post.excerpt}</p>
      </a>
    </li>
  {/each}
</ul>

Single Post

// src/routes/blog/[slug]/+page.server.ts
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ fetch, params }) => {
  const res = await fetch(`${process.env.PUBLIC_CMS_URL}/api/v1/posts/${params.slug}`);
  if (res.status === 404) throw error(404, 'Post not found');
  if (!res.ok) throw error(500, 'CMS error');
  const { data: post } = await res.json();
  return { post };
};

Webhook Revalidation

SvelteKit pages cached by your reverse proxy (Vercel, Netlify, Cloudflare) can be invalidated via webhook:

// src/routes/api/revalidate/+server.ts
import { json, error } from '@sveltejs/kit';
import crypto from 'node:crypto';

export async function POST({ request }) {
  const raw = await request.text();
  const sig = request.headers.get('signature') || '';
  const expected = crypto
    .createHmac('sha256', process.env.UNFOLDCMS_WEBHOOK_SECRET!)
    .update(raw)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    throw error(401, 'Invalid signature');
  }

  const { event, data } = JSON.parse(raw);

  // Trigger CDN cache invalidation for the affected URL
  // (provider-specific — example for Vercel)
  if (event === 'post.published' || event === 'post.updated') {
    await fetch(`https://api.vercel.com/v1/integrations/deploy/${process.env.VERCEL_HOOK_ID}`, {
      method: 'POST',
    });
  }

  return json({ ok: true });
}

Register the webhook in UnfoldCMS pointing at https://your-site.com/api/revalidate.

Layout with Site Settings

// src/routes/+layout.server.ts
export const load = async ({ fetch }) => {
  const res = await fetch(`${process.env.PUBLIC_CMS_URL}/api/v1/settings/public`);
  const { data: settings } = await res.json();
  return { settings };
};
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  export let data;
</script>

<header>
  <img src={data.settings['general.logo_url']} alt={data.settings['general.site_name']} />
  <h1>{data.settings['general.site_name']}</h1>
</header>

<slot />

Categories

// src/routes/category/[slug]/+page.server.ts
import { error } from '@sveltejs/kit';

export const load = async ({ fetch, params }) => {
  const [catRes, postsRes] = await Promise.all([
    fetch(`${process.env.PUBLIC_CMS_URL}/api/v1/categories/${params.slug}`),
    fetch(`${process.env.PUBLIC_CMS_URL}/api/v1/categories/${params.slug}/posts`),
  ]);

  if (catRes.status === 404) throw error(404, 'Category not found');

  const { data: category } = await catRes.json();
  const { data: posts, pagination } = await postsRes.json();

  return { category, posts, pagination };
};
// src/routes/search/+page.server.ts
export const load = async ({ fetch, url }) => {
  const q = url.searchParams.get('q') || '';
  if (q.length < 2) return { q, results: null };

  const res = await fetch(
    `${process.env.PUBLIC_CMS_URL}/api/v1/search?q=${encodeURIComponent(q)}`
  );
  const { data: results } = await res.json();
  return { q, results };
};

Common Pitfalls

  • Use the framework fetch, not global fetch — pass fetch from load parameters so SvelteKit handles caching and SSR.
  • Always set Accept: application/json on auth endpoints to ensure JSON 401s instead of HTML redirects.
  • Image URLs are absolute. Don't prepend the CMS URL — featured_image.large is complete.
  • Don't commit admin tokens. Use $env/dynamic/private for any token that ships in load functions.