A high-performance, SEO-optimized SvelteKit boilerplate for the Enodo Butterfly CMS. Built with simplicity and best practices in mind.
At Enodo, we believe in simplification ("enodo" = to untie, to simplify). This boilerplate delivers a simple yet powerful foundation for creating SEO-optimized blogs. All optimizations are included out of the box while maintaining clean, maintainable code.
The design is intentionally neutral to give you full creative freedom, while the semantic structure is ultra-optimized for accessibility and search engines.
Image and Picture enable proper lazy loading of images from the Butterfly API, with easy support for responsive images with multiple sizes. Image uses srcset and sizes attributes, while Picture uses <source> elements for more advanced responsive scenariosview-transition APIschema-dts/search route with keyboard navigation supportapp.version, app.platform, app.env, page.index, page.query, content.type, content.id, content.group, content.flags, content.tagsdata.scope: Contains a Butterfly.ApiResponse object for the current page context (category, post, author, tag, etc.)data.feeds: Contains promises or records of Butterfly.ApiResponse objects. The <Feed> component automatically handles lazy loading on the client side via Svelte streams, or direct SSR rendering when appropriate, making content loading seamless and performantdata.meta: Contains SEO metadata (url, title, description, robots) used for meta tags, Open Graph, and canonical URLsdata.layer: Contains analytics and tracking data (page.index, page.query, content.type, content.id, content.group, content.tags, content.flags) automatically pushed to Google Tag Manager data layerlib/api.ts for easy configurationFork this repository from enodo-io/sveltekit-butterfly-boilerplate
Install dependencies:
npm install
npm run setup
This interactive wizard will guide you through:
npm run dev
Your site will be available at http://localhost:5173
For deployment, see the official SvelteKit adapter documentation. This boilerplate currently uses @sveltejs/adapter-node but can be switched to any adapter (Vercel, Netlify, Cloudflare, etc.).
To build for production:
npm run build
npm run preview
The setup script creates a .env file with the following variables:
| Variable | Required | Description |
|---|---|---|
PUBLIC_BASE_URL |
Yes | Your website's public URL (e.g., https://mywebsite.com) |
PUBLIC_API_URL |
Yes | Butterfly API domain (from your Butterfly settings) |
PUBLIC_API_KEY |
Yes | Butterfly API key (from your Butterfly settings) |
PUBLIC_MEDIA_URL |
Yes | Butterfly media domain (for static assets) |
PUBLIC_STATIC_PAGES |
No | JSON object mapping page slugs to IDs (e.g., {"about":1,"legal":2}) |
PUBLIC_INDEXABLE |
Yes | Set to false for staging/dev to prevent indexing. Defaults to true if not set |
PUBLIC_LOCALE |
Yes | Locale code for your site (e.g., fr_FR, en_US, es_ES) |
PUBLIC_LANGUAGE |
Yes | Language code (e.g., fr, en, es) - auto-extracted from locale by default |
PUBLIC_GTM_ID |
No | Google Tag Manager container ID (e.g., GTM-XXXXXXX) |
This boilerplate expects the following structure in your Butterfly CMS:
tags must existpage for static pages (Legal, About, etc.)featured must existCustomize static page URLs via the PUBLIC_STATIC_PAGES environment variable.
Adapt your site's color palette in src/assets/styles/main.scss:
@use '@enodo/foundation-css' with (
$colors: (
light: (
'main': #ffffff,
'025': #fafafa,
// ... more shades
'900': #0a0a0a,
),
primary: (
'main': #3b82f6,
// Your primary color
),
secondary: (
'main': #f59e0b,
// Your secondary color
),
)
);
You're not limited to primary and secondary. You can define colors by name (blue, green, red) or by usage (brand, success, error, warning).
Change fonts in the same file:
@use '@fontsource/poppins';
@use '@fontsource-variable/open-sans';
@use '@fontsource/fira-mono';
@use '@enodo/foundation-css' with (
$font-families: (
brand: (
'Poppins',
sans-serif,
),
// Brand heading font
sans: (
'Open Sans',
sans-serif,
),
// Body font
mono: (
'Fira Code',
monospace,
),
// Code font
)
);
Install fonts via:
npm install -D @fontsource/{font-name}
Language and locale are configured via environment variables:
PUBLIC_LOCALE (e.g., fr_FR, en_US, es_ES, de_DE)PUBLIC_LANGUAGE (e.g., fr, en, es) - auto-extracted from locale by defaultThese variables are used throughout the application for:
lang attribute in src/app.htmlog:locale meta tagsImportant: After setting the environment variables, you must also:
src/lib/httpErrors.tsExample translations needed:
src/lib/httpErrors.tsReplace default assets:
favicon-*.png files and favicon.ico in the static/ folderlogo-*.jpg and logo-*.png files in the static/ foldersrc/assets/images/ (recommended size: 1366x768px)Update static/robots.txt with your actual site URL:
Sitemap: https://www.yoursite.com/sitemaps/index.xml
Sitemap: https://www.yoursite.com/sitemaps/news.xml
/File: src/routes/+page.svelte
featured to display a hero article/[path]File: src/routes/[...path]/+page.svelte
Lists all posts in the corresponding category with pagination.
/articlesFile: src/routes/articles/+page.svelte
Displays all published posts across all categories.
/searchFile: src/routes/search/+page.svelte
Full-text search functionality with keyboard navigation.
/authorsFile: src/routes/authors/+page.svelte
Lists all site authors with their profile information.
/authors/[slug]File: src/routes/authors/[slug]/+page.svelte
Displays author profile and their latest posts.
/tagsFile: src/routes/tags/+page.svelte
Lists all tags from the tags taxonomy.
/tags/[slug]File: src/routes/tags/[slug]/+page.svelte
Displays all posts tagged with the specified tag.
/rss.htmlFile: src/routes/rss.html/+page.svelte
Syndication directory listing all available RSS feeds.
/[slug]-[id].htmlFile: src/routes/[slug=post]/+page.svelte
/[env-defined-slug].htmlFile: src/routes/[slug=page].html/+page.svelte
Example: /about.html, /legal.html (configured via PUBLIC_STATIC_PAGES)
Important: Remember to set proper canonical URLs in Butterfly for these pages.
All feeds are available at /[format=feed]/ routes:
/rss/index.xmlLatest posts from all categories
/rss/sections/[path].xmlPosts from a specific category
/rss/authors/[slug].xmlPosts by a specific author
/rss/tags/[id].xmlPosts tagged with a specific tag
All sitemaps are generated automatically and available at /sitemaps/:
/sitemaps/index.xml - Master sitemap/sitemaps/news.xml - Posts published in the last 48 hours/sitemaps/sections.xml - All category pages/sitemaps/tags.xml - All tag pages/sitemaps/authors.xml - All author pages/sitemaps/pages.xml - Static pages (post type page and hardcoded pages)/sitemaps/posts.xml - All article pagesThe $lib directory contains reusable utility functions and helpers for the application.
File: lib/api.ts
Pre-configured Butterfly API client instance using environment variables. Used throughout the application to interact with the Butterfly CMS API.
Usage:
import api from '$lib/api';
// Using path
const posts = await api.get({ path: '/v1/posts' });
const next = await api.get({ path: posts.links.next });
// Using endpoint and ID
const post = await api.get({ endpoint: 'posts', id: 1 });
// With query parameters
const search = await api.get({
endpoint: 'posts',
query: { filter: { query: 'search query' } },
});
// With custom fetch options
const data = await client.get({
endpoint: 'posts',
signal: abortController.signal,
intercept: (response) => console.log(response),
});
Client methods:
client.get<T>(options) - Fetch data from Butterfly APIpath - Full API path (e.g., /v1/posts)endpoint - Endpoint name (e.g., posts, authors)id - Resource IDquery - Query parameters objectsignal - AbortSignal for cancellationintercept - Function to intercept responsefetch - Custom fetch functionFile: lib/getMediaUrl.ts
Generates media URLs from Butterfly media objects. Automatically injects the media domain from environment variables.
Usage:
import { getMediaUrl } from '$lib/getMediaUrl';
// Basic usage with format and width
const url = getMediaUrl({
media: butterflyMedia,
format: 'thumb',
width: 800,
});
// With custom extension
const jpgUrl = getMediaUrl({
media: imageMedia,
format: 'cover',
width: 1200,
ext: 'jpg',
});
// Video with definition (required for .mp4)
const videoUrl = getMediaUrl({
media: videoMedia,
format: 'source',
ext: 'mp4',
definition: 'hd',
});
// Custom slug
const customUrl = getMediaUrl({
media: butterflyMedia,
format: 'square',
width: 400,
slug: 'avatar',
});
Parameters:
media - Butterfly media object (required)format - Image/video format: 'default', 'source', 'thumb', 'square', 'cover', 'stories' (default: 'default')width - Image width in pixels (for images)ext - File extension (e.g., 'jpg', 'png', 'webp', png, 'mp4', 'mp3')slug - URL slug (default: 'media')definition - Video definition (required for .mp4 or .mp3 formats: 'sd', 'hd'.)File: lib/httpErrors.ts
User-friendly error messages for common HTTP status codes (400, 401, 403, 404, 410, 422, 429, 500, 503). Used in error pages throughout the application.
import httpErrors from '$lib/httpErrors';
const message = httpErrors[404]; // "Oops! The page you're looking for wandered off 🕵️♂️"
File: lib/breakpoints.js
Responsive breakpoint values for media queries. Auto-generated during build.
import { breakpoints } from '$lib/breakpoints';
// { sm: 600, md: 1008, lg: 1280 }
Located in lib/JsonLD/, these utilities generate structured data (Schema.org) for SEO:
Article.ts: Generates Article/NewsArticle schemaBreadcrumbList.ts: Generates breadcrumb navigation schemaOrganization.ts: Generates publisher organization schemaProfilePage.ts: Generates author profile schemaWebPage.ts: Generates standard web page schemaWebSite.ts: Generates website schema with search actionFAQPage.ts: Generates standard FAQ page schemaMain entry point:
import { generateJsonLd } from '$lib/JsonLD';
const schemas = generateJsonLd(pageData, ['WebPage', 'Article', 'BreadcrumbList']);
All these utilities are available throughout the app via the $lib alias.
Layout/Header.svelteMain site header with navigation menu.
Layout/Footer.svelteSite footer with links and metadata.
Layout/Categories.svelteDisplays the category hierarchy.
Modal.svelteAccessible modal with clickable backdrop (closes on direct backdrop click) and Escape key support.
Props:
open: booleanonClose?: () => voidcontent: SnippetCard.sveltePost card component for displaying articles.
Props:
post: The Butterfly post data (required)included: Array of included resources from the API response (required)width: Image width (default: 380)heading: Heading level 'h1' to 'h4' (default: 'h3')format: Image format 'default', 'source', 'thumb', 'square', 'cover', 'stories' (default: 'thumb')lazyload: Enable lazy loading (default: true)thumbnail: Show thumbnail image (default: true)resume: Show post resume/description (default: true)author: Show author info (default: true)date: Show date info (default: true)widths: Array of image widths for responsive srcset (default: [320, 480, 540])sizes: Sizes attribute for responsive imagesUsage:
<Card post={article} included={response.included} heading="h2" format="cover" />
Customization:
:global(.card) in route styles or edit src/assets/styles/_cards.scssPost/Body.svelteRenders a Butterfly post body (headings, paragraphs, lists, media, embeds, etc.).
:global(.post--[element]) in src/assets/styles/main.scss (e.g., .post--list, .post--quote, .post--youtube).Feed.svelteWrapper component that displays post cards with loading skeleton states.
Props:
feed: Promise or Butterfly API response containing posts (required)length: Number of skeleton cards to show while loading (default: 6)width: Image width for cards (default: 380)format: Image format (default: 'thumb')heading: Heading level (default: 'h3')lazyloadAfter: Index after which to lazy load images (default: 0)thumbnail: Show thumbnail (default: true)resume: Show resume (default: true)author: Show author (default: true)date: Show date (default: true)widths: Image widths arraysizes: Sizes attributeUsage:
<Feed feed={posts} length={9} lazyloadAfter={3} />
Breadcrumb.svelteDisplays a semantic breadcrumb trail based on page.data JSON-LD.
Props: None (reads from page.data)
Usage:
<Breadcrumb />
Automatically generates from BreadcrumbList schema in page data.
Pagination.svelteHandles pagination UI with optional infinite scroll support.
Props:
current: Current page number (required)max: Maximum number of pages (required)url: Function that generates URL for a page number (default: (page) => \/?page=${page}``)pad: Number of pages to show before/after current (default: 2)label: Button label for "load more" (default: 'Load more')controls: ARIA controls IDonload: Async function to load next page (returns Promise<boolean>)next: Whether there is a next page availableinfiniteScroll: Enable infinite scroll modeUsage:
<Pagination
current={page}
max={totalPages}
url={(p) => `/articles?page=${p}`}
onload={loadNextPage}
next={hasMore}
/>
Image.svelteEnhanced <img> component with lazy loading and responsive images.
Props:
media: Butterfly media objectlazyload: Enable lazy loading (default: true)format: Image format 'default', 'source', 'thumb', 'square', 'cover', 'stories' (default: 'default')width: Base image width (default: 380)widths: Array of widths for srcset (default: [320, 480, 768, 990])sizes: Sizes attribute (default: '100vw')alt: Alt text<img> attributesUsage:
<Image
media={article.media}
format="thumb"
width={540}
widths={[400, 800, 1200]}
sizes="(max-width: 768px) 100vw, 800px"
alt={article.title}
/>
Automatically generates 2x versions and falls back to the default thumbnail image from src/assets/images/thumb.jpg if no media.
Picture.svelteAdvanced picture component with multiple sources for different breakpoints.
Props:
media: Butterfly media objectlazyload: Enable lazy loading (default: true)format: Image format (default: 'default')width: Base image width (default: 380)srcset: Array of { query: string, width: number } objects for responsive sourcesalt: Alt textUsage:
<Picture
media={article.media}
srcset={[
{ query: '(max-width: 768px)', width: 400 },
{ query: '(min-width: 769px)', width: 800 },
]}
alt="Description"
/>
Post/Elements/Video.svelteMinimal <video> wrapper with HD/SD sources and poster, no lazyload or custom player. Automatically picks HD/SD depending on network conditions (Network Information API when available).
The boilerplate includes typed JSON-LD schemas using schema-dts:
src/lib/JsonLD/src/lib/JsonLD/index.ts+page.svelteGTM is integrated via the GTM.svelte component and automatically loads when PUBLIC_GTM_ID is configured.
The following variables are automatically pushed to the data layer:
Page Data:
page.index: Current page index (for pagination)page.query: Search queryContent Data:
content.type: Content type (home, post, category, author, etc.)content.id: Content IDcontent.group: Content group/categorycontent.flags: Content flags arraycontent.tags: Content tags arrayApp Data:
app.version: Package version + git commitapp.platform: Node.js platformapp.env: Environment nameYou can push custom events to the data layer:
window.dataLayer.push({
event: 'custom_event',
// your custom properties
});
# Install the font
npm install -D @fontsource/inter
# Update main.scss
@use '@fontsource/inter';
@use '@enodo/foundation-css' with (
$font-families: (
sans: ('Inter', sans-serif)
)
);
Edit src/assets/styles/main.scss:
$colors: (
light: (
/* neutral palette */
),
primary: (
'main': #3b82f6,
),
// Your brand color
accent: (
'main': #f59e0b,
),
// Accent color
success: (
'main': #10b981,
),
// Success states
error: (
'main': #ef4444,
), // Error states
);
# Setup environment
npm run setup
# Development
npm run dev # Start dev server
npm run check # Type check
npm run check:watch # Watch type checking
# Build
npm run build # Production build
npm run preview # Preview production build
# Testing
npm run test # Run all tests
npm run test:unit # Unit tests
npm run test:e2e # E2E tests with Playwright
# Code Quality
npm run lint # Lint code
npm run format # Format code
This is the official Enodo boilerplate. For issues or questions:
MIT © Enodo