Claude Code Plugins

Community-maintained marketplace

Feedback

Comprehensive guide for building SvelteKit applications in SPA (Single Page Application) mode with client-side rendering only. Use when working on SvelteKit projects that use adapter-static with CSR, especially those with separate backends like Golang/Echo. Covers routing, page options, data loading, and proper SPA configuration while avoiding SSR features.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name sveltekit-spa
description Comprehensive guide for building SvelteKit applications in SPA (Single Page Application) mode with client-side rendering only. Use when working on SvelteKit projects that use adapter-static with CSR, especially those with separate backends like Golang/Echo. Covers routing, page options, data loading, and proper SPA configuration while avoiding SSR features.

SvelteKit SPA Mode

Guide for building SvelteKit applications in pure SPA/CSR mode with adapter-static, specifically optimized for projects with separate backends (e.g., Golang + Echo).

About This Skill

Type: Reference guide (flexible pattern)

This skill provides patterns and conventions for SvelteKit SPA development. The core requirements (SSR disabled, adapter-static configuration) are mandatory for SPA mode, but implementation details should be adapted to your project's needs.

💡 Additional Resources: This skill includes detailed reference documentation:

  • references/routing-patterns.md - Complex routing scenarios, nested layouts, route guards
  • references/backend-integration.md - Detailed API patterns, authentication flows, error handling

Core Concept

SvelteKit SPA mode creates a fully client-rendered single-page application. The entire app runs in the browser with a fallback HTML page that bootstraps the application for any route.

Key characteristics:

  • No server-side rendering (SSR disabled)
  • All routing handled client-side
  • Backend is separate (API-only)
  • Uses adapter-static with fallback page
  • Full SvelteKit routing capabilities without SSR complexity

Initial SPA Setup Checklist

When setting up a new SvelteKit project in SPA mode:

  • Install @sveltejs/adapter-static
  • Configure adapter in svelte.config.js with fallback page
  • Create src/routes/+layout.ts with export const ssr = false and export const prerender = false
  • Set up environment variables for API URL (.env file)
  • Configure CORS on backend API
  • Test build process with bun run build
  • Test locally with bun run preview
  • Verify routing works after page refresh

Migrating Existing SvelteKit Project to SPA Mode

If converting an existing SvelteKit project:

  • Install @sveltejs/adapter-static (remove other adapters)
  • Update svelte.config.js adapter configuration
  • Add ssr = false and prerender = false to root +layout.ts
  • Convert all +page.server.js files to +page.ts
  • Remove all +server.js API routes (move to backend)
  • Update load functions to use absolute API URLs with environment variables
  • Test all routes still work client-side
  • Update deployment configuration for static hosting

Project Configuration

Adapter Setup

// svelte.config.js
import adapter from '@sveltejs/adapter-static';

export default {
	kit: {
		adapter: adapter({
			pages: 'build',
			assets: 'build',
			fallback: '200.html', // or '404.html', 'index.html' depending on host
			precompress: false,
			strict: false // Set false since we're not prerendering
		})
	}
};

Fallback page selection:

  • 200.html - For hosts like Surge that support catch-all routes
  • 404.html - For hosts that serve 404.html for missing routes (GitHub Pages)
  • index.html - Avoid unless necessary (can conflict with prerendered pages)

Disable SSR Globally

// src/routes/+layout.ts
export const ssr = false;
export const prerender = false;

Critical: Both exports are required for SPA mode:

  • ssr = false - Disables server-side rendering (all rendering happens client-side)
  • prerender = false - Disables prerendering at build time (unless selectively enabled per route)

This configuration ensures the entire application runs as a pure SPA with client-side rendering only.

Routing in SPA Mode

SvelteKit's filesystem-based routing works identically in SPA mode. The only difference is that all routes render client-side.

Basic Route Structure

laneweaver-frontend/
├── bun.lock
├── components.json
├── e2e/                    # Playwright end-to-end tests
│   └── demo.test.ts
├── eslint.config.js
├── package.json
├── playwright.config.ts
├── src/
│   ├── app.css             # Global styles
│   ├── app.d.ts            # TypeScript declarations
│   ├── app.html            # HTML template
│   ├── lib/
│   │   ├── assets/
│   │   │   └── favicon.svg
│   │   ├── components/
│   │   │   └── ui/         # Reusable UI components
│   │   ├── index.ts        # Library exports
│   │   └── utils.ts        # Utility functions
│   └── routes/
│       ├── +layout.svelte  # Root layout (nav, etc.)
│       ├── +layout.ts      # SSR disable, shared data
│       ├── +page.svelte    # Home page
│       ├── dashboard/
│       │   ├── +page.svelte        # /dashboard
│       │   ├── +layout.svelte      # Dashboard layout
│       │   └── [id]/
│       │       └── +page.svelte    # /dashboard/:id
│       └── api/
│           └── +server.ts  # ❌ AVOID - Use backend API instead
├── static/                 # Static assets (served as-is)
├── svelte.config.js
├── tsconfig.json
└── vite.config.ts

Route Files

+page.svelte - Page component

<script>
	let { data } = $props(); // Data from +page.js load function
</script>

<h1>{data.title}</h1>

+page.ts - Client-side data loading

export const ssr = false; // Optional if set in root layout
export const prerender = false; // Optional if set in root layout

/** @type {import('./$types').PageLoad} */
export async function load({ fetch, params }) {
	// Fetch from your backend API
	const API_URL = import.meta.env.VITE_API_URL;
	const res = await fetch(`${API_URL}/api/items/${params.id}`);
	return await res.json();
}

+layout.svelte - Shared layout

<script>
	let { children } = $props();
</script>

<nav>
	<a href="/">Home</a>
	<a href="/dashboard">Dashboard</a>
</nav>

{@render children()}

Dynamic Routes

src/routes/
└── blog/
    └── [slug]/
        ├── +page.svelte    # /blog/hello-world
        └── +page.ts        # Load data for slug
// src/routes/blog/[slug]/+page.ts
export const ssr = false;
export const prerender = false;

/** @type {import('./$types').PageLoad} */
export async function load({ params, fetch }) {
	const API_URL = import.meta.env.VITE_API_URL;
	const response = await fetch(`${API_URL}/api/blog/${params.slug}`);
	if (!response.ok) {
		throw error(404, 'Post not found');
	}
	return await response.json();
}

Security Considerations

Authentication Token Storage

⚠️ IMPORTANT: Many examples in this guide use localStorage for simplicity in demonstrating concepts, but this has significant security implications:

Risks:

  • Vulnerable to XSS (Cross-Site Scripting) attacks
  • Accessible to all JavaScript code on the page, including third-party scripts
  • Not automatically cleared on browser close
  • No built-in protection against CSRF attacks

Recommended alternatives for production:

  1. HttpOnly Cookies (preferred)

    • Set by backend server
    • Not accessible to JavaScript (immune to XSS token theft)
    • Automatically sent with requests to same domain
    • Can be marked as Secure and SameSite
  2. sessionStorage (slightly better than localStorage)

    • Cleared when tab/window closes
    • Still vulnerable to XSS
    • Better for temporary sessions
  3. In-memory storage with session timeout

    • Store in component state or stores
    • Cleared on page refresh
    • Most secure for highly sensitive apps

Best practice: Use HttpOnly cookies with your backend API for authentication tokens. Reserve localStorage only for non-sensitive application state.

CORS Configuration

Ensure your backend properly configures CORS to only allow your frontend origin:

// Example: Golang + Echo
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
    AllowOrigins: []string{"https://yourdomain.com"}, // Never use "*" in production
    AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
    AllowHeaders: []string{"Authorization", "Content-Type"},
    AllowCredentials: true, // Required for cookies
}))

Data Loading Patterns

Client-Side Load Function

Load functions in +page.ts run in the browser for SPA mode:

// src/routes/dashboard/+page.ts
export const ssr = false;
export const prerender = false;

/** @type {import('./$types').PageLoad} */
export async function load({ fetch, parent, url }) {
	// Access parent layout data
	const parentData = await parent();
	
	// Fetch from backend API
	const API_URL = import.meta.env.VITE_API_URL;
	const response = await fetch(`${API_URL}/api/dashboard`, {
		headers: {
			'Authorization': `Bearer ${parentData.token}`
		}
	});
	
	// Access URL search params
	const filter = url.searchParams.get('filter');
	
	return {
		dashboardData: await response.json(),
		filter
	};
}

Authentication Token Flow

// src/routes/+layout.ts
export const ssr = false;
export const prerender = false;

/** @type {import('./$types').LayoutLoad} */
export async function load({ fetch }) {
	// Get token from localStorage or cookie
	// NOTE: See Security Considerations section for production-ready alternatives
	const token = localStorage.getItem('auth_token');
	
	if (!token) {
		return { user: null, token: null };
	}
	
	// Validate token with backend
	const API_URL = import.meta.env.VITE_API_URL;
	const response = await fetch(`${API_URL}/api/auth/me`, {
		headers: { 'Authorization': `Bearer ${token}` }
	});
	
	if (!response.ok) {
		localStorage.removeItem('auth_token');
		return { user: null, token: null };
	}
	
	return {
		user: await response.json(),
		token
	};
}

Error Handling

import { error, redirect } from '@sveltejs/kit';

export const ssr = false;
export const prerender = false;

/** @type {import('./$types').PageLoad} */
export async function load({ fetch, parent }) {
	const { token } = await parent();
	
	if (!token) {
		throw redirect(303, '/login');
	}
	
	const API_URL = import.meta.env.VITE_API_URL;
	const response = await fetch(`${API_URL}/api/protected-resource`);
	
	if (response.status === 401) {
		throw redirect(303, '/login');
	}
	
	if (response.status === 404) {
		throw error(404, 'Resource not found');
	}
	
	if (!response.ok) {
		throw error(response.status, 'Failed to load resource');
	}
	
	return await response.json();
}

Backend Integration (Golang + Echo)

API Communication

// src/lib/api.ts
const API_BASE = import.meta.env.VITE_API_URL;

export async function apiRequest(endpoint: string, options: RequestInit = {}) {
	// NOTE: See Security Considerations section for production-ready alternatives
	const token = localStorage.getItem('auth_token');
	
	const response = await fetch(`${API_BASE}${endpoint}`, {
		...options,
		headers: {
			'Content-Type': 'application/json',
			...(token && { 'Authorization': `Bearer ${token}` }),
			...options.headers
		}
	});
	
	if (!response.ok) {
		const error = await response.json().catch(() => ({}));
		throw new Error(error.message || 'API request failed');
	}
	
	return response.json();
}

Form Submission

<script>
	import { apiRequest } from '$lib/api';
	import { goto } from '$app/navigation';
	
	let formData = $state({ email: '', password: '' });
	let error = $state(null);
	
	async function handleSubmit() {
		try {
			const result = await apiRequest('/api/auth/login', {
				method: 'POST',
				body: JSON.stringify(formData)
			});
			
			// NOTE: See Security Considerations section for production-ready alternatives
			localStorage.setItem('auth_token', result.token);
			goto('/dashboard');
		} catch (e) {
			error = e.message;
		}
	}
</script>

<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
	<input type="email" bind:value={formData.email} />
	<input type="password" bind:value={formData.password} />
	{#if error}
		<p class="error">{error}</p>
	{/if}
	<button type="submit">Login</button>
</form>

Page Options Reference

Available Options

// +page.ts or +layout.ts
export const ssr = false;        // Disable server-side rendering
export const prerender = false;  // Don't prerender this page
export const csr = true;         // Enable client-side rendering (default)

Important for SPA mode:

  • Always set ssr = false and prerender = false in root layout or individual pages
  • Keep csr = true (it's the default)
  • Both exports are required for pure SPA behavior

When to Prerender in SPA Mode

You can selectively prerender pages even in SPA mode:

// src/routes/about/+page.ts
export const ssr = true;         // Enable for prerendering
export const prerender = true;   // Prerender this page at build

This creates static HTML for /about while keeping other routes as SPA. Useful for:

  • Marketing pages
  • About pages
  • Terms of service
  • Any static content

Navigation

Programmatic Navigation

import { goto } from '$app/navigation';

// Navigate to route
goto('/dashboard');

// Navigate with options
goto('/search', {
	replaceState: true,  // Replace history instead of push
	noScroll: true,      // Don't scroll to top
	keepFocus: true,     // Keep current focus
	state: { from: 'home' }  // Pass state
});

// Navigate with search params
goto('/search?q=sveltekit&page=2');

Link Behavior

<!-- Standard navigation -->
<a href="/dashboard">Dashboard</a>

<!-- Disable client-side routing for this link -->
<a href="/external" data-sveltekit-reload>External Site</a>

<!-- Prefetch on hover -->
<a href="/dashboard" data-sveltekit-preload-data="hover">
	Dashboard
</a>

<!-- Prefetch on viewport -->
<a href="/dashboard" data-sveltekit-preload-data="viewport">
	Dashboard
</a>

State Management

URL State with $page

<script>
	import { page } from '$app/state';
	
	// Access current route info
	$effect(() => {
		console.log(page.url.pathname);  // Current path
		console.log(page.params);        // Route parameters
		console.log(page.data);          // Data from load functions
	});
</script>

<div>
	Current path: {page.url.pathname}
	{#if page.params.id}
		Viewing ID: {page.params.id}
	{/if}
</div>

Navigation State

<script>
	import { navigating } from '$app/state';
	
	// Show loading indicator during navigation
</script>

{#if navigating}
	<div class="loading-bar">Loading...</div>
{/if}

Error Pages

<!-- src/routes/+error.svelte -->
<script>
	import { page } from '$app/state';
</script>

<div class="error-page">
	<h1>{page.status}</h1>
	<p>{page.error?.message}</p>
	<a href="/">Go home</a>
</div>

Environment Variables

// .env
VITE_API_URL=http://localhost:8080
VITE_PUBLIC_KEY=pk_test_...
// Access in code
const apiUrl = import.meta.env.VITE_API_URL;
const publicKey = import.meta.env.VITE_PUBLIC_KEY;

Important: All env vars must be prefixed with VITE_ to be accessible in client-side code.

Common Patterns

Protected Routes

// src/routes/dashboard/+layout.ts
import { redirect } from '@sveltejs/kit';

export const ssr = false;
export const prerender = false;

/** @type {import('./$types').LayoutLoad} */
export async function load({ parent }) {
	const { user } = await parent();
	
	if (!user) {
		throw redirect(303, '/login');
	}
	
	return { user };
}

Data Fetching with Loading States

<script>
	import { onMount } from 'svelte';
	
	let data = $state(null);
	let loading = $state(true);
	let error = $state(null);
	
	onMount(async () => {
		try {
			const response = await fetch('/api/data');
			data = await response.json();
		} catch (e) {
			error = e.message;
		} finally {
			loading = false;
		}
	});
</script>

{#if loading}
	<p>Loading...</p>
{:else if error}
	<p>Error: {error}</p>
{:else if data}
	<div>{data.content}</div>
{/if}

Pagination

<script>
	import { page } from '$app/state';
	import { goto } from '$app/navigation';
	
	let { data } = $props();
	
	function goToPage(pageNum) {
		const url = new URL(window.location.href);
		url.searchParams.set('page', pageNum);
		goto(url.pathname + url.search);
	}
</script>

<div class="items">
	{#each data.items as item}
		<div>{item.title}</div>
	{/each}
</div>

<div class="pagination">
	{#each Array(data.totalPages) as _, i}
		<button onclick={() => goToPage(i + 1)}>
			{i + 1}
		</button>
	{/each}
</div>

Build and Deployment

Build Command

bun run build

This generates static files in the build/ directory (or path specified in adapter config).

Preview Locally

bun run preview

Directory Structure After Build

build/
├── _app/
│   ├── immutable/      # Hashed JS/CSS chunks
│   └── version.json
├── 200.html            # Fallback page (your SPA entry)
└── index.html          # Root page (if prerendered)

Deployment Checklist

  1. adapter-static configured with correct fallback
  2. ssr = false in root layout
  3. ✅ Backend API URLs configured via environment variables
  4. ✅ CORS configured on backend for frontend origin
  5. ✅ Build successful with no errors
  6. ✅ Test locally with bun run preview
  7. ✅ Deploy build/ directory to static host

SvelteKit SPA vs Pure Vite

Choose SvelteKit SPA when you want:

  • File-based routing
  • Load functions for data fetching
  • Built-in error pages
  • Layouts and nested routes
  • Programmatic navigation with goto()
  • URL state management

Choose Pure Vite + Svelte when you want:

  • Manual routing (or no routing)
  • Complete control over bundle structure
  • Minimal framework overhead
  • Custom build configuration

SvelteKit SPA provides routing and data loading conventions while remaining fully client-side.

Troubleshooting

Issue: "Cannot access server-side modules"

Cause: Trying to use +page.server.ts in SPA mode.

Solution: Use +page.ts instead. All load functions run client-side in SPA mode.

Issue: "This page will be rendered on the server"

Cause: ssr = false and prerender = false not set.

Solution: Add both exports to root +layout.ts:

export const ssr = false;
export const prerender = false;

Issue: 404 errors on refresh

Cause: Server doesn't serve fallback page for all routes.

Solution:

  • Verify adapter fallback configuration
  • Configure your static host to serve the fallback page for all routes
  • Test with bun run preview locally first

Issue: API CORS errors

Cause: Backend not configured to allow frontend origin.

Solution: Configure CORS on your Golang/Echo backend:

// In your Echo server
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
    AllowOrigins: []string{"http://localhost:5173", "https://yourdomain.com"},
    AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
    AllowHeaders: []string{"Authorization", "Content-Type"},
}))

Issue: Environment variables not available

Cause: Variables not prefixed with VITE_.

Solution: Rename all client-side env vars to start with VITE_.

Performance Optimization

Prefetching Strategies

SvelteKit provides built-in prefetching for faster navigation:

<!-- Prefetch on hover (most common) -->
<a href="/dashboard" data-sveltekit-preload-data="hover">
	Dashboard
</a>

<!-- Prefetch when link enters viewport -->
<a href="/reports" data-sveltekit-preload-data="viewport">
	Reports
</a>

<!-- Prefetch immediately on page load -->
<a href="/critical" data-sveltekit-preload-data="tap">
	Critical Page
</a>

Code Splitting

Leverage dynamic imports for large components:

<script>
	import { onMount } from 'svelte';
	
	let HeavyComponent;
	
	onMount(async () => {
		// Load component only when needed
		const module = await import('$lib/components/HeavyChart.svelte');
		HeavyComponent = module.default;
	});
</script>

{#if HeavyComponent}
	<svelte:component this={HeavyComponent} />
{/if}

Selective Prerendering

Prerender static pages for instant loading:

// src/routes/about/+page.ts
export const prerender = true;
export const ssr = true; // Enable for build-time rendering

Good candidates for prerendering:

  • Marketing pages
  • About/Terms/Privacy pages
  • Documentation
  • Blog posts (if content is static)

Request Deduplication

Prevent duplicate API calls in load functions:

// src/lib/cache.ts
const cache = new Map<string, any>();
const pending = new Map<string, Promise<any>>();

export async function cachedFetch(url: string, options: RequestInit = {}) {
	const key = `${url}:${JSON.stringify(options)}`;
	
	// Return cached result
	if (cache.has(key)) {
		return cache.get(key);
	}
	
	// Return pending request
	if (pending.has(key)) {
		return pending.get(key);
	}
	
	// Make new request
	const promise = fetch(url, options).then(r => r.json());
	pending.set(key, promise);
	
	try {
		const result = await promise;
		cache.set(key, result);
		return result;
	} finally {
		pending.delete(key);
	}
}

Loading State Optimization

Show instant feedback during navigation:

<script>
	import { navigating } from '$app/state';
</script>

{#if navigating}
	<div class="loading-bar" />
{/if}

<style>
	.loading-bar {
		position: fixed;
		top: 0;
		left: 0;
		right: 0;
		height: 3px;
		background: linear-gradient(90deg, #4f46e5, #06b6d4);
		animation: slide 1s ease-in-out infinite;
	}
	
	@keyframes slide {
		0% { transform: translateX(-100%); }
		100% { transform: translateX(100%); }
	}
</style>

Bundle Optimization Tips

  1. Tree-shake unused code - Import only what you need
  2. Use lightweight alternatives - Consider bundle size of dependencies
  3. Lazy load routes - SvelteKit does this automatically
  4. Optimize images - Use modern formats (WebP, AVIF)
  5. Enable compression - Configure your hosting for gzip/brotli

Best Practices

  1. Disable SSR and prerender early - Set both ssr = false and prerender = false in root +layout.ts immediately
  2. Use load functions - Centralize data fetching in +page.ts load functions with proper TypeScript types
  3. Handle errors gracefully - Use error boundaries and proper error states
  4. Protect routes - Implement authentication checks in layout load functions
  5. Use environment variables - Never hardcode API URLs or keys
  6. Test locally - Always test with bun run preview before deploying
  7. Configure CORS properly - Ensure backend allows frontend origin
  8. Consider prerendering - Prerender static pages for better initial load
  9. Use absolute API URLs - Avoid relative paths when calling backend
  10. Handle loading states - Show feedback during data fetching

Related Documentation

For advanced patterns and additional context:

  • See references/routing-patterns.md for complex routing scenarios
  • See references/backend-integration.md for detailed backend integration patterns