Introducing�Sveltia CMS
Kohei Yoshino
March 31, 2023
Kohei Yoshino
UX engineer based in �Toronto, Canada�
Svelte user for a year
What is a headless CMS?
Source: https://jamstack.org/headless-cms/
API-driven
Git-based
Why I’ve built Sveltia CMS
Source: https://github.com/decaporg/decap-cms/graphs/contributors
is now
Why Svelte, not SvelteKit
Bundle it!
One single JavaScript file,�no HTML, no CSS
// vite.config.js
import { svelte } from '@sveltejs/vite-plugin-svelte';
import sveltePreprocess from 'svelte-preprocess';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
chunkSizeWarningLimit: 1000,
rollupOptions: {
input: 'src/main.js', // Don’t output HTML
output: {
entryFileNames: 'sveltia-cms.js', // Bundle file name
},
},
},
plugins: [
svelte({
preprocess: sveltePreprocess(), // Compile Sass
emitCss: false, // Include CSS in the bundle
}),
],
});
Bundle it!
In the Svelte way
// main.js
import App from './app.svelte';
const app = new App({ target: document.body });
export default app;
<!-- app.svelte -->
<script>
import { AppShell } from '@sveltia/ui';
import { isLoading } from 'svelte-i18n';
import { user } from '$lib/services/auth';
import { entriesLoaded } from '$lib/services/contents';
import EntrancePage from './lib/entrance/entrance-page.svelte';
import MainRouter from './lib/global/main-router.svelte';
</script>
<AppShell>
{#if !$isLoading}
{#if $user && $entriesLoaded}
<MainRouter />
{:else}
<EntrancePage />
{/if}
{/if}
</AppShell>
Internals
Hash-based routing
Because no SvelteKit
ProTip: The same technique can be used to �build a browser extension with Svelte
Hash-based routing
<svelte:component>
<!-- main-router.svelte —->
<script>
import AssetsPage from '../assets/assets-page.svelte';
import ConfigPage from '../config/config-page.svelte';
import ContentsPage from '../contents/contents-page.svelte';
import GlobalToolbar � from '../global/global-toolbar/global-toolbar.svelte';
import SearchPage from '../search/search-page.svelte';
import WorkflowPage from '../workflow/workflow-page.svelte';
import { selectedPageName, selectPage } � from '$lib/services/navigation';
const pages = {
collections: ContentsPage,
assets: AssetsPage,
search: SearchPage,
workflow: WorkflowPage,
config: ConfigPage,
};
</script>
<svelte:window on:hashchange={() => selectPage()} />
<svelte:component this={pages[$selectedPageName]} />
Hash-based routing
Layout component �with named slots
(and <svelte:component>)
<!-- contents-page.svelte —->
<PageContainer class="content">
<PrimarySidebar slot="primary_sidebar" />
<PrimaryToolbar slot="primary_toolbar" />
<SecondaryToolbar slot="secondary_toolbar" />
<svelte:component slot="main" � this={$selectedCollection.files ? FileList : EntryList} />
<SecondarySidebar slot="secondary_sidebar" />
</PageContainer>
<!-- page-container.svelte —->
<Group class="browser" {...$$restProps}>
<slot name="primary_sidebar" />
<Group class="main">
<slot name="primary_toolbar" />
<div class="main-inner">
<div class="main-inner-main">
<slot name="secondary_toolbar" />
<slot name="main" />
</div>
<slot name="secondary_sidebar" />
</div>
</Group>
</Group>
Entry listing
Simple list or grid
Entry listing
<svelte:component>
<!-- entry-list.svelte —->
<script>
import { Group } from '@sveltia/ui';
import { defaultContentLocale } from '$lib/services/config';
import { currentView, entryGroups } � from '$lib/services/contents/view';
import BasicGridView from '../../common/basic-grid-view.svelte';
import BasicListView from '../../common/basic-list-view.svelte';
import EntryListItem from './entry-list-item.svelte';
</script>
{#each Object.entries($entryGroups) � as [groupName, entries] (groupName)}
<Group>
<h3>{groupName}</h3>
<svelte:component this={$currentView.type === 'grid' � ? BasicGridView : BasicListView}>
{#each entries as entry (entry.slug)}
{@const { content } � = entry.locales[$defaultContentLocale] || {}}
<EntryListItem {entry} {content} />
{/each}
</svelte:component>
</Group>
{/each}
Field editor & preview
15+ types of widgets
Field editor & preview
<svelte:component>
<!-- field-preview.svelte —->
<script>
import { entryDraft } from '$lib/services/contents/editor';
import DateTimePreview from './widgets/date-time-preview.svelte';
import FilePreview from './widgets/file-preview.svelte';
import StringPreview from './widgets/string-preview.svelte';
export let locale = '';
export let keyPath = '';
export let fieldConfig = {};
const widgets = {
datetime: DateTimePreview,
image: FilePreview,
string: StringPreview,
};
$: ({ label = '', widget = 'string' } = fieldConfig);
$: currentValue = $entryDraft.currentValues[locale][keyPath];
</script>
<section>
<h4>{label}</h4>
<svelte:component this={widgets[widget]} � {keyPath} {locale} {fieldConfig} {currentValue} />
</section>
Other highlights
What’s under the hood?
Use the CMS with SvelteKit
Find the data
Just static JSON, YAML, TOML or Front Matter files
Load the data
import.meta.glob() �is your friend!
// projects.js
export const allProjects = {};
// @see https://vitejs.dev/guide/features.html#glob-import
const files = import.meta.glob(� '../../data/projects/*.json',� { as: 'raw', eager: true }
);
Object.entries(files).forEach(([path, rawFile]) => {
const [, slug] = path.match(/.+?\/(\w+?)\.\w+$/);
const content = JSON.parse(rawFile);
Object.entries(content).forEach(([locale, item]) => {
allProjects[locale] ||= [];
allProjects[locale][slug] = item;
});
});
Use the data
Render content as you wish
<!-- +page.svelte —->
<script>
import { marked } from 'marked';
import { page } from '$app/stores';
import { allProjects } from '$lib/services/marketing/projects';
$: ({ locale = 'en' } = $page.params);
</script>
{#each Object.entries(allProjects[locale])� as [slug, { image, name, description }] (slug)}
<section>
<header>
<img src={image} alt="" />
<h3>{name}</h3>
</header>
<!-- Sanitize HTML if the content can’t be trusted! —->
{@html marked.parse(description)}
</section>
{/each}
Thank you!
https://github.com/sveltia/sveltia-cms�
@kyoshino on GitHub & Discord�Available for hire!