Svelte · SvelteKit · 2026

CMS for Svelte

ElmapiCMS is a self-hosted headless CMS with a REST API and official JavaScript SDK. This guide shows how to connect it to Svelte and SvelteKit: configure server-only env vars, fetch content in load functions, build slug routes, and refresh deployments with webhooks.

Why

A good fit for Svelte projects

SvelteKit gives you fast pages and a clean server/client split. ElmapiCMS adds editor-managed content without forcing content teams into Git workflows.

This setup works well for marketing sites, docs, and blogs where you want lightweight frontend performance plus a flexible API-driven content backend.

If you are comparing options first, start with the headless CMS overview and use this page as the implementation reference.

Setup

Install the SDK and configure environment variables

Install the official JavaScript SDK in your Svelte or SvelteKit project:

npm install @elmapicms/js-sdk

Add your credentials in environment variables. Keep API tokens server-side and do not expose them through client bundles.

ELMAPI_API_URL=https://your-cms.example.com/api
ELMAPI_PROJECT_ID=your-project-uuid
ELMAPI_API_KEY=your-token

Find Project ID and API token management under Settings → API Access.

Client

Create a shared server-only CMS client

Put SDK initialization in a single server utility file so all routes and pages share the same configuration.

// src/lib/server/elmapi.ts
import { createClient } from "@elmapicms/js-sdk";
import { ELMAPI_API_KEY, ELMAPI_API_URL, ELMAPI_PROJECT_ID } from "$env/static/private";
 
if (!ELMAPI_API_URL || !ELMAPI_API_KEY || !ELMAPI_PROJECT_ID) {
  throw new Error("Missing ElmapiCMS environment variables");
}
 
export const cms = createClient(ELMAPI_API_URL, ELMAPI_API_KEY, ELMAPI_PROJECT_ID);

The returned client includes getEntries, getEntry, getCollections, and getAssets. Full endpoint behavior is documented in API introduction.

Pages

Fetch content in server load functions

In SvelteKit, +page.server.ts is the safest place for authenticated CMS calls. It keeps secrets off the client and gives predictable SSR behavior.

List route

Fetch posts with pagination and normalize response shape once so page components always receive a stable posts array.

// src/routes/posts/+page.server.ts
import type { PageServerLoad } from "./$types";
import { cms } from "$lib/server/elmapi";
 
export const load: PageServerLoad = async ({ url }) => {
  const page = Number(url.searchParams.get("page") || 1);
 
  const result = await cms.getEntries("posts", {
    state: "published",
    sort: "created_at:desc",
    paginate: 12,
    page,
  });
 
  const posts = Array.isArray(result) ? result : result.data;
  return { posts, raw: result };
};

Slug detail route

Query by slug using a where clause and throw a proper 404 when no entry matches.

// src/routes/posts/[slug]/+page.server.ts
import { error } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
import { cms } from "$lib/server/elmapi";
 
export const load: PageServerLoad = async ({ params }) => {
  const result = await cms.getEntries("posts", {
    where: { slug: params.slug },
    limit: 1,
    state: "published",
  });
 
  const rows = Array.isArray(result) ? result : result.data;
  const post = rows[0];
 
  if (!post) {
    error(404, "Post not found");
  }
 
  return { post };
};
Queries

Filtering, sorting, and pagination

Use where for filtering, sort with field:asc or field:desc, and paginate + page for list endpoints.

// Equality filter
const featured = await cms.getEntries("posts", {
  where: { featured: true },
  sort: "created_at:desc",
  state: "published",
});
 
// Operator filter
const recent = await cms.getEntries("posts", {
  where: { created_at: { gte: "2026-01-01" } },
  sort: "created_at:desc",
  paginate: 10,
  page: 1,
});
 
// Fetch by UUID
const entry = await cms.getEntry("posts", "entry-uuid-here");

For advanced operators, relation filters, and query examples, see advanced filtering and filtering examples.

Fresh Content

Webhooks and deployment refresh

Static SvelteKit deployments are typically refreshed by webhook-triggered rebuilds. For SSR deployments, fetch on request and cache at CDN or edge level.

// src/routes/api/revalidate/+server.ts
import { createHmac, timingSafeEqual } from "node:crypto";
import type { RequestHandler } from "./$types";
 
export const POST: RequestHandler = async ({ request }) => {
  const rawBody = await request.text();
  const signature = request.headers.get("x-webhook-signature");
  const secret = process.env.ELMAPI_REVALIDATE_SECRET;
 
  if (secret) {
    if (!signature) return new Response("Missing signature", { status: 401 });
 
    const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
    const valid =
      signature.length === expected.length &&
      timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
 
    if (!valid) return new Response("Invalid signature", { status: 401 });
  }
 
  // Trigger platform build hook for static deployments.
  if (process.env.DEPLOY_HOOK_URL) {
    await fetch(process.env.DEPLOY_HOOK_URL, { method: "POST" });
  }
 
  return new Response(JSON.stringify({ revalidated: true }), {
    headers: { "content-type": "application/json" },
  });
};

Configure webhooks under Project Settings → Webhooks, and review production queue setup in the deployment webhook docs.

Checklist

Before you deploy

  • API keys stay in server-only environment variables.
  • Public routes request state: "published" content.
  • List logic handles both paginated and non-paginated SDK response shapes.
  • Slug routes return proper 404s for missing entries.
  • Webhook signature validation and deploy hook flow are tested.

Last updated: June 28, 2026