Claude-skill-registry composable-svelte-ssr
Server-side rendering patterns for Composable Svelte. Use when implementing SSR, hydration, server rendering, isomorphic code, or working with meta tags and SEO. Covers renderToHTML, hydrateStore, server-side routing, state serialization, and avoiding common SSR pitfalls.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/composable-svelte-ssr" ~/.claude/skills/majiayu000-claude-skill-registry-composable-svelte-ssr && rm -rf "$T"
skills/data/composable-svelte-ssr/SKILL.mdComposable Svelte SSR
This skill covers server-side rendering (SSR) patterns for Composable Svelte applications.
SSR APIs
renderToHTML - Server Rendering
import { renderToHTML } from '@composable-svelte/core/ssr'; import { createStore } from '@composable-svelte/core'; import App from './App.svelte'; app.get('/', async (req, res) => { // 1. Load data for this request const data = await loadData(req.user); // 2. Create store with pre-populated data const store = createStore({ initialState: data, reducer: appReducer, dependencies: {} // Empty on server - effects won't run }); // 3. Render to HTML const html = renderToHTML(App, { store }, { head: `<link rel="stylesheet" href="/assets/index.css">`, clientScript: '/assets/index.js' }); // 4. Send response res.send(html); });
hydrateStore - Client Hydration
import { hydrateStore } from '@composable-svelte/core/ssr'; import { mount } from 'svelte'; import App from './App.svelte'; // 1. Read state from script tag const stateJSON = document.getElementById('__COMPOSABLE_SVELTE_STATE__')?.textContent; // 2. Hydrate with client dependencies const store = hydrateStore(stateJSON, { reducer: appReducer, dependencies: { api: createAPIClient(), storage: createLocalStorage() } }); // 3. Mount app (reuses existing DOM from SSR) mount(App, { target: document.body, props: { store } });
ISOMORPHIC PATTERNS
Server vs Client Dependencies
Key Pattern: Server has empty dependencies, client has real implementations.
// server.ts const store = createStore({ initialState: data, reducer: appReducer, dependencies: {} as AppDependencies // Empty - effects won't run // ssr.deferEffects defaults to true, so effects are automatically skipped }); // client.ts const store = hydrateStore(stateJSON, { reducer: appReducer, dependencies: { api: createAPIClient(), // Real API client storage: localStorage, // Real storage clock: new SystemClock() // Real clock } });
Why: Server doesn't need to execute effects - it just renders initial state. Client needs real dependencies for interactivity.
Router Pure Functions on Server
Key Pattern: Use router's pure functions (
parseDestination, matchPath, serializeDestination) on both server and client.
// routing.ts (shared between server and client) export function parsePostFromURL(path: string, defaultId: number): number { const match = path.match(/^\/posts\/(\d+)$/); return match ? parseInt(match[1], 10) : defaultId; } // server.ts async function renderApp(request: any, reply: any) { const posts = await loadPosts(); const path = request.url; const requestedPostId = parsePostFromURL(path, posts[0]?.id || 1); const store = createStore({ initialState: { posts, selectedPostId: requestedPostId, meta: computeMetaForPost(posts.find(p => p.id === requestedPostId)) }, reducer: appReducer, dependencies: {} }); const html = renderToHTML(App, { store }); reply.type('text/html').send(html); }
STATE INITIALIZATION FROM URL
Pattern: Parse URL on Server, Initialize State
// server.ts import { parseDestination } from './shared/routing'; app.get('/posts/:id', async (req, res) => { const postId = parseInt(req.params.id, 10); // Load data based on URL const posts = await loadPosts(); const selectedPost = posts.find(p => p.id === postId) || posts[0]; // Initialize state with URL-driven selection const store = createStore({ initialState: { posts, selectedPostId: selectedPost?.id || null, // Set initial meta based on URL-selected post meta: selectedPost ? { title: `${selectedPost.title} - Blog`, description: selectedPost.content.slice(0, 160), ogImage: `/og/post-${selectedPost.id}.jpg`, canonical: `https://example.com/posts/${selectedPost.id}` } : initialState.meta }, reducer: appReducer, dependencies: {} }); const html = renderToHTML(App, { store }); res.send(html); });
STATE SERIALIZATION/DESERIALIZATION
Automatic Serialization
When you call
renderToHTML, the store state is automatically serialized and embedded in the HTML:
const html = renderToHTML(App, { store }); // HTML contains: <script id="__COMPOSABLE_SVELTE_STATE__" type="application/json">...</script>
Automatic Deserialization
When you call
hydrateStore, the state is automatically deserialized:
const stateJSON = document.getElementById('__COMPOSABLE_SVELTE_STATE__')?.textContent; const store = hydrateStore(stateJSON, { reducer, dependencies });
Custom Serialization (Advanced)
For complex types (Date, Map, Set), provide custom serializers:
import { serializeState, parseState } from '@composable-svelte/core/ssr'; // Server const serialized = serializeState(store.state, { customSerializers: { Date: (date) => ({ __type: 'Date', value: date.toISOString() }), Map: (map) => ({ __type: 'Map', entries: Array.from(map.entries()) }) } }); // Client const state = parseState(serialized, { customParsers: { Date: (obj) => new Date(obj.value), Map: (obj) => new Map(obj.entries) } });
STATE-DRIVEN META TAGS
Pattern: Compute Meta Tags in Reducer
Best Practice: Meta tags should be computed from state in the reducer, then rendered via
<svelte:head> in components.
// State interface AppState { posts: Post[]; selectedPostId: number | null; meta: MetaTags; } interface MetaTags { title: string; description: string; ogImage?: string; canonical?: string; } // Reducer computes meta tags case 'selectPost': { const post = state.posts.find(p => p.id === action.postId); return [ { ...state, selectedPostId: action.postId, meta: post ? { title: `${post.title} - Blog`, description: post.content.slice(0, 160), ogImage: `/og/post-${post.id}.jpg`, canonical: `https://example.com/posts/${post.id}` } : state.meta }, Effect.none() ]; } // Component renders meta tags <svelte:head> <title>{$store.meta.title}</title> <meta name="description" content={$store.meta.description} /> {#if $store.meta.ogImage} <meta property="og:title" content={$store.meta.title} /> <meta property="og:description" content={$store.meta.description} /> <meta property="og:image" content={$store.meta.ogImage} /> {/if} {#if $store.meta.canonical} <link rel="canonical" href={$store.meta.canonical} /> {/if} </svelte:head>
Why: Meta tags are part of application state. Computing them in the reducer ensures they're consistent on server and client, and testable with TestStore.
COMPLETE SSR EXAMPLE
Server (Fastify)
// server/index.ts import Fastify from 'fastify'; import fastifyStatic from '@fastify/static'; import { createStore } from '@composable-svelte/core'; import { renderToHTML } from '@composable-svelte/core/ssr'; import App from '../shared/App.svelte'; import { appReducer } from '../shared/reducer'; import { initialState } from '../shared/types'; import { loadPosts } from './data'; import { parsePostFromURL } from '../shared/routing'; const app = Fastify({ logger: { level: process.env.NODE_ENV === 'production' ? 'info' : 'debug' } }); // Serve static files (client bundle) app.register(fastifyStatic, { root: join(__dirname, '../client'), prefix: '/assets/' }); // Main SSR route handler async function renderAppRoute(request: any, reply: any) { try { // 1. Parse URL using router (same logic as client!) const posts = await loadPosts(); const path = request.url; const requestedPostId = parsePostFromURL(path, posts[0]?.id || 1); // Find the requested post const selectedPost = posts.find((p) => p.id === requestedPostId) || posts[0]; // 2. Create store with URL-driven state const store = createStore({ initialState: { ...initialState, posts, selectedPostId: selectedPost?.id || null, // Set initial meta based on URL-selected post meta: selectedPost ? { title: `${selectedPost.title} - Blog`, description: selectedPost.content.slice(0, 160), ogImage: `/og/post-${selectedPost.id}.jpg`, canonical: `https://example.com/posts/${selectedPost.id}` } : initialState.meta }, reducer: appReducer, dependencies: {} // ssr.deferEffects defaults to true, so effects are automatically skipped }); // 3. Render component to HTML const html = renderToHTML(App, { store }, { head: ` <link rel="stylesheet" href="/assets/index.css"> <style> * { box-sizing: border-box; } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; } </style> `, clientScript: '/assets/index.js' }); // 4. Send response reply.type('text/html').send(html); } catch (error) { request.log.error(error); reply.status(500).send({ error: 'Internal Server Error', message: error instanceof Error ? error.message : 'Unknown error' }); } } // Register routes app.get('/', renderAppRoute); app.get('/posts/:id', renderAppRoute); // Start server const start = async () => { try { const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; const host = process.env.HOST || '0.0.0.0'; await app.listen({ port, host }); console.log(`Server running at http://localhost:${port}`); } catch (err) { app.log.error(err); process.exit(1); } }; start();
Client
// client/index.ts import { hydrate as hydrateComponent } from 'svelte'; import { hydrateStore } from '@composable-svelte/core/ssr'; import { syncBrowserHistory } from '@composable-svelte/core/routing'; import App from '../shared/App.svelte'; import { appReducer } from '../shared/reducer'; import type { AppDependencies, AppState, AppAction } from '../shared/types'; import { parserConfig, serializerConfig } from '../shared/routing'; // Client-side dependencies const clientDependencies: AppDependencies = { fetchPosts: async () => { // In a real app, this would fetch from an API // For this example, we'll just return empty array // (the data is already loaded via SSR) return []; } }; // Hydrate the application function hydrate() { try { // 1. Read serialized state from the server const stateElement = document.getElementById('__COMPOSABLE_SVELTE_STATE__'); if (!stateElement || !stateElement.textContent) { throw new Error('No hydration data found. Server-side rendering may have failed.'); } // 2. Hydrate the store with client dependencies const store = hydrateStore<AppState, AppAction, AppDependencies>( stateElement.textContent, { reducer: appReducer, dependencies: clientDependencies } ); // 3. Sync browser history with state (URL routing!) syncBrowserHistory(store, { serializers: serializerConfig.serializers, parsers: parserConfig.parsers, // Map state → destination for URL serialization getDestination: (state) => { if (state.selectedPostId !== null) { return { type: 'post' as const, state: { postId: state.selectedPostId } }; } return null; }, // Map destination → action for back/forward navigation destinationToAction: (dest) => { if (dest?.type === 'post') { return { type: 'selectPost', postId: dest.state.postId }; } return null; } }); // 4. Hydrate the app (reuse existing DOM from SSR) const app = hydrateComponent(App, { target: document.body, props: { store } }); console.log('✅ Composable Svelte hydrated successfully with URL routing'); // Cleanup on unmount (for HMR during development) if (import.meta.hot) { import.meta.hot.dispose(() => { app.$destroy?.(); }); } } catch (error) { console.error('❌ Hydration failed:', error); // Show error to user document.body.innerHTML = ` <div style="display: flex; align-items: center; justify-content: center; min-height: 100vh; background: #fee; color: #c00; font-family: monospace; padding: 2rem;"> <div> <h1>Hydration Error</h1> <p>${error instanceof Error ? error.message : 'Unknown error'}</p> </div> </div> `; } } // Start hydration when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', hydrate); } else { hydrate(); }
AVOIDING STATE LEAKS
❌ WRONG - Shared Store
// server.ts // ❌ BAD: Shared store across requests const globalStore = createStore({ initialState: {}, reducer: appReducer, dependencies: {} }); app.get('/', (req, res) => { // ❌ State leaks between requests! const html = renderToHTML(App, { store: globalStore }); res.send(html); });
✅ CORRECT - Per-Request Store
// server.ts // ✅ GOOD: Create new store for each request app.get('/', async (req, res) => { const store = createStore({ initialState: await loadDataForRequest(req), reducer: appReducer, dependencies: {} }); const html = renderToHTML(App, { store }); res.send(html); });
Why: Each request needs its own store to prevent state leaking between users.
I18N SSR PATTERNS
Manual i18n Initialization (Fastify/Custom Servers)
Key Pattern: For non-SvelteKit servers (Fastify, Express, etc.), manually initialize i18n state and dependencies.
import { createInitialI18nState, BundledTranslationLoader, createStaticLocaleDetector, serverDOM, browserDOM } from '@composable-svelte/core/i18n'; // Server: Detect locale from request function detectLocale(request: any): string { // 1. Check query param (?lang=fr) const queryLang = request.query?.lang; if (queryLang && ['en', 'fr', 'es'].includes(queryLang)) { return queryLang; } // 2. Check Accept-Language header const acceptLanguage = request.headers?.['accept-language']; if (acceptLanguage && typeof acceptLanguage === 'string') { const languages = acceptLanguage.split(',') .map(lang => lang.trim().split(';')[0].split('-')[0]); for (const lang of languages) { if (['en', 'fr', 'es'].includes(lang)) { return lang; } } } // 3. Default to English return 'en'; } // Server: Initialize i18n for SSR async function renderApp(request, reply) { const locale = detectLocale(request); const i18nState = createInitialI18nState(locale, ['en', 'fr', 'es'], 'en'); // Create translation loader const translationLoader = new BundledTranslationLoader({ bundles: { en: { common: enTranslations }, fr: { common: frTranslations }, es: { common: esTranslations } } }); // Preload translations for current locale const translations = await translationLoader.load('common', locale); const updatedI18nState = { ...i18nState, translations: { [`${locale}:common`]: translations } }; // Create mock storage for server (no-op) const mockStorage = { getItem: (key: string) => null, setItem: (key: string, value: unknown) => {}, removeItem: (key: string) => {}, keys: () => [], has: (key: string) => false, clear: () => {} }; // Create i18n dependencies for server const i18nDependencies = { translationLoader, localeDetector: createStaticLocaleDetector(locale, ['en', 'fr', 'es']), storage: mockStorage, dom: serverDOM }; const store = createStore({ initialState: { ...initialState, i18n: updatedI18nState }, reducer: appReducer, dependencies: { ...otherDependencies, ...i18nDependencies } }); const html = renderToHTML(App, { store }); reply.type('text/html').send(html); }
Client i18n Hydration
import { BundledTranslationLoader, createStaticLocaleDetector, browserDOM } from '@composable-svelte/core/i18n'; // Client: Hydrate with localStorage-backed storage const clientStorage = { getItem: (key: string) => { try { return localStorage.getItem(key); } catch { return null; } }, setItem: (key: string, value: unknown) => { try { localStorage.setItem(key, String(value)); } catch {} }, removeItem: (key: string) => { try { localStorage.removeItem(key); } catch {} }, keys: () => { try { return Object.keys(localStorage); } catch { return []; } }, has: (key: string) => { try { return localStorage.getItem(key) !== null; } catch { return false; } }, clear: () => { try { localStorage.clear(); } catch {} } }; // Hydrate i18n on client async function hydrate() { const stateElement = document.getElementById('__COMPOSABLE_SVELTE_STATE__'); const parsedState = JSON.parse(stateElement.textContent); const locale = parsedState.i18n.currentLocale; const translationLoader = new BundledTranslationLoader({ bundles: { en: { common: enTranslations }, fr: { common: frTranslations }, es: { common: esTranslations } } }); const i18nDependencies = { translationLoader, localeDetector: createStaticLocaleDetector(locale, ['en', 'fr', 'es']), storage: clientStorage, // Real localStorage dom: browserDOM }; const store = hydrateStore(stateElement.textContent, { reducer: appReducer, dependencies: { ...otherDependencies, ...i18nDependencies } }); hydrateComponent(App, { target: document.body, props: { store } }); }
Storage Interface Requirements
Critical: The i18n system expects a
Storage interface, not a Map.
// ❌ WRONG - Map doesn't have setItem/getItem const i18nDependencies = { storage: new Map() // TypeError: storage.setItem is not a function }; // ✅ CORRECT - Implement Storage interface const mockStorage: Storage = { getItem: (key: string) => null, setItem: (key: string, value: unknown) => {}, removeItem: (key: string) => {}, keys: () => [], has: (key: string) => false, clear: () => {} };
i18n + URL Routing Pattern
Combine i18n with URL routing to support language selection via URL:
// Server: Support ?lang=fr query parameter app.get('/', async (req, res) => { const locale = detectLocale(req); // Checks ?lang= first, then Accept-Language const destination = parseDestinationFromURL(req.url); const store = createStore({ initialState: { destination, i18n: createInitialI18nState(locale, ['en', 'fr', 'es']) // ... } }); // ... });
URL Pattern:
http://localhost:3000/?lang=fr or http://localhost:3000/posts/1?lang=es
SSR PERFORMANCE CONSIDERATIONS
1. Defer Effects on Server
Automatic: Effects are automatically deferred on server (via
ssr.deferEffects: true default).
// No need to set this explicitly - it's the default const store = createStore({ initialState: data, reducer: appReducer, dependencies: {}, // ssr: { deferEffects: true } // Default });
2. Load Data Once on Server
app.get('/posts/:id', async (req, res) => { // Load data once const posts = await loadPosts(); const postId = parseInt(req.params.id, 10); const selectedPost = posts.find(p => p.id === postId); // Initialize state with loaded data const store = createStore({ initialState: { posts, // Data already loaded selectedPostId: postId, meta: computeMeta(selectedPost) }, reducer: appReducer, dependencies: {} }); const html = renderToHTML(App, { store }); res.send(html); });
Why: Server pre-loads data, client hydrates with it. No need to fetch again on client.
COMMON SSR PITFALLS
1. Using Browser APIs on Server
❌ WRONG:
// reducer.ts case 'init': const theme = localStorage.getItem('theme'); // ❌ localStorage not available on server! return [{ ...state, theme }, Effect.none()];
✅ CORRECT:
// Use environment detection import { isServer } from '@composable-svelte/core/ssr'; case 'init': const theme = isServer ? 'light' : localStorage.getItem('theme') || 'light'; return [{ ...state, theme }, Effect.none()];
2. Not Handling Hydration Errors
❌ WRONG:
// client.ts const store = hydrateStore(stateJSON, { reducer, dependencies }); mount(App, { target: document.body, props: { store } }); // If hydration fails, user sees blank screen!
✅ CORRECT:
try { const store = hydrateStore(stateJSON, { reducer, dependencies }); mount(App, { target: document.body, props: { store } }); console.log('✅ Hydrated successfully'); } catch (error) { console.error('❌ Hydration failed:', error); // Show error UI document.body.innerHTML = `<div class="error">Hydration failed: ${error.message}</div>`; }
3. Forgetting to Set Meta Tags
❌ WRONG:
// No meta tags in state, no <svelte:head> in component // Search engines see generic meta tags
✅ CORRECT:
// State includes meta tags interface AppState { meta: { title: string; description: string; ogImage?: string }; } // Component renders meta tags <svelte:head> <title>{$store.meta.title}</title> <meta name="description" content={$store.meta.description} /> </svelte:head>
SSR CHECKLIST
- 1. Create new store for each request (no shared state)
- 2. Use empty dependencies on server
- 3. Load data based on URL on server
- 4. Initialize state with loaded data
- 5. Compute meta tags in reducer
- 6. Render meta tags with
<svelte:head> - 7. Hydrate with real dependencies on client
- 8. Sync browser history on client (if using routing)
- 9. Handle hydration errors gracefully
- 10. Use environment detection for browser APIs
STATIC SITE GENERATION (SSG)
generateStaticSite - Build-Time Generation
import { generateStaticSite } from '@composable-svelte/core/ssr'; import App from './App.svelte'; import { appReducer } from './reducer'; const posts = await loadPosts(); const result = await generateStaticSite(App, { routes: [ { path: '/' }, { path: '/about' }, { path: '/posts/:id', paths: posts.map(p => `/posts/${p.id}`), getServerProps: async (path) => ({ post: await loadPost(path) }) } ], outDir: './dist', baseURL: 'https://example.com', onPageGenerated: (path, outPath) => { console.log(`Generated ${path} → ${outPath}`); } }, { reducer: appReducer, dependencies: {}, getInitialState: (path) => ({ /* compute state for path */ }) }); console.log(`Generated ${result.pagesGenerated} pages in ${result.duration}ms`);
SSG Configuration
Full-Site Generation:
// Generate all routes at build time await generateStaticSite(App, { routes: [ { path: '/' }, // Static route { path: '/about' }, // Static route { path: '/posts/:id', // Dynamic route paths: ['/posts/1', '/posts/2'] // Pre-rendered paths } ], outDir: './static' }, { reducer, dependencies: {} });
Selective Generation:
// Generate only specific pages await generateStaticSite(App, { routes: [ { path: '/' }, // Only homepage { path: '/posts/1' } // Only one post ], outDir: './static' }, { reducer, dependencies: {} });
Dynamic Path Generation:
// Fetch paths dynamically at build time await generateStaticSite(App, { routes: [ { path: '/posts/:id', paths: async () => { const posts = await loadAllPosts(); return posts.map(p => `/posts/${p.id}`); } } ], outDir: './static' }, { reducer, dependencies: {} });
SSG + i18n Pattern
Multi-Locale Static Generation:
const supportedLocales = ['en', 'fr', 'es']; const posts = await loadPosts(); // Generate routes for each locale const routes = []; for (const locale of supportedLocales) { const localePrefix = locale === 'en' ? '' : `/${locale}`; // Home page routes.push({ path: `${localePrefix}/`, getServerProps: async (path) => { const i18nState = await initI18n(locale); return { ...initialState, i18n: i18nState }; } }); // Post pages for (const post of posts) { routes.push({ path: `${localePrefix}/posts/${post.id}`, getServerProps: async (path) => { const i18nState = await initI18n(locale); const post = await loadPost(post.id); return { ...initialState, post, i18n: i18nState }; } }); } } await generateStaticSite(App, { routes, outDir: './static' }, { reducer });
SSG Build Script
Create build script (
src/build/ssg.ts):
import { generateStaticSite } from '@composable-svelte/core/ssr'; import App from '../shared/App.svelte'; import { appReducer } from '../shared/reducer'; import { loadPosts } from '../server/data'; async function build() { console.log('Starting SSG build...'); const posts = await loadPosts(); const result = await generateStaticSite(App, { routes: [ { path: '/' }, { path: '/posts/:id', paths: posts.map(p => `/posts/${p.id}`), getServerProps: async (path) => { const id = parseInt(path.split('/').pop()!); const post = await loadPost(id); return { posts: [post] }; } } ], outDir: './static', baseURL: 'https://example.com' }, { reducer: appReducer, dependencies: {} }); console.log(`✅ Generated ${result.pagesGenerated} pages in ${result.duration}ms`); } build().catch(console.error);
Add script to package.json:
{ "scripts": { "build:ssg": "vite build && tsx src/build/ssg.ts" } }
Run build:
pnpm build:ssg
SSG vs SSR Decision Matrix
| Use Case | Recommendation | Reason |
|---|---|---|
| Blog posts | SSG | Content rarely changes, many reads |
| User dashboards | SSR | Personalized, private data |
| Product catalog | SSG | Public, static content |
| Search results | SSR | Dynamic, user-specific |
| Marketing pages | SSG | Static, performance-critical |
| Admin panels | SSR | Dynamic, authenticated |
Hybrid SSG + SSR Pattern
Use SSG for static pages, SSR for dynamic:
-
Build-time (SSG): Generate static pages
pnpm build:ssg # Generates /static/index.html, /static/posts/*/index.html -
Runtime (SSR): Serve dynamic pages
// Server fallback for non-static routes app.get('*', async (req, res) => { // Try to serve static file first const staticPath = join(__dirname, '../static', req.url, 'index.html'); if (existsSync(staticPath)) { return res.sendFile(staticPath); } // Fall back to SSR for dynamic routes const store = createStore({ /* ... */ }); const html = renderToHTML(App, { store }); res.send(html); });
SUMMARY
This skill covers SSR and SSG patterns for Composable Svelte:
- SSR APIs: renderToHTML, hydrateStore
- SSG APIs: generateStaticSite, generateStaticPage
- Isomorphic Patterns: Server vs client dependencies, router pure functions
- State Initialization: Parse URL on server, initialize state
- State Serialization: Automatic serialization/deserialization
- Meta Tags: State-driven meta tags computed by reducer
- i18n SSR/SSG: Manual i18n initialization, locale detection, multi-locale generation
- Complete Examples: Fastify server + SSG build script + client hydration
- Avoiding Pitfalls: Per-request stores, environment detection, error handling
SSR Key Points:
- Create new store for each request
- Use empty dependencies on server
- Hydrate with real dependencies on client
- State is serialized automatically
SSG Key Points:
- Generate static HTML at build time
- Support dynamic routes with path enumeration
- Use getServerProps to load data for each path
- Combine with i18n for multi-locale sites
- Ideal for content-heavy, rarely-changing sites
i18n Key Points:
- Use
withBundledTranslationLoader
wrapperbundles - Server: Mock storage (no-op),
,createStaticLocaleDetectorserverDOM - Client: localStorage-backed storage,
,createStaticLocaleDetectorbrowserDOM - Detect locale from query param → Accept-Language → default
- Storage interface requires
, not Map'sgetItem/setItemget/set
For core architecture, see composable-svelte-core skill. For URL routing, see composable-svelte-navigation skill. For testing SSR/SSG, see composable-svelte-testing skill.