Here are practical examples of how to use the CMS in your Svelte components with the use:cms action.
The simplest pattern uses the use:cms action directive:
<script>
import { cmsStore, cms } from '$lib/cms';
</script>
<h1 data-cms-ref="page.title" data-cms-type="text" use:cms>
{$cmsStore['page.title']?.content || 'Default Page Title'}
</h1>How it works:
data-cms-ref- Unique identifier for this contentdata-cms-type- Content type (text, html, image)use:cms- Svelte action that makes the element editable{$cmsStore['page.title']?.content || 'Fallback'}- Displays saved content or fallback
For content with HTML formatting:
<script>
import { cmsStore, cms } from '$lib/cms';
</script>
<article data-cms-ref="blog.post.body" data-cms-type="html" use:cms>
{@html $cmsStore['blog.post.body']?.content || '<p>Default content...</p>'}
</article>Note: In production, always sanitize HTML! Consider using a library like DOMPurify:
<script>
import { cmsStore, cms } from '$lib/cms';
import DOMPurify from 'isomorphic-dompurify';
$: cleanHTML = DOMPurify.sanitize($cmsStore['blog.post.body']?.content || '<p>Default</p>');
</script>
<article data-cms-ref="blog.post.body" data-cms-type="html" use:cms>
{@html cleanHTML}
</article>Use the same ref in multiple places - edit once, updates everywhere!
<script>
import { cmsStore, cms } from '$lib/cms';
</script>
<header>
<h1 data-cms-ref="global.site.name" data-cms-type="text" use:cms>
{$cmsStore['global.site.name']?.content || 'My Site'}
</h1>
<span data-cms-ref="global.site.tagline" data-cms-type="text" use:cms>
{$cmsStore['global.site.tagline']?.content || 'Your tagline here'}
</span>
</header><script>
import { cmsStore, cms } from '$lib/cms';
</script>
<footer>
<!-- Same tagline ref - automatically stays in sync -->
<p data-cms-ref="global.site.tagline" data-cms-type="text" use:cms>
{$cmsStore['global.site.tagline']?.content || 'Your tagline here'}
</p>
<p data-cms-ref="global.footer.copyright" data-cms-type="text" use:cms>
{$cmsStore['global.footer.copyright']?.content || '© 2025 Your Company'}
</p>
</footer>The global.site.tagline appears in both components but is managed as a single piece of content!
Content can be used in conditional blocks:
<script>
import { cmsStore, cms } from '$lib/cms';
let showBanner = $state(true);
</script>
{#if showBanner}
<div class="banner" data-cms-ref="home.promo.banner" data-cms-type="text" use:cms>
{$cmsStore['home.promo.banner']?.content || 'Special offer!'}
</div>
{/if}Each list item should have a unique ref:
<script>
import { cmsStore, cms } from '$lib/cms';
const features = ['feature1', 'feature2', 'feature3'];
</script>
<ul>
{#each features as feature}
<li data-cms-ref="home.features.{feature}" data-cms-type="text" use:cms>
{$cmsStore[`home.features.${feature}`]?.content || 'Feature description'}
</li>
{/each}
</ul>import { cmsStore } from '$lib/cms';
const features = [
{ id: 1, ref: 'features.speed' },
{ id: 2, ref: 'features.security' },
{ id: 3, ref: 'features.scalability' }
];
-
{#each features as feature}
- {$cmsStore[feature.ref]?.content || 'Feature description'} {/each}
Images can be uploaded and managed directly through the CMS interface. The image URL is stored in the database.
<script>
import { cmsStore, cms } from '$lib/cms';
import defaultHeroImage from '$lib/assets/default-hero.jpg';
</script>
<!-- Simple image -->
<img
data-cms-ref="home.hero.image"
data-cms-type="image"
src={$cmsStore['home.hero.image']?.content || defaultHeroImage}
alt="Hero"
use:cms
/>For better control and styling, wrap the image in a div:
<script>
import { cmsStore, cms } from '$lib/cms';
import defaultMissionImage from '$lib/assets/default_image.jpg';
</script>
<div data-cms-ref="about.mission.image" data-cms-type="image" class="image-container" use:cms>
<img src={$cmsStore['about.mission.image']?.content || defaultMissionImage} alt="Our Mission" />
</div>
<style>
.image-container {
max-width: 600px;
margin: 2rem auto;
}
.image-container img {
width: 100%;
height: auto;
border-radius: 8px;
}
</style>You can also use the CMSContent component wrapper:
<script>
import { CMSContent } from '$lib/cms';
import defaultImage from '$lib/assets/default.jpg';
</script>
<CMSContent ref="portfolio.project.thumbnail" type="image">
<img src={$cmsStore['portfolio.project.thumbnail']?.content || defaultImage} alt="Project" />
</CMSContent>How image upload works:
- In edit mode, click on an image element
- A file upload dialog appears in the overlay
- Select an image (max 5MB, formats: JPG, PNG, GIF, WebP)
- Image is uploaded to Supabase Storage
- The public URL is saved to the database
- All instances of that ref are updated automatically
Image Object-Fit Control:
When editing an image, you can control how it fits within its container:
- Fill: Image stretches to fill the container (may distort)
- Contain: Image scales to fit entirely within container (default, maintains aspect ratio)
- Cover: Image covers entire container (may crop, maintains aspect ratio)
- None: Image displays at natural size
The setting is saved as metadata and persists across sessions. In edit mode, images always use "contain" to ensure the edit overlay is properly positioned.
Setup Required: See IMAGE-STORAGE-SETUP.md for configuring Supabase Storage.
<script>
import { cmsStore, cms } from '$lib/cms';
</script>
<picture>
<source
media="(min-width: 768px)"
srcset={$cmsStore['home.hero.image']?.content || '/default-hero.jpg'}
/>
<img
data-cms-ref="home.hero.image"
data-cms-type="image"
src={$cmsStore['home.hero.mobile']?.content || '/default-hero-mobile.jpg'}
alt="Hero"
use:cms
/>
</picture>Use Svelte's reactive syntax with CMS content:
<script>
import { cmsStore, cms } from '$lib/cms';
// Reactive derived value
$: welcomeMessage = $cmsStore['home.welcome']?.content || 'Welcome!';
$: isLongMessage = welcomeMessage.length > 50;
</script>
<div
class="welcome"
class:long={isLongMessage}
data-cms-ref="home.welcome"
data-cms-type="text"
use:cms
>
{welcomeMessage}
</div>Handle multiple fallback levels:
<script>
import { cmsStore, cms } from '$lib/cms';
const DEFAULT_TITLE = 'Welcome to Our Site';
$: pageTitle = $cmsStore['page.title']?.content || DEFAULT_TITLE;
</script>
<h1 data-cms-ref="page.title" data-cms-type="text" use:cms>
{pageTitle}
</h1>You can update content programmatically (useful for bulk operations or migrations):
<script>
import { saveContent } from '$lib/cms';
async function updateContent() {
const success = await saveContent('test.title', 'New content');
if (success) {
console.log('Content updated!');
}
}
</script>
<button onclick={updateContent}> Update Content Programmatically </button>Check if edit mode is active to show/hide elements:
<script>
import { cmsStore, cms, editMode } from '$lib/cms';
</script>
<div class="content" data-cms-ref="page.content" data-cms-type="text" use:cms>
{$cmsStore['page.content']?.content || 'Default content'}
</div>
{#if $editMode}
<div class="editor-hint">🖊️ Click any text to edit</div>
{/if}Show content based on whether the user is an editor:
<script>
import { cmsStore, cms, isEditor } from '$lib/cms';
</script>
<h1 data-cms-ref="page.title" data-cms-type="text" use:cms>
{$cmsStore['page.title']?.content || 'Page Title'}
</h1>
{#if $isEditor}
<div class="cms-info">You're logged in as an editor. Toggle Edit Mode to make changes.</div>
{/if}Here's a full page using the CMS:
<script>
import { cmsStore, cms } from '$lib/cms';
</script>
<div class="page">
<header>
<h1 data-cms-ref="about.header.title" data-cms-type="text" use:cms>
{$cmsStore['about.header.title']?.content || 'About Us'}
</h1>
<p data-cms-ref="about.header.tagline" data-cms-type="text" use:cms>
{$cmsStore['about.header.tagline']?.content || 'Learn more about our company'}
</p>
</header>
<main>
<section class="intro">
<article data-cms-ref="about.intro" data-cms-type="html" use:cms>
{@html $cmsStore['about.intro']?.content || '<p>Company introduction...</p>'}
</article>
</section>
<section class="values">
<h2 data-cms-ref="about.values.title" data-cms-type="text" use:cms>
{$cmsStore['about.values.title']?.content || 'Our Values'}
</h2>
<div class="values-grid">
<div class="value">
<h3 data-cms-ref="about.values.trust.title" data-cms-type="text" use:cms>
{$cmsStore['about.values.trust.title']?.content || 'Trust'}
</h3>
<p data-cms-ref="about.values.trust.desc" data-cms-type="text" use:cms>
{$cmsStore['about.values.trust.desc']?.content || 'We build trust...'}
</p>
</div>
<div class="value">
<h3 data-cms-ref="about.values.innovation.title" data-cms-type="text" use:cms>
{$cmsStore['about.values.innovation.title']?.content || 'Innovation'}
</h3>
<p data-cms-ref="about.values.innovation.desc" data-cms-type="text" use:cms>
{$cmsStore['about.values.innovation.desc']?.content || 'We innovate...'}
</p>
</div>
</div>
</section>
</main>
</div>
<style>
.page {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.values-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
</style><!-- Good -->
<h1 data-cms-ref="about.team.title" data-cms-type="text" use:cms>...</h1>
<!-- Avoid -->
<h1 data-cms-ref="h1_1" data-cms-type="text" use:cms>...</h1><!-- Good -->
<p data-cms-ref="content.description" data-cms-type="text" use:cms>
{$cmsStore['content.description']?.content || 'Meaningful default text'}
</p>
<!-- Avoid -->
<p data-cms-ref="content.description" data-cms-type="text" use:cms>
{$cmsStore['content.description']?.content}
</p>Organize refs by page/section:
global.site.name
global.footer.copyright
home.hero.title
about.team.description
pricing.cta.button
After adding new data-cms-ref attributes:
npm run cms:syncThis creates the database entries for your new refs.
Content not appearing:
- Check that
cms:synchas been run - Verify the ref exists in your Supabase
cms_contenttable - Check browser console for errors
Can't edit content:
- Ensure you're logged in as a user with editor role
- Toggle Edit Mode using the button
- Check that the element has both
data-cms-refanduse:cms
Edits not saving:
- Verify RLS policies are set up (run
sql/supabase-rls-policies.sql) - Check browser console for 403 errors
- Ensure editor role is properly set in user metadata