Skip to content

Documentation

Code Structure & Organization

Comprehensive overview of SveltyCMS codebase organization, architecture, and design patterns including multilingual content management

3/27/2026
26 min read Edit on GitHub

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 check before commits. Use the oxlint and oxfmt setup (provided via latest vite-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 (via tailwind-merge & clsx) for building conflict-free UI components.
  • pluralize.ts: Universal runtime pluralization utilizing Intl.PluralRules to 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:

  1. Initial Load: Cookie β†’ Environment Default β†’ 'en'
  2. URL Navigation: Server returns contentLanguage β†’ Syncs to app singleton
  3. User Toggle: translation-status dropdown β†’ Updates app singleton β†’ Navigates with invalidateAll: true
  4. Widget Display: Widgets read app.contentLanguage reactively
<!-- +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:

  1. System Language (app.systemLanguage) - Admin interface language (menus, buttons, labels)
  2. 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:

  1. Start in Default Language (usually β€˜en’)

    • fields marked translated: true accept input in default language
    • System saves: { title: { en: "New Post" } }
  2. 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" } }
  3. 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_LANGUAGE for initial data entry
  • Provide translation status indicators in the UI
  • Cache data with language-specific keys: :lang:${language}

❌ DON’T:

  • Don’t access value.en directly in widgets (use contentLanguage.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 IDBAdapter interface in src/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.ts or 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 build output 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

  1. 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


🀝 Contributing

When contributing code:

  1. Strict Casing: All .svelte files and widget folders must be strictly lowercase (kebab-case).
  2. Use Aliases: Always use standard aliases (@src, @widgets, @utils, etc.) instead of relative paths.
  3. Write Tests: Unit tests for logic (White-Box), E2E for features (Black-Box).
  4. Document: Update relevant MDX docs in docs/.

See Contributing Guide for detailed guidelines.


Related

architecturestructurepatternsorganizationmultilinguali18n
Was this page helpful?