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
npx create-adapto-app --template sveltekit The installer will prompt you for the remaining details — including your Adapto API key — then scaffold the project, install dependencies, and set up your .env file. When it finishes, cd into your new project and 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'}`);
};