SvelteKit Starter
Using Adapto CMS with SvelteKit
This guide covers integrating every Adapto CMS content type into a SvelteKit 2 + Svelte 5 project. For SDK method signatures and type definitions, see the SDK Reference.
Prerequisites
- An Adapto CMS account with an API key (found in the backoffice under Developer Tools → API Keys)
- Node.js 18+
Quick Start
git clone https://github.com/adaptocms/adapto-sveltekit-client.git
cd adapto-sveltekit-client
npm install
cp .env.example .env Edit .env and set your keys, then run npm run dev.
Configuration
Environment variables are loaded from SvelteKit's $env/dynamic/private and re-exported from src/config.ts.
// src/config.ts
import { env } from '$env/dynamic/private';
export const API_URL = env.ADAPTO_API_URL ?? '';
export const API_KEY = env.ADAPTO_API_KEY ?? '';
export const IS_DEV = env.DEV === 'true';
export const DEFAULT_LANGUAGE = 'en-US';
export const PAGE_SIZE = 10; Articles
Listing articles
The summary field is auto-generated from the article's first paragraph — see the SDK Reference for the full IArticle type.
// src/routes/[lang]/articles/+page.server.ts
import { adapto } from '$lib/adapto-sdk';
import { PAGE_SIZE } from '../../../config';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const { lang } = params;
const { items: articles, total, pages: totalPages } = await adapto.articles.list({
language: lang,
status: 'published',
page: 1,
limit: PAGE_SIZE,
});
return { articles, total, totalPages, currentPage: 1 };
}; <!-- src/routes/[lang]/articles/+page.svelte -->
<script lang="ts">
let { data } = $props();
</script>
<p>{data.total} articles</p>
{#each data.articles.items as article (article.id)}
<article>
<a href="/{data.lang}/articles/{article.slug}">{article.title}</a>
{#if article.summary}<p>{article.summary}</p>{/if}
{#if article.published_at}
<time>{new Date(article.published_at).toLocaleDateString()}</time>
{/if}
</article>
{/each} Article detail
article.content is an HTML string. For articles with embedded media, pass it through hydrateMediaPlacements before rendering — see the SDK Reference.
// src/routes/[lang]/articles/[slug]/+page.server.ts
import { adapto } from '$lib/adapto-sdk';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const { slug } = params;
try {
const article = await adapto.articles.getBySlug(slug);
return { article };
} catch {
error(404, 'Article not found');
}
}; <!-- src/routes/[lang]/articles/[slug]/+page.svelte -->
<script lang="ts">
let { data } = $props();
</script>
<svelte:head><title>{data.article.title}</title></svelte:head>
<article>
<h1>{data.article.title}</h1>
<p>By {data.article.author}</p>
<div>{@html data.article.content}</div>
</article> Pagination
Page 1 lives at /[lang]/articles. Pages 2+ live at /[lang]/articles/page/[pageNum].
// src/routes/[lang]/articles/page/[pageNum]/+page.server.ts
import { adapto } from '$lib/adapto-sdk';
import { error } from '@sveltejs/kit';
import { PAGE_SIZE } from '../../../../../config';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const { lang, pageNum } = params;
const page = parseInt(pageNum, 10);
if (isNaN(page) || page < 2) error(404, 'Invalid page number');
const { items: articles, pages: totalPages } = await adapto.articles.list({
language: lang, status: 'published', page, limit: PAGE_SIZE,
});
if (page > totalPages) error(404, 'Page not found');
return { articles, currentPage: page, totalPages };
}; Categories
See the SDK Reference for the full ICategory type, including parent_id and description.
Listing categories
// src/routes/[lang]/articles/categories/+page.server.ts
export const load: PageServerLoad = async ({ params }) => {
const { lang } = params;
const { items: categories } = await adapto.categories.list({ language: lang });
return { categories: categories.filter((c) => c.parent_id === null) };
}; Category detail with filtered articles
Articles store category membership as an array of category IDs in their categories field. Pass the category's id to the category filter when listing articles.
// src/routes/[lang]/articles/categories/[slug]/+page.server.ts
import { adapto } from '$lib/adapto-sdk';
import { error } from '@sveltejs/kit';
import { PAGE_SIZE } from '../../../../../config';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const { lang, slug } = params;
const category = await adapto.categories.getBySlug(slug).catch(() => null);
if (!category) error(404, 'Category not found');
const { items: articles, pages: totalPages } = await adapto.articles.list({
language: lang,
status: 'published',
category: category.id,
page: 1,
limit: PAGE_SIZE,
});
return { category, articles, totalPages, currentPage: 1 };
}; <script lang="ts">
let { data } = $props();
</script>
<h1>{data.category.name}</h1>
{#if data.category.description}
<div>{@html data.category.description}</div>
{/if}
{#each data.articles as article (article.id)}
<a href="/{data.lang}/articles/{article.slug}">{article.title}</a>
{/each} Subcategories
const subcategories = await adapto.categories.getSubcategories(category.id); Pages
CMS-managed pages (Home, About, Contact, etc.) are fetched by slug. See the SDK Reference for the full IPage type including menu_label.
// src/routes/[lang]/[slug]/+page.server.ts
import { adapto } from '$lib/adapto-sdk';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const { slug } = params;
try {
const page = await adapto.pages.getBySlug(slug);
return { page };
} catch {
error(404, 'Page not found');
}
}; <script lang="ts">
let { data } = $props();
</script>
<svelte:head><title>{data.page.title}</title></svelte:head>
<main>
<h1>{data.page.title}</h1>
<div>{@html data.page.content}</div>
</main> Custom Collections
A collection defines a schema via its fields array. Items store their values in a plain data object keyed by field name. See the SDK Reference for all ICollectionField types.
Collection list
// src/routes/[lang]/[collection_slug]/+page.server.ts
import { adapto } from '$lib/adapto-sdk';
import { error } from '@sveltejs/kit';
import { PAGE_SIZE } from '../../../config';
export const load = async ({ params }) => {
const { lang, collection_slug } = params;
const collection = await adapto.collections.getBySlug(collection_slug).catch(() => null);
if (!collection) error(404, 'Collection not found');
const { items, pages: totalPages } = await adapto.collections.listItems(
collection.id,
{ language: lang, status: 'published', page: 1, limit: PAGE_SIZE },
);
return { collection, items, totalPages, currentPage: 1 };
}; Collection item detail
// src/routes/[lang]/[collection_slug]/[item_slug]/+page.server.ts
import { adapto } from '$lib/adapto-sdk';
import { error } from '@sveltejs/kit';
export const load = async ({ params }) => {
const { collection_slug, item_slug } = params;
try {
const collection = await adapto.collections.getBySlug(collection_slug);
const item = await adapto.collections.getItemBySlug(collection.id, item_slug);
return { collection, item };
} catch {
error(404, 'Item not found');
}
}; <script lang="ts">
let { data } = $props();
</script>
<h1>{data.item.title}</h1>
{#each Object.entries(data.item.data) as [fieldName, value] (fieldName)}
{@const field = data.collection.fields.find((f) => f.name === fieldName)}
{#if field && value !== null && value !== ''}
<section>
<h3>{field.label}</h3>
{#if field.type === 'rich_text'}
<div>{@html String(value)}</div>
{:else if field.type === 'image'}
<img src={String(value)} alt={field.label} />
{:else if field.type === 'url' || field.type === 'email'}
<a href={String(value)}>{String(value)}</a>
{:else if field.type === 'boolean'}
<span>{value ? 'Yes' : 'No'}</span>
{:else if field.type === 'multi_select'}
{#each value as option}<span>{option}</span>{/each}
{:else}
<p>{String(value)}</p>
{/if}
</section>
{/if}
{/each} Micro Copy
Micro copy entries are short, language-scoped strings managed in the CMS. Load the full dictionary for a language in the [lang] layout so every child route can use it without extra fetches.
// src/routes/[lang]/+layout.server.ts
import { adapto } from '$lib/adapto-sdk';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ params }) => {
const { lang } = params;
const [languages, copy] = await Promise.all([
adapto.languages.list(),
adapto.microCopy.getDictionary(lang),
]);
return { lang, languages, copy };
}; <!-- Usage in any child +page.svelte -->
<script lang="ts">
let { data } = $props();
</script>
<button>{data.copy['btn.submit']}</button> Multi-language
Every content type has a language field storing the full locale code (e.g. "en-US"). The [lang] URL segment uses the same value. The root route redirects to the first available language.
// src/routes/+page.server.ts
import { redirect } from '@sveltejs/kit';
import { adapto } from '$lib/adapto-sdk';
export const load = async () => {
const languages = await adapto.languages.list();
redirect(302, `/${languages[0] ?? 'en-US'}`);
};