Code Structure & Organization
Comprehensive overview of SveltyCMS codebase organization, architecture, and design patterns including multilingual content management
On this page
This document provides a comprehensive overview of the SveltyCMS codebase organization, architecture, and design patterns.
π οΈ Technical Standards
- Modern Stack: Latest TypeScript (^5.9.3), Node.js (>=24), Svelte 5 (^5.46.4), Vite 7 (^7.3.1), Bun (3-4x faster runtime).
- Code Quality: Verify with
bun run lint && bun run checkbefore commits. Use theoxlintandoxfmtsetup (provided via latestvite-plus) for sub-second formatting and lightning-fast linting.
| Category | Convention | Examples |
|---|---|---|
| Naming | camelCase | dbAdapter, loadSettings (logic, variables, props) |
| PascalCase | Auth, MediaService (types, interfaces, classes) |
|
| kebab-case | user-avatar.svelte, auth-service.ts (files, folders) |
|
| UPPER_SNAKE_CASE | DB_TYPE, DEFAULT_THEME (global constants) |
|
| File Headers | Mandatory format | /** @file [path] @description [desc] features: [list] */ |
π Directory Structure
SveltyCMS/
βββ config/ # Core configuration
β βββ collections/ # Collection schema definitions
β βββ public.ts # Public settings (version controlled)
β βββ private.ts # Private settings (gitignored)
β
βββ src/ # Source code
β βββ routes/ # SvelteKit routes (UI and API)
β β βββ (admin)/ # Admin panel routes
β β βββ api/ # API endpoints (95+ endpoints)
β β
β βββ databases/ # Database adapters
β β βββ auth/ # Authentication system
β β βββ mongodb/ # MongoDB adapter
β β βββ mariadb/ # MariaDB adapter
β β βββ postgresql/ # PostgreSQL adapter
β β
β βββ components/ # Reusable Svelte components
β β βββ atoms/ # Basic UI elements
β β βββ molecules/ # Component combinations
β β βββ organisms/ # Complex UI sections
β β
β βββ widgets/ # CMS widget implementations
β βββ stores/ # Svelte stores (state management)
β βββ utils/ # Utility functions
β β βββ compilation/ # Build & AST transformers
β β βββ ...
β βββ hooks/ # SvelteKit server hooks
β βββ content/ # Content management system
β
βββ docs/ # Documentation
β βββ api/ # API documentation
β βββ guides/ # User guides
β βββ architecture/ # Architecture docs
β βββ widgets/ # Widget documentation
β βββ contributing/ # Contribution guides
β
βββ tests/ # Test suites
β βββ e2e/ # E2E Black-Box tests (Playwright)
β βββ integration/ # Integration tests (Bun)
β βββ unit/ # Unit White-Box tests (Bun)
β
βββ static/ # Static assets
βββ build/ # Production build output
ποΈ Core Components
1. Database Layer (src/databases/)
The database layer provides a unified interface for different database backends through the adapter pattern.
Key Files:
db-interface.ts- Database adapter interface definition (IDBAdapter)db.ts- Database initialization and adapter selection- Database adapters implement the IDBAdapter interface for consistency
IDBAdapter Interface Structure:
interface IDBAdapter {
// Connection Management
connect(connectionString: string, options?: unknown): Promise<DatabaseResult<void>>;
disconnect(): Promise<DatabaseResult<void>>;
isConnected(): boolean;
// Authentication & User Management
auth: {
setupAuthModels(): Promise<void>;
createUser(userData: Partial<User>): Promise<DatabaseResult<User>>;
getUserByEmail(criteria: {
email: string;
tenantId?: string;
}): Promise<DatabaseResult<User | null>>;
createSession(sessionData: {
user_id: string;
expires: Date;
}): Promise<DatabaseResult<Session>>;
validateSession(session_id: string): Promise<DatabaseResult<User | null>>;
// ... 20+ more auth methods
};
// CRUD Operations
crud: {
findOne<T>(collection: string, query: Partial<T>): Promise<DatabaseResult<T | null>>;
findMany<T>(collection: string, query: Partial<T>): Promise<DatabaseResult<T[]>>;
insert<T>(
collection: string,
data: Omit<T, "_id" | "createdAt" | "updatedAt">,
): Promise<DatabaseResult<T>>;
update<T>(collection: string, id: DatabaseId, data: Partial<T>): Promise<DatabaseResult<T>>;
delete(collection: string, id: DatabaseId): Promise<DatabaseResult<void>>;
// ... batch operations, upsert, aggregate, etc.
};
// Content Management
content: {
nodes: {
getStructure(mode: "flat" | "nested"): Promise<DatabaseResult<ContentNode[]>>;
create(
node: Omit<ContentNode, "createdAt" | "updatedAt">,
): Promise<DatabaseResult<ContentNode>>;
update(path: string, changes: Partial<ContentNode>): Promise<DatabaseResult<ContentNode>>;
// ... more node operations
};
drafts: {
/* draft operations */
};
revisions: {
/* revision operations */
};
};
// Media Management
media: {
files: {
upload(
file: Omit<MediaItem, "_id" | "createdAt" | "updatedAt">,
): Promise<DatabaseResult<MediaItem>>;
uploadMany(
files: Omit<MediaItem, "_id" | "createdAt" | "updatedAt">[],
): Promise<DatabaseResult<MediaItem[]>>;
getByFolder(
folderId?: DatabaseId,
options?: PaginationOptions,
): Promise<DatabaseResult<PaginatedResult<MediaItem>>>;
// ... more file operations
};
folders: {
/* folder operations */
};
};
// Theme & Widget Management
themes: {
getActive(): Promise<DatabaseResult<Theme>>;
setDefault(themeId: DatabaseId): Promise<DatabaseResult<void>>;
install(theme: Omit<Theme, "_id" | "createdAt" | "updatedAt">): Promise<DatabaseResult<Theme>>;
// ... more theme operations
};
widgets: {
register(
widget: Omit<Widget, "_id" | "createdAt" | "updatedAt">,
): Promise<DatabaseResult<Widget>>;
findAll(): Promise<DatabaseResult<Widget[]>>;
activate(widgetId: DatabaseId): Promise<DatabaseResult<void>>;
// ... more widget operations
};
// Collection Schema Management
collection: {
getModel(id: string): Promise<CollectionModel>;
createModel(schema: Schema): Promise<void>;
updateModel(schema: Schema): Promise<void>;
deleteModel(id: string): Promise<void>;
};
// System Preferences (system.preferences namespace)
system: {
preferences: {
get<T>(
key: string,
scope?: "user" | "system",
userId?: DatabaseId,
): Promise<DatabaseResult<T>>;
set<T>(
key: string,
value: T,
scope?: "user" | "system",
userId?: DatabaseId,
category?: string,
): Promise<DatabaseResult<void>>;
getMany<T>(
keys: string[],
scope?: "user" | "system",
userId?: DatabaseId,
): Promise<DatabaseResult<Record<string, T>>>;
setMany(
settings: Array<{
key: string;
value: unknown;
category: string;
scope: "user" | "system";
userId?: DatabaseId;
}>,
): Promise<DatabaseResult<void>>;
};
};
// Performance & Caching
performance: {
getMetrics(): Promise<DatabaseResult<PerformanceMetrics>>;
clearMetrics(): Promise<DatabaseResult<void>>;
};
cache: {
get<T>(key: string): Promise<DatabaseResult<T | null>>;
set<T>(key: string, value: T, options?: CacheOptions): Promise<DatabaseResult<void>>;
invalidateCollection(collection: string): Promise<DatabaseResult<void>>;
};
// Batch Operations
batch: {
execute<T>(operations: BatchOperation<T>[]): Promise<DatabaseResult<BatchResult<T>>>;
bulkInsert<T>(
collection: string,
items: Omit<T, "_id" | "createdAt" | "updatedAt">[],
): Promise<DatabaseResult<T[]>>;
bulkUpdate<T>(
collection: string,
updates: Array<{ id: DatabaseId; data: Partial<T> }>,
): Promise<DatabaseResult<{ modifiedCount: number }>>;
// ... more batch operations
};
// Utility Methods
utils: {
generateId(): DatabaseId;
normalizePath(path: string): string;
validateId(id: string): boolean;
};
}
// DatabaseResult type for consistent error handling
type DatabaseResult<T> =
| { success: true; data: T; meta?: QueryMeta }
| { success: false; error: DatabaseError; message: string };
Supported Adapters:
- β
MongoDB (
/src/databases/mongodb/) - Via Mongoose - β
MariaDB (
/src/databases/mariadb/) - Via mariadb driver - β
PostgreSQL (
/src/databases/postgresql/) - Via pg driver - β
SQLite (
/src/databases/sqlite/) - Via bun:sqlite
Why Adapter Pattern?
- Database-agnostic application code
- Easy to add new database support
- Consistent API across all databases
- Simplified testing and mocking
2. Authentication System (src/databases/auth/)
Handles user authentication, authorization, and session management.
Features:
- JWT token generation and validation
- Session management (cookie-based)
- OAuth integration (Google, GitHub)
- Role-based access control (RBAC)
- 2FA (Two-Factor Authentication)
- Password hashing (Argon2 or Bcrypt)
Key Components:
// Authentication flow
login() β validateCredentials() β generateJWT() β createSession()
// Authorization flow
checkPermission() β getUserRole() β validatePermission() β allow/deny
Security Measures:
- Secure password hashing
- JWT with expiration
- CSRF protection
- Secure session cookies (httpOnly, secure, sameSite)
- Rate limiting on auth endpoints
3. Component Library (src/components/)
Reusable UI components following Atomic Design principles. Standardized strictly to lowercase (kebab-case) for Linux/CI compatibility.
Structure:
components/
βββ atoms/ # Basic building blocks
β βββ button.svelte
β βββ input.svelte
β βββ icon.svelte
β
βββ molecules/ # Combinations of atoms
β βββ form-field.svelte
β βββ search-bar.svelte
β βββ card.svelte
β
βββ organisms/ # Complex UI sections
βββ header.svelte
βββ sidebar.svelte
βββ data-table.svelte
Svelte 5 Patterns:
<script lang="ts">
// Using Svelte 5 runes
let count = $state(0);
let doubled = $derived(count * 2);
function increment() {
count++;
}
</script>
<button onclick={increment}>
Count: {count} (doubled: {doubled})
</button>
4. Configuration System (config/ + src/stores/)
Manages CMS configuration through static files and reactive stores.
Configuration Files:
config/public.ts - Public, non-sensitive settings:
export const publicConfig = {
siteName: "My CMS",
siteUrl: "https://example.com",
defaultLanguage: "en",
mediaUploadLimit: 10485760, // 10MB
// ... More public settings
};
config/private.ts - Private, sensitive settings (gitignored):
export const privateConfig = {
jwtSecret: process.env.JWT_SECRET,
databaseUrl: process.env.DATABASE_URL,
oauthClientId: process.env.OAUTH_CLIENT_ID,
// ... API keys, secrets, etc.
};
Global Settings Store:
// src/stores/global-settings.svelte.ts
export class GlobalSettings {
// Settings loaded from database at startup
// Provides reactive, type-safe access throughout app
}
export const globalSettings = new GlobalSettings();
5. Content Management (src/content/)
The ContentSystem handles all content operations.
Responsibilities:
- Load collection schemas from
config/collections/ - Create database models dynamically
- Provide CRUD API for content
- Handle content relationships
- Validate content against schemas
Example:
import { ContentSystem } from "$lib/content";
// Load collections
const contentSystem = new ContentSystem(db);
await contentSystem.loadCollections();
// CRUD operations
const post = await contentSystem.create("posts", {
title: "Hello World",
content: "My first post",
author: userId,
});
6. Widget System (src/widgets/)
Extensible widget framework for custom fields and displays.
Widget Types:
- Form widgets - Input fields (text, number, date, etc.)
- Display widgets - Content rendering
- Media widgets - File uploads, image galleries
- Custom widgets - User-defined widgets
Widget Structure:
interface Widget {
id: string;
name: string;
type: "form" | "display" | "media";
component: SvelteComponent;
schema: WidgetSchema;
validate: (value: any) => ValidationResult;
}
Multilingual Widget Pattern:
Widgets automatically handle translated content by reading the current language from the contentLanguage store:
<!-- src/widgets/core/input/input.svelte -->
<script lang="ts">
import { app } from '@stores/store.svelte';
import { DEFAULT_CONTENT_LANGUAGE } from '@src/utils/constants';
interface Props {
field: FieldInstance;
value: Record<string, string> | string; // Translated or plain value
}
let { field, value = $bindable() }: Props = $props();
// Reactive language selection - uses app.contentLanguage for translated fields
const _language = $derived(field.translated ? app.contentLanguage : DEFAULT_CONTENT_LANGUAGE);
// Reactive value access for current language
const safeValue = $derived(
field.translated && typeof value === 'object' && value !== null ? (value[_language] ?? '') : typeof value === 'string' ? value : ''
);
function updateValue(newValue: string) {
if (field.translated) {
// Update language-specific key: { en: "English", de: "Deutsch" }
value = { ...(value as object), [_language]: newValue };
} else {
// Update plain string value
value = newValue;
}
}
</script>
<input type="text" value={safeValue} oninput={(e) => updateValue(e.currentTarget.value)} />
Database-Agnostic Multilingual Data:
The widget layer works with a normalized data structure, while the database adapter handles storage:
// Widget layer (database-agnostic)
const fieldData = {
firstName: { en: "John", de: "Johann" },
lastName: { en: "Smith", de: "Schmidt" },
};
// MongoDB stores directly as nested object
db.insertOne("users", fieldData);
// Future SQL/Drizzle adapter transforms to relational structure
// users table: id | createdAt | updatedAt
// translations table: id | entity_id | field | lang | value
// 1 | user_1 | firstName | en | John
// 2 | user_1 | firstName | de | Johann
// 3 | user_1 | lastName | en | Smith
// 4 | user_1 | lastName | de | Schmidt
// Adapter reconstructs: { firstName: { en: "John", de: "Johann" }, ... }
Widget Language Behavior:
| Field Setting | User Views EN | User Views DE | Data Structure |
|---|---|---|---|
translated: true |
Reads value.en |
Reads value.de |
{ en: "...", de: "..." } |
translated: false |
Reads value |
Reads value (same) |
"single value" |
See Widget System Architecture for details.
7. Core Utilities (src/utils/)
Provides globally available, dependency-free (or highly optimized) utility functions.
Key Utilities:
cn.ts: Tailwind class merging (viatailwind-merge&clsx) for building conflict-free UI components.pluralize.ts: Universal runtime pluralization utilizingIntl.PluralRulesto handle dynamic database content across complex languages (Arabic, Russian, etc.) where Paraglide JS compile-time variants cannot reach.slugify.ts: URL-safe identifier generation with robust Unicode and diacritic normalization.date.ts: Standardized ISO-8601 boundary operations without heavy moment.js/date-fns overhead.
π¨ Key Design Patterns
1. Adapter Pattern (Database)
Provides a unified interface for different database implementations.
// Usage in API endpoint
export async function GET({ locals }) {
const db = getDB(locals.dbType); // Gets correct adapter
const items = await db.find("posts", {});
return json(items);
}
Benefits:
- Database-agnostic code
- Easy to test (mock adapters)
- Simple to add new databases
2. Repository Pattern
Separates data access logic from business logic.
class ContentRepository {
constructor(private db: DatabaseAdapter) {}
async findById(id: string): Promise<Content | null> {
return this.db.findOne("content", { _id: id });
}
async findPublished(): Promise<Content[]> {
return this.db.find("content", {
status: "published",
publishDate: { $lte: new Date() },
});
}
}
3. Dependency Injection
Services receive dependencies through constructors.
class ContentService {
constructor(
private db: DatabaseAdapter,
private cache: CacheService,
private events: EventEmitter,
) {}
async getContent(id: string): Promise<Content> {
// Check cache first
const cached = await this.cache.get(`content:${id}`);
if (cached) return cached;
// Fetch from database
const content = await this.db.findOne("content", { _id: id });
// Cache result
await this.cache.set(`content:${id}`, content);
return content;
}
}
4. Event-Driven Architecture
Decouples components through events.
class ContentSystem {
async createContent(data: ContentData): Promise<Content> {
// Create content
const content = await this.repository.create(data);
// Emit event for other services to react
await this.events.emit("content:created", content);
return content;
}
}
// Elsewhere, listen for events
events.on("content:created", async (content) => {
// Clear cache
await cache.invalidate("content:*");
// Send notification
await notifications.send("New content created", content);
});
5. Plugin System
Extensible architecture for custom functionality.
interface Plugin {
name: string;
version: string;
initialize: (cms: CMS) => Promise<void>;
routes?: Route[];
widgets?: Widget[];
}
class PluginManager {
async register(plugin: Plugin): Promise<void> {
// Validate plugin
this.validate(plugin);
// Initialize plugin
await plugin.initialize(this.cms);
// Register routes and widgets
if (plugin.routes) this.registerRoutes(plugin.routes);
if (plugin.widgets) this.registerWidgets(plugin.widgets);
}
}
π State Management
SveltyCMS uses a combination of state management approaches:
1. Svelte Stores (Client-Side)
import { writable, derived } from "svelte/store";
// Simple store
export const count = writable(0);
// Derived store
export const doubled = derived(count, ($count) => $count * 2);
// Custom store with methods
function createCounter() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update((n) => n + 1),
decrement: () => update((n) => n - 1),
reset: () => set(0),
};
}
export const counter = createCounter();
Language State Management
SveltyCMS maintains two separate language contexts:
System Language (systemLanguage) - UI/admin interface language
Content Language (contentLanguage) - Language for content viewing/editing
// src/stores/store.svelte.ts
// app Singleton with Svelte 5 runes
export class AppStore {
_systemLanguage = $state<Locale>("en" as Locale);
_contentLanguage = $state<Locale>("en" as Locale);
// Multi-level accessors with persistence
get contentLanguage() {
return this._contentLanguage;
}
set contentLanguage(v: Locale) {
this._contentLanguage = v;
setCookie("contentLanguage", v);
}
}
export const app = new AppStore();
Language Synchronization Flow:
- Initial Load: Cookie β Environment Default β
'en' - URL Navigation: Server returns
contentLanguageβ Syncs to app singleton - User Toggle: translation-status dropdown β Updates app singleton β Navigates with
invalidateAll: true - Widget Display: Widgets read
app.contentLanguagereactively
<!-- +page.svelte - Syncs server data to app singleton -->
<script lang="ts">
import { app } from '@stores/store.svelte';
let { data } = $props();
let serverContentLanguage = $derived(data?.contentLanguage);
// Sync singleton with server-loaded language
$effect(() => {
if (serverContentLanguage && app.contentLanguage !== serverContentLanguage) {
app.contentLanguage = serverContentLanguage;
}
});
</script>
2. Server-Side State (SvelteKit Load Functions)
// +page.server.ts
export async function load({ locals, params }) {
const db = getDB(locals.dbType);
const post = await db.findOne("posts", { slug: params.slug });
return {
post,
};
}
Language-Aware Server Loading:
// src/routes/(app)/[language]/[...collection]/+page.server.ts
export async function load({ params, url, locals }) {
const { language, collection: collectionPath } = params;
const db = getDB();
// Language-specific cache key prevents cross-language data pollution
const cacheKey = `collection:${collectionPath}:${page}:${pageSize}:lang:${language}`;
const collectionData =
(await cache.get(cacheKey)) || (await db.findMany("collections", { path: collectionPath }));
// Return language with data for client sync
return {
collectionSchema,
entries,
pagination,
contentLanguage: language, // Synced to client store
};
}
3. Cache Layers
// In-memory cache with TTL
class CacheService {
private cache = new Map<string, CacheEntry>();
async get(key: string): Promise<any> {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expires) {
this.cache.delete(key);
return null;
}
return entry.value;
}
async set(key: string, value: any, ttl = 3600): Promise<void> {
this.cache.set(key, {
value,
expires: Date.now() + ttl * 1000,
});
}
}
π Multilingual Content Management
SveltyCMS provides comprehensive multilingual support with separation between UI language and content language.
Language Architecture
Two-Language System:
- System Language (
app.systemLanguage) - Admin interface language (menus, buttons, labels) - Content Language (
app.contentLanguage) - Language for viewing/editing content data
Configuration:
// config/public.ts
export const publicConfig = {
DEFAULT_CONTENT_LANGUAGE: "en",
AVAILABLE_CONTENT_LANGUAGES: ["en", "de", "fr", "es"],
BASE_LOCALE: "en", // System UI language
};
Content Language Flow
User Action β Singleton Update β Server Request β Cache Lookup β Widget Render
β β β β
app.contentLanguage /:lang/:path :lang:cache value[lang]
1. User Toggles Language:
<!-- translation-status.svelte -->
<script lang="ts">
import { goto } from '$app/navigation';
import { app } from '@stores/store.svelte';
async function switchLanguage(newLang: string) {
app.contentLanguage = newLang;
// Navigate with server data refresh
await goto(
`/${newLang}/${collectionPath}`,
{ invalidateAll: true } // Forces server reload
);
}
</script>
<button onclick={() => switchLanguage('de')}> π©πͺ Deutsch </button>
2. Server Returns Language-Specific Data:
// +page.server.ts
export async function load({ params }) {
const { language } = params; // 'en', 'de', etc.
// Language-specific cache prevents cross-contamination
const cacheKey = `entries:${collectionId}:lang:${language}`;
return {
entries: await getEntries(collectionId),
contentLanguage: language, // Passed to client
};
}
3. Client Syncs Store with Server:
<!-- +page.svelte -->
<script lang="ts">
let { data } = $props();
let serverContentLanguage = $derived(data.contentLanguage);
$effect(() => {
// Keep client store in sync with URL/server
if (serverContentLanguage !== contentLanguage.value) {
contentLanguage.set(serverContentLanguage);
}
});
</script>
4. Widgets Display Current Language:
<!-- Widget reads from store -->
<script lang="ts">
const _language = $derived(field.translated ? contentLanguage.value : 'en');
const displayValue = $derived(value[_language] ?? '');
</script>
<input bind:value={displayValue} />
Multilingual Data Model
Field Schema Definition:
// config/collections/Posts/fields.ts
const fields: FieldInstance[] = [
{
widget: "input",
label: "Title",
db_fieldName: "title",
translated: true, // β Marks field as multilingual
required: true,
},
{
widget: "input",
label: "Slug",
db_fieldName: "slug",
translated: false, // β Single value across all languages
required: true,
},
];
Data Storage (Database-Agnostic):
// Application layer (widgets, components)
const entry = {
_id: "post_123",
title: {
en: "Hello World",
de: "Hallo Welt",
fr: "Bonjour le monde",
},
slug: "hello-world", // Not translated
status: "published",
};
// MongoDB adapter (current)
db.collection("posts").insertOne(entry);
// Stores nested object directly: { title: { en: "...", de: "..." } }
// Future SQL/Drizzle adapter (planned)
// Transforms to relational structure:
// posts: id='post_123', slug='hello-world', status='published'
// post_translations:
// - post_id='post_123', field='title', lang='en', value='Hello World'
// - post_id='post_123', field='title', lang='de', value='Hallo Welt'
// - post_id='post_123', field='title', lang='fr', value='Bonjour le monde'
//
// Adapter reads from DB and reconstructs: { title: { en: "...", de: "..." } }
entry-list Display:
<!-- Shows current language in table view -->
<td>
{#if typeof entry[fieldName] === 'object' && entry[fieldName] !== null}
{entry[fieldName][contentLanguage.value] || '-'}
{:else}
{entry[fieldName] || '-'}
{/if}
</td>
Translation Workflow
Creating Multilingual Content:
-
Start in Default Language (usually βenβ)
- fields marked
translated: trueaccept input in default language - System saves:
{ title: { en: "New Post" } }
- fields marked
-
Switch to Secondary Language (e.g., βdeβ)
- translation-status dropdown shows completion: βEN: 100% | DE: 0%β
- Same fields now show empty (no German translation yet)
- User enters German text
- System saves:
{ title: { en: "New Post", de: "Neuer Beitrag" } }
-
Add More Languages
- Repeat process for French, Spanish, etc.
- System merges:
{ title: { en: "...", de: "...", fr: "...", es: "..." } }
Translation Status Indicator:
<!-- Shows % complete per language -->
<translation-status
availableLanguages={['en', 'de', 'fr']}
currentLanguage={contentLanguage.value}
completionStatus={{
en: 100, // All translated fields filled
de: 66, // 2 of 3 translated fields filled
fr: 0 // No translations yet
}}
/>
Best Practices
β DO:
- Mark all user-facing content as
translated: true(titles, descriptions, body text) - Keep technical fields as
translated: false(slugs, IDs, statuses) - Use
DEFAULT_CONTENT_LANGUAGEfor initial data entry - Provide translation status indicators in the UI
- Cache data with language-specific keys:
:lang:${language}
β DONβT:
- Donβt access
value.endirectly in widgets (usecontentLanguage.value) - Donβt mix UI language with content language
- Donβt cache multilingual data without language in the cache key
- Donβt assume all fields are translated (check
field.translated)
Migration to SQL/Drizzle
When migrating from MongoDB to SQL:
1. Database Adapter Changes:
- Implement
IDBAdapterinterface insrc/databases/drizzle/ - Transform nested objects β relational tables
- Widget layer remains unchanged
2. Data Migration Script:
// Transform MongoDB documents to SQL rows
for (const entry of mongoEntries) {
// Insert main record
await sql.insert("posts", {
id: entry._id,
slug: entry.slug,
status: entry.status,
});
// Insert translations
for (const [field, translations] of Object.entries(entry)) {
if (typeof translations === "object" && translations !== null) {
for (const [lang, value] of Object.entries(translations)) {
await sql.insert("post_translations", {
post_id: entry._id,
field,
language: lang,
value,
});
}
}
}
}
3. Adapter Read/Write:
// Drizzle adapter reads from SQL and reconstructs MongoDB format
async findOne(collection: string, id: string) {
const mainRecord = await db.select().from(posts).where(eq(posts.id, id));
const translations = await db.select()
.from(postTranslations)
.where(eq(postTranslations.post_id, id));
// Reconstruct: { title: { en: "...", de: "..." } }
const reconstructed = { ...mainRecord };
for (const trans of translations) {
if (!reconstructed[trans.field]) reconstructed[trans.field] = {};
reconstructed[trans.field][trans.language] = trans.value;
}
return reconstructed; // Widgets expect this format
}
π¦ Routing Architecture
SvelteKit file-based routing with clear separation and multilingual support:
src/routes/
βββ (app)/ # Main application (grouped route)
β βββ [language]/ # Language parameter (en, de, fr, etc.)
β β βββ [...collection]/ # Dynamic collection paths
β β β βββ +page.svelte # Collection view (entry-list/fields)
β β β βββ +page.server.ts # SSR data loading with language context
β β βββ +layout.svelte # Language-aware layout
β βββ +layout.svelte # App layout wrapper
β
βββ (admin)/ # Admin panel (grouped route)
β βββ +layout.svelte # Admin layout wrapper
β βββ dashboard/ # Dashboard pages
β βββ collections/ # Collection management
β βββ media/ # Media library
β βββ settings/ # Settings pages
β βββ users/ # User management
β
βββ api/ # API endpoints
β βββ auth/ # Authentication endpoints
β βββ collections/ # Collection CRUD
β βββ media/ # Media upload/management
β βββ settings/ # Settings API
β βββ widgets/ # Widget API
β
βββ +page.svelte # Homepage
βββ +layout.svelte # Root layout
βββ [slug]/ # Dynamic content pages
βββ +page.server.ts # Server-side rendering
Multilingual Route Examples:
/en/posts β English posts list
/de/posts β German posts list
/en/posts/create β Create post in English context
/de/6f8a3c2b-1234-5678-90ab β View collection by UUID in German
/fr/categories/blog/posts β French posts under blog category
Route Protection:
// hooks.server.ts
export async function handle({ event, resolve }) {
// Check authentication
const session = await getSession(event.cookies);
event.locals.user = session?.user || null;
// Protect admin routes
if (event.url.pathname.startsWith("/admin") && !event.locals.user) {
throw redirect(302, "/login");
}
return resolve(event);
}
Language-Aware Server Load:
// src/routes/(app)/[language]/[...collection]/+page.server.ts
export async function load({ params, url, locals }) {
const { language, collection } = params;
// Validate language
if (!AVAILABLE_CONTENT_LANGUAGES.includes(language)) {
throw error(404, `Language '${language}' not available`);
}
// Load data with language context
const cacheKey = `collection:${collection}:page:${page}:lang:${language}`;
return {
collectionSchema: await getCollectionSchema(collection),
entries: await getEntries(collection),
contentLanguage: language, // Synced to client store
};
}
π Navigation Best Practices
Recommended: Use Anchor Tags with Preloading
β DO: Use semantic HTML with SvelteKitβs built-in preloading:
<!-- Automatically preloads data when user hovers -->
<a href="/collection/posts/edit" data-sveltekit-preload-data="hover"> Edit Post </a>
<!-- For query parameters -->
<a href={`?edit=${entryId}`} data-sveltekit-preload-data="hover"> Edit Entry </a>
<!-- For multilingual routes -->
<a href={`/${contentLanguage.value}/posts`} data-sveltekit-preload-data="hover"> View Posts </a>
Benefits:
- β Automatic hover preloading (85-95% faster perceived load)
- β Better SEO (crawlable links)
- β Native keyboard navigation (Tab, Enter)
- β Right-click βOpen in new tabβ support
- β Screen reader friendly
- β No JavaScript required for basic functionality
β DONβT: Use buttons with programmatic navigation:
<!-- Anti-pattern: No preloading, worse accessibility -->
<button onclick={() => goto('/collection/posts/edit')}> Edit Post </button>
When to Use goto()
Only use programmatic navigation when you need to:
- Combine navigation with complex side effects
- Navigate conditionally based on async logic
- Handle form submissions with validation
- Close modals/sidebars before navigation
<script>
import { goto } from '$app/navigation';
async function handleComplexAction() {
// Complex logic before navigation
await saveData();
closeModal();
updateAnalytics();
// Then navigate programmatically
await goto('/success');
}
</script>
<!-- Use button for complex actions -->
<button onclick={handleComplexAction}> Save and Continue </button>
<!-- But use <a> tag for simple navigation -->
<a href="/cancel" data-sveltekit-preload-data="hover"> Cancel </a>
Side Effects with Navigation
Combine anchor tags with onclick handlers for side effects:
<script>
function handleSideEffects() {
// Side effects only (don't prevent navigation)
closeSidebar();
trackEvent('navigation_clicked');
}
</script>
<!-- Anchor handles navigation, onclick handles side effects -->
<a href="/dashboard" data-sveltekit-preload-data="hover" onclick={handleSideEffects}> Go to Dashboard </a>
External Links
<!-- External links should NOT use preload -->
<a href="https://external-site.com" target="_blank" rel="noopener noreferrer"> External Link </a>
For more details, see Hover Preloading Architecture.
π§ͺ Testing Strategy
SveltyCMS employs a dual-layered testing strategy to ensure both code quality and production reliability.
1. Unit Tests (White-Box)
Located in tests/unit/.
- Engine: Bun Test runner.
- Focus: Isolated logic, utility functions, state stores, and service layers.
- Environment: Purely in-memory. Uses global mocks (defined in
tests/unit/bun-preload.ts) for database adapters and configuration. - Isolation: Tests must not depend on a physical
config/private.tsor a live database.
2. Integration & E2E Tests (Black-Box)
Located in tests/integration/ and tests/e2e/.
- Engine: Bun (for Integration) and Playwright (for E2E).
- Focus: Full system lifecycle, API contracts, and User Experience.
- Environment: Realistic. Runs against the production
buildoutput and real database containers (MongoDB, Postgres, etc.) via Docker. - The βClean Slateβ Rule: These tests start with zero configuration. They must proceed through the Setup Wizard natively to generate
config/private.ts, validating the authentic installation process.
β‘ Performance Optimizations
1. Code Splitting
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
"svelte-vendor": ["svelte"],
"ui-components": ["./src/components/atoms", "./src/components/molecules"],
admin: ["./src/routes/(admin)"],
},
},
},
},
});
2. Lazy Loading
<script>
import { onMount } from 'svelte';
let HeavyComponent;
onMount(async () => {
const module = await import('./HeavyComponent.svelte');
HeavyComponent = module.default;
});
</script>
{#if HeavyComponent}
<svelte:component this={HeavyComponent} />
{/if}
3. Database Optimization
- Indexes on frequently queried fields
- Connection pooling for database connections
- Query optimization (select only needed fields)
- Pagination for large result sets
4. Caching Strategy
-
In-memory cache for frequently accessed data
-
Language-specific cache keys to prevent cross-language data pollution
// β Correct: Includes language in cache key const cacheKey = `entries:${collectionId}:page:${page}:lang:${language}`; // β Wrong: Missing language - will serve wrong language data const cacheKey = `entries:${collectionId}:page:${page}`;
- **Redis** for distributed caching (optional)
- **HTTP caching** headers for static assets
- **CDN** for media files
**Cache Invalidation for Multilingual Content:**
```typescript
// When updating translated content, invalidate all language variants
async function updateEntry(entryId: string, data: any) {
await db.update("entries", entryId, data);
// Clear cache for all languages
for (const lang of AVAILABLE_CONTENT_LANGUAGES) {
await cache.delete(`entry:${entryId}:lang:${lang}`);
}
}
π Security Measures
1. Authentication
- β JWT with expiration (24h default)
- β Secure session cookies (httpOnly, secure, sameSite)
- β Password hashing with bcrypt (10 rounds)
- β OAuth2 integration (Google, GitHub)
- β 2FA support (TOTP)
2. Authorization
- β Role-based access control (RBAC)
- β Permission system (create, read, update, delete)
- β Field-level permissions
- β Content ownership validation
3. Data Protection
- β Input validation (Zod schemas)
- β Output sanitization (HTML escaping)
- β SQL injection prevention (parameterized queries)
- β XSS prevention (content sanitization)
- β CSRF protection (tokens)
- β Rate limiting on API endpoints
π Build and Deployment
Development
# Install dependencies
npm install
# Start development server
npm run dev
# Run tests
npm test
# Lint code
npm run lint
# Format code
npm run format
Production Build
# Build for production
npm run build
# Preview production build
npm run preview
Deployment Options
-
Docker
FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npm run build CMD ["node", "build"]
2. **Vercel/Netlify**
- Automatic deployments from Git
- Serverless functions support
- Edge caching
3. **Traditional Hosting**
- VPS with Node.js
- PM2 for process management
- Nginx reverse proxy
---
## οΏ½ Quick Reference
### Multilingual Content Checklist
When working with multilingual content in SveltyCMS:
**For Widget Development:**
```typescript
// β
Read current language from store
import { contentLanguage } from "@stores/store.svelte";
const _language = $derived(field.translated ? contentLanguage.value : DEFAULT_CONTENT_LANGUAGE);
// β
Access language-specific value
const displayValue = $derived(value[_language] ?? "");
// β
Update language-specific value
function updateValue(newVal: string) {
if (field.translated) {
value = { ...(value as object), [_language]: newVal };
} else {
value = newVal;
}
}
For API Endpoints:
// β
Include language in cache keys
const cacheKey = `resource:${id}:lang:${language}`;
// β
Return contentLanguage to client
return { data, contentLanguage: params.language };
// β
Validate language parameter
if (!AVAILABLE_CONTENT_LANGUAGES.includes(language)) {
throw error(404, "Language not available");
}
For Components:
// β
Display translated values in lists
{
entry[fieldName][contentLanguage.value] || "-";
}
// β
Sync store from server data
$effect(() => {
if (serverContentLanguage !== contentLanguage.value) {
contentLanguage.set(serverContentLanguage);
}
});
// β
Navigate with language
await goto(`/${contentLanguage.value}/${path}`, { invalidateAll: true });
For Collection Schemas:
// β
Mark translatable fields
{
widget: 'input',
db_fieldName: 'title',
translated: true, // β Enables multilingual support
}
// β
Keep technical fields untranslated
{
widget: 'input',
db_fieldName: 'slug',
translated: false, // β Single value across languages
}
οΏ½π Additional Resources
- API Documentation - Complete API reference
- Widget System - Widget architecture
- Database Architecture - Database adapter pattern
- Security Plugin - Security implementation
- Contributing Guide - How to contribute
π€ Contributing
When contributing code:
- Strict Casing: All
.sveltefiles and widget folders must be strictly lowercase (kebab-case). - Use Aliases: Always use standard aliases (
@src,@widgets,@utils, etc.) instead of relative paths. - Write Tests: Unit tests for logic (White-Box), E2E for features (Black-Box).
- Document: Update relevant MDX docs in
docs/.
See Contributing Guide for detailed guidelines.