1 of 28

Introducing�Sveltia CMS

Kohei Yoshino

March 31, 2023

2 of 28

Kohei Yoshino

UX engineer based in �Toronto, Canada�

Svelte user for a year

3 of 28

What is a headless CMS?

4 of 28

Source: https://jamstack.org/headless-cms/

API-driven

Git-based

5 of 28

Why I’ve built Sveltia CMS

6 of 28

Source: https://github.com/decaporg/decap-cms/graphs/contributors

is now

7 of 28

8 of 28

9 of 28

10 of 28

11 of 28

Why Svelte, not SvelteKit

12 of 28

13 of 28

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

}),

],

});

14 of 28

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>

15 of 28

Internals

16 of 28

Hash-based routing

Because no SvelteKit

ProTip: The same technique can be used to �build a browser extension with Svelte

17 of 28

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 GlobalToolbarfrom '../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]} />

18 of 28

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>

19 of 28

Entry listing

Simple list or grid

20 of 28

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}

21 of 28

Field editor & preview

15+ types of widgets

22 of 28

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>

23 of 28

Other highlights

What’s under the hood?

  • Custom goto method�for page navigation
  • svelte-i18n �for UI localization
  • @sveltia/ui (early alpha) �as UI library
  • File System Access API �for local file editing
  • 3rd party integrations:�DeepL, Pexels, Unsplash

24 of 28

Use the CMS with SvelteKit

25 of 28

Find the data

Just static JSON, YAML, TOML or Front Matter files

26 of 28

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;

});

});

27 of 28

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}

28 of 28

Thank you!

https://github.com/sveltia/sveltia-cms

@kyoshino on GitHub & Discord�Available for hire!