Skip to main content
SvelteKit is the official full-stack framework for Svelte, providing file-based routing, server-side rendering, and more. When using SvelteKit with Mizu, you’ll typically use SvelteKit’s static adapter to generate a static site that Mizu serves.

Why SvelteKit?

SvelteKit is Svelte’s answer to Next.js (React) and Nuxt (Vue). It provides a batteries-included framework with file-based routing, layouts, data loading, and more—all built on top of Svelte’s compiler magic. Key benefits:
  • File-based routing: Routes are automatically created from your file structure
  • Nested layouts: Share UI and logic across routes
  • Code splitting: Automatic code splitting per route
  • Data loading: Load data before rendering pages
  • TypeScript-first: Excellent TypeScript support out of the box
  • Build optimizations: Smart bundling and preloading
  • Developer experience: Hot Module Replacement, error overlays, and more

SvelteKit vs Other Meta-Frameworks

FeatureSvelteKitNext.jsNuxtRemix
FrameworkSvelteReactVueReact
RoutingFile-basedFile-basedFile-basedFile-based
SSRYesYesYesYes
SSGYesYesYesLimited
Bundle Size~30 KB~85 KB~50 KB~80 KB
Learning CurveGentleModerateGentleModerate
Data Loadingload()getServerSidePropsuseAsyncDataloader()
FormsActionsAPI routesAPI routesActions
TypeScriptExcellentExcellentExcellentExcellent
Image OptimizationManualBuilt-inBuilt-inManual

SvelteKit with Mizu vs Standalone SvelteKit

AspectSvelteKit + MizuStandalone SvelteKit
BackendGo (Mizu)Node.js (adapter)
API RoutesGo handlersSvelteKit endpoints
DatabaseGo libs (pgx, sqlc)Node libs (Prisma, Drizzle)
DeploymentSingle binaryAdapter-specific
RenderingStatic (SPA mode)SSR + SSG
Build Outputbuild/ → embeddedDepends on adapter
Best ForGo backendsFull Node.js stack

Quick Start

Create a new SvelteKit project:
mizu new ./my-sveltekit-app --template frontend/sveltekit
cd my-sveltekit-app
make dev
Visit http://localhost:3000 to see your app!

Architecture

Development Mode

┌─────────────────────────────────────────────────────────────┐
│                         Browser                              │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  SvelteKit App (HMR, file-based routing)               │  │
│  │  ┌─────────┐  ┌─────────┐  ┌──────────┐               │  │
│  │  │ Router  │→ │ +page   │→ │ API Call │               │  │
│  │  └─────────┘  └─────────┘  └──────────┘               │  │
│  └───────────────────────────────────────────────────────┘  │
│         ↑ HMR WebSocket           ↓ /api/*                  │
└─────────┼──────────────────────────┼────────────────────────┘
          │                          │
┌─────────┼──────────────────────────┼────────────────────────┐
│  Mizu Server (:3000)               │                         │
│         │                          ↓                         │
│    ┌────┴─────────┐        ┌────────────┐                   │
│    │   Proxy to   │        │ API Routes │                   │
│    │ Vite (:5173) │        │ (Go)       │                   │
│    └──────────────┘        └────────────┘                   │
│         ↑                                                    │
└─────────┼────────────────────────────────────────────────────┘

┌─────────┼────────────────────────────────────────────────────┐
│  SvelteKit Dev Server (:5173)                                │
│    ┌──────────────┐  ┌─────────────┐  ┌──────────────┐     │
│    │ Svelte       │  │ Vite        │  │ File watcher │     │
│    │ Compiler     │  │ HMR         │  │ (routes)     │     │
│    └──────────────┘  └─────────────┘  └──────────────┘     │
└──────────────────────────────────────────────────────────────┘

Production Mode

┌─────────────────────────────────────────────────────────────┐
│                         Browser                              │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  SvelteKit App (static, prerendered)                   │  │
│  │  ┌─────────┐  ┌─────────┐  ┌──────────┐               │  │
│  │  │ Router  │→ │ Pages   │→ │ API Call │               │  │
│  │  └─────────┘  └─────────┘  └──────────┘               │  │
│  └───────────────────────────────────────────────────────┘  │
│         ↑ HTML + assets           ↓ /api/*                  │
└─────────┼──────────────────────────┼────────────────────────┘
          │                          │
┌─────────┼──────────────────────────┼────────────────────────┐
│  Mizu Server (single binary)       │                         │
│         │                          ↓                         │
│    ┌────┴─────────┐        ┌────────────┐                   │
│    │ Embedded FS  │        │ API Routes │                   │
│    │ (build/...)  │        │ (Go)       │                   │
│    └──────────────┘        └────────────┘                   │
│  ←────────────────────────────────────────────────────────  │
│  Static files served from Go binary (//go:embed all:build)  │
└──────────────────────────────────────────────────────────────┘

Why SvelteKit with Mizu?

SvelteKit can run as a full-stack framework with its own server, but with Mizu you use it as a static site generator: SvelteKit provides:
  • File-based routing (src/routes/+page.svelte)
  • Layouts and nested routes
  • Data loading (+page.ts)
  • Static site generation
  • Code splitting per route
  • Built-in transitions
Mizu provides:
  • Go-based API backend
  • Database access with Go libraries
  • Type-safe business logic
  • Authentication and authorization
  • Easy deployment (single binary)
  • Performance (Go runtime)

Project Structure

my-sveltekit-app/
├── cmd/server/
│   └── main.go
├── app/server/
│   ├── app.go
│   ├── config.go
│   └── routes.go              # API routes
├── frontend/                    # SvelteKit app
│   ├── src/
│   │   ├── routes/            # File-based routing
│   │   │   ├── +layout.svelte # Root layout
│   │   │   ├── +page.svelte   # / route
│   │   │   ├── about/
│   │   │   │   └── +page.svelte # /about route
│   │   │   └── blog/
│   │   │       ├── +page.svelte # /blog route
│   │   │       ├── +page.ts     # /blog data loading
│   │   │       └── [slug]/
│   │   │           ├── +page.svelte # /blog/:slug
│   │   │           └── +page.ts     # Data loading
│   │   ├── lib/               # Shared code
│   │   │   ├── components/
│   │   │   ├── stores/
│   │   │   └── utils/
│   │   └── app.html           # HTML template
│   ├── static/                # Public files
│   ├── svelte.config.js
│   ├── vite.config.ts
│   └── package.json
├── build/                     # Built output (static HTML/CSS/JS)
└── Makefile

SvelteKit Configuration

frontend/svelte.config.js

import adapter from '@sveltejs/adapter-static'
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'

export default {
  preprocess: vitePreprocess(),

  kit: {
    adapter: adapter({
      pages: '../build',
      assets: '../build',
      fallback: 'index.html',  // SPA mode (client-side routing)
      precompress: false,
      strict: true
    }),

    alias: {
      $components: 'src/lib/components',
      $stores: 'src/lib/stores',
      $utils: 'src/lib/utils'
    }
  }
}
Key settings:
  • adapter-static: Generates static HTML/CSS/JS
  • pages/assets: Output directory (build instead of dist)
  • fallback: 'index.html': SPA mode for client-side routing
  • alias: Custom path aliases (in addition to default $lib)

frontend/vite.config.ts

import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [sveltekit()],

  server: {
    port: 5173,
    strictPort: true,
    hmr: {
      clientPort: 3000,  // Mizu's port for HMR
    },
  },
})

Backend Configuration

app/server/app.go

package server

import (
    "embed"
    "io/fs"

    "github.com/go-mizu/mizu"
    "github.com/go-mizu/mizu/frontend"
)

//go:embed all:../../build
var buildFS embed.FS

func New(cfg *Config) *mizu.App {
    app := mizu.New()

    // API routes
    setupRoutes(app)

    // Frontend (note: 'build' instead of 'dist')
    build, _ := fs.Sub(buildFS, "build")
    app.Use(frontend.WithOptions(frontend.Options{
        Mode:        frontend.ModeAuto,
        FS:          build,
        Root:        "./build",  // SvelteKit output dir
        DevServer:   "http://localhost:" + cfg.DevPort,
        IgnorePaths: []string{"/api"},
    }))

    return app
}
Important: SvelteKit outputs to build/ by default, not dist/.

API Routes Example

// app/server/routes.go
package server

import "github.com/go-mizu/mizu"

type Post struct {
    ID      int    `json:"id"`
    Title   string `json:"title"`
    Slug    string `json:"slug"`
    Content string `json:"content"`
}

func setupRoutes(app *mizu.App) {
    // GET /api/posts
    app.GET("/api/posts", func(c *mizu.Ctx) error {
        posts := []Post{
            {ID: 1, Title: "First Post", Slug: "first-post", Content: "Hello world"},
            {ID: 2, Title: "Second Post", Slug: "second-post", Content: "More content"},
        }
        return c.JSON(posts)
    })

    // GET /api/posts/:slug
    app.GET("/api/posts/:slug", func(c *mizu.Ctx) error {
        slug := c.Param("slug")
        post := Post{
            ID:      1,
            Title:   "First Post",
            Slug:    slug,
            Content: "Hello world",
        }
        return c.JSON(post)
    })
}

File-Based Routing

SvelteKit uses file-based routing where the structure of your src/routes directory defines your app’s routes.

Basic Routes

src/routes/
├── +page.svelte           → /
├── about/
│   └── +page.svelte       → /about
├── contact/
│   └── +page.svelte       → /contact
└── pricing/
    └── +page.svelte       → /pricing

Dynamic Routes

Use [param] for dynamic segments:
src/routes/
├── blog/
│   ├── +page.svelte       → /blog
│   └── [slug]/
│       └── +page.svelte   → /blog/:slug
└── users/
    └── [id]/
        └── +page.svelte   → /users/:id
Access params in your page:
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
  import { page } from '$app/stores'

  $: slug = $page.params.slug
</script>

<h1>Blog Post: {slug}</h1>

Optional Parameters

Use [[param]] for optional parameters:
src/routes/
└── archive/
    └── [[year]]/
        └── +page.svelte   → /archive or /archive/2024
<!-- src/routes/archive/[[year]]/+page.svelte -->
<script lang="ts">
  import { page } from '$app/stores'

  $: year = $page.params.year || new Date().getFullYear()
</script>

<h1>Archive for {year}</h1>

Rest Parameters

Use [...rest] to match multiple segments:
src/routes/
└── docs/
    └── [...path]/
        └── +page.svelte   → /docs/a, /docs/a/b, /docs/a/b/c
<!-- src/routes/docs/[...path]/+page.svelte -->
<script lang="ts">
  import { page } from '$app/stores'

  $: path = $page.params.path  // "a/b/c"
  $: segments = path.split('/')
</script>

<h1>Docs: {path}</h1>

Route Groups

Group routes without affecting the URL with (group):
src/routes/
├── (marketing)/
│   ├── +layout.svelte     # Shared layout for marketing pages
│   ├── about/
│   │   └── +page.svelte   → /about
│   └── pricing/
│       └── +page.svelte   → /pricing
└── (app)/
    ├── +layout.svelte     # Shared layout for app pages
    ├── dashboard/
    │   └── +page.svelte   → /dashboard
    └── settings/
        └── +page.svelte   → /settings

Layouts

Layouts wrap pages and can be nested. They persist across route changes.

Root Layout

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import './styles.css'
</script>

<nav>
  <a href="/">Home</a>
  <a href="/about">About</a>
  <a href="/blog">Blog</a>
</nav>

<main>
  <slot />  <!-- Page content renders here -->
</main>

<footer>
  <p>&copy; 2024 My App</p>
</footer>

<style>
  nav {
    display: flex;
    gap: 1rem;
    padding: 1rem;
    background: #f5f5f5;
  }

  main {
    padding: 2rem;
    min-height: calc(100vh - 200px);
  }
</style>

Nested Layouts

src/routes/
├── +layout.svelte         # Root layout
├── +page.svelte
└── blog/
    ├── +layout.svelte     # Blog layout (inherits root)
    ├── +page.svelte
    └── [slug]/
        └── +page.svelte
<!-- src/routes/blog/+layout.svelte -->
<aside>
  <h2>Categories</h2>
  <ul>
    <li><a href="/blog?category=tech">Tech</a></li>
    <li><a href="/blog?category=design">Design</a></li>
  </ul>
</aside>

<div class="blog-content">
  <slot />  <!-- Blog pages render here -->
</div>

<style>
  aside {
    float: left;
    width: 200px;
  }

  .blog-content {
    margin-left: 220px;
  }
</style>

Layout Data

Load data in layouts:
// src/routes/blog/+layout.ts
export async function load({ fetch }) {
  const res = await fetch('/api/categories')
  const categories = await res.json()

  return {
    categories
  }
}
<!-- src/routes/blog/+layout.svelte -->
<script lang="ts">
  export let data
</script>

<aside>
  <h2>Categories</h2>
  <ul>
    {#each data.categories as category}
      <li><a href="/blog?category={category.slug}">{category.name}</a></li>
    {/each}
  </ul>
</aside>

<slot />

Resetting Layouts

Break out of parent layouts with [email protected]:
src/routes/
├── +layout.svelte         # Root layout
├── admin/
│   ├── +layout.svelte     # Admin layout
│   └── login/
│       └── [email protected]  # Skips admin layout, uses root
Or skip all layouts with +layout@[id].svelte:
<!-- src/routes/print/[email protected] -->
<!-- This page has no layout, just the page content -->
<slot />

Data Loading

SvelteKit’s load functions fetch data before rendering pages.

Basic Load Function

// src/routes/blog/+page.ts
export async function load({ fetch }) {
  const response = await fetch('/api/posts')
  const posts = await response.json()

  return {
    posts
  }
}
<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
  export let data
</script>

<h1>Blog</h1>

<ul>
  {#each data.posts as post}
    <li>
      <a href="/blog/{post.slug}">{post.title}</a>
    </li>
  {/each}
</ul>

Load Function with Params

// src/routes/blog/[slug]/+page.ts
export async function load({ params, fetch }) {
  const response = await fetch(`/api/posts/${params.slug}`)

  if (!response.ok) {
    return {
      status: 404,
      error: new Error('Post not found')
    }
  }

  const post = await response.json()

  return {
    post
  }
}

Parallel Loading

Load multiple resources in parallel:
// src/routes/dashboard/+page.ts
export async function load({ fetch }) {
  const [usersRes, statsRes, postsRes] = await Promise.all([
    fetch('/api/users'),
    fetch('/api/stats'),
    fetch('/api/posts')
  ])

  const [users, stats, posts] = await Promise.all([
    usersRes.json(),
    statsRes.json(),
    postsRes.json()
  ])

  return {
    users,
    stats,
    posts
  }
}

Dependent Loads

When data depends on other data:
// src/routes/user/[id]/+page.ts
export async function load({ params, fetch }) {
  // First, fetch the user
  const userRes = await fetch(`/api/users/${params.id}`)
  const user = await userRes.json()

  // Then, fetch posts by this user
  const postsRes = await fetch(`/api/posts?authorId=${user.id}`)
  const posts = await postsRes.json()

  return {
    user,
    posts
  }
}

Streaming with Promises

Return promises to stream data:
// src/routes/dashboard/+page.ts
export async function load({ fetch }) {
  return {
    // This loads immediately
    user: await fetch('/api/user').then(r => r.json()),

    // These stream in when ready
    stats: fetch('/api/stats').then(r => r.json()),
    posts: fetch('/api/posts').then(r => r.json())
  }
}
<!-- src/routes/dashboard/+page.svelte -->
<script lang="ts">
  export let data
</script>

<h1>Welcome {data.user.name}</h1>

{#await data.stats}
  <p>Loading stats...</p>
{:then stats}
  <div>
    <p>Posts: {stats.postCount}</p>
    <p>Views: {stats.viewCount}</p>
  </div>
{/await}

{#await data.posts}
  <p>Loading posts...</p>
{:then posts}
  <ul>
    {#each posts as post}
      <li>{post.title}</li>
    {/each}
  </ul>
{/await}
SvelteKit provides programmatic navigation and lifecycle hooks.

Using goto

<script lang="ts">
  import { goto } from '$app/navigation'

  function navigateToAbout() {
    goto('/about')
  }

  function navigateWithOptions() {
    goto('/blog', {
      replaceState: true,      // Replace history instead of push
      noScroll: true,          // Don't scroll to top
      keepFocus: true,         // Keep focus on current element
      invalidateAll: true      // Reload all data
    })
  }
</script>

<button on:click={navigateToAbout}>Go to About</button>
<script lang="ts">
  import { beforeNavigate, afterNavigate } from '$app/navigation'
  import { page } from '$app/stores'

  let hasUnsavedChanges = false

  beforeNavigate(({ from, to, cancel }) => {
    if (hasUnsavedChanges) {
      if (!confirm('You have unsaved changes. Leave anyway?')) {
        cancel()
      }
    }
  })

  afterNavigate(({ from, to, type }) => {
    console.log('Navigated from', from?.url)
    console.log('Navigated to', to?.url)
    console.log('Navigation type', type) // 'link', 'popstate', 'goto'
  })
</script>

Prefetching

SvelteKit can prefetch data before navigation:
<!-- Prefetch on hover (default) -->
<a href="/blog" data-sveltekit-preload-data="hover">Blog</a>

<!-- Prefetch on tap (mobile-friendly) -->
<a href="/blog" data-sveltekit-preload-data="tap">Blog</a>

<!-- Disable prefetching -->
<a href="/blog" data-sveltekit-preload-data="off">Blog</a>

<!-- Prefetch immediately -->
<a href="/blog" data-sveltekit-preload-data>Blog</a>

Disabling Client-Side Routing

For external links or special cases:
<!-- Force full page reload -->
<a href="/admin" data-sveltekit-reload>Admin</a>

<!-- Disable client-side routing for this link -->
<a href="/legacy" data-sveltekit-noscroll>Legacy Page</a>

Page Options

Configure page behavior with +page.ts:
// src/routes/blog/+page.ts
export const prerender = true  // Prerender at build time
export const ssr = false       // Disable server-side rendering
export const csr = true        // Enable client-side rendering
export const trailingSlash = 'always'  // or 'never' or 'ignore'

export async function load({ fetch }) {
  // ...
}
Options:
  • prerender: Generate static HTML at build time
  • ssr: Server-side rendering (not applicable with static adapter)
  • csr: Client-side rendering
  • trailingSlash: URL trailing slash handling

SvelteKit Stores

SvelteKit provides built-in stores for navigation state.

$app/stores

<script lang="ts">
  import { page, navigating, updated } from '$app/stores'

  // Page store - current page state
  $: console.log('Current route:', $page.url.pathname)
  $: console.log('Route params:', $page.params)
  $: console.log('Query params:', $page.url.searchParams.get('q'))
  $: console.log('Page data:', $page.data)

  // Navigating store - navigation in progress
  $: if ($navigating) {
    console.log('Navigating from', $navigating.from?.url)
    console.log('Navigating to', $navigating.to?.url)
  }

  // Updated store - new version deployed
  $: if ($updated) {
    // New version available, reload page
    location.reload()
  }
</script>

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

<p>Current page: {$page.url.pathname}</p>

Forms and Actions

In static mode, SvelteKit form actions don’t work. Use client-side forms instead.

Client-Side Form Handling

<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
  let name = ''
  let email = ''
  let message = ''
  let submitting = false
  let success = false
  let error: string | null = null

  async function handleSubmit() {
    submitting = true
    success = false
    error = null

    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name, email, message })
      })

      if (!response.ok) throw new Error('Failed to send message')

      success = true
      name = ''
      email = ''
      message = ''
    } catch (err) {
      error = err instanceof Error ? err.message : 'Unknown error'
    } finally {
      submitting = false
    }
  }
</script>

<form on:submit|preventDefault={handleSubmit}>
  <input bind:value={name} placeholder="Name" required />
  <input bind:value={email} type="email" placeholder="Email" required />
  <textarea bind:value={message} placeholder="Message" required />

  <button type="submit" disabled={submitting}>
    {submitting ? 'Sending...' : 'Send Message'}
  </button>
</form>

{#if success}
  <p class="success">Message sent successfully!</p>
{/if}

{#if error}
  <p class="error">{error}</p>
{/if}

Error Handling

Handle errors with +error.svelte pages.

Error Page

<!-- src/routes/+error.svelte -->
<script lang="ts">
  import { page } from '$app/stores'
</script>

<div class="error-page">
  <h1>{$page.status}: {$page.error?.message}</h1>

  {#if $page.status === 404}
    <p>Page not found</p>
    <a href="/">Go home</a>
  {:else}
    <p>Something went wrong</p>
    <button on:click={() => location.reload()}>Reload</button>
  {/if}
</div>

<style>
  .error-page {
    text-align: center;
    padding: 4rem 2rem;
  }

  h1 {
    color: #e63900;
  }
</style>

Throwing Errors in Load Functions

// src/routes/blog/[slug]/+page.ts
import { error } from '@sveltejs/kit'

export async function load({ params, fetch }) {
  const response = await fetch(`/api/posts/${params.slug}`)

  if (!response.ok) {
    throw error(404, {
      message: 'Post not found'
    })
  }

  const post = await response.json()
  return { post }
}

Environment Variables

Access environment variables safely.

Public Variables

// src/routes/+page.ts
import { PUBLIC_API_URL } from '$env/static/public'

export async function load({ fetch }) {
  const response = await fetch(`${PUBLIC_API_URL}/posts`)
  const posts = await response.json()
  return { posts }
}
# .env
PUBLIC_API_URL=https://api.example.com
Important: PUBLIC_* variables are embedded in client-side code.

Private Variables (Build-time only)

// src/routes/+page.ts (or server-side code)
import { SECRET_KEY } from '$env/static/private'

// Only available during build, not in client bundle

Complete Real-World Example: Blog

Let’s build a complete blog with routing, layouts, and data loading.

Backend (Go)

// app/server/routes.go
package server

import (
	"time"

	"github.com/go-mizu/mizu"
)

type Post struct {
	ID        int       `json:"id"`
	Title     string    `json:"title"`
	Slug      string    `json:"slug"`
	Excerpt   string    `json:"excerpt"`
	Content   string    `json:"content"`
	Author    string    `json:"author"`
	Tags      []string  `json:"tags"`
	CreatedAt time.Time `json:"createdAt"`
}

var posts = []Post{
	{
		ID:        1,
		Title:     "Getting Started with SvelteKit",
		Slug:      "getting-started",
		Excerpt:   "Learn the basics of SvelteKit",
		Content:   "SvelteKit is amazing...",
		Author:    "Alice",
		Tags:      []string{"sveltekit", "tutorial"},
		CreatedAt: time.Now().AddDate(0, 0, -5),
	},
	{
		ID:        2,
		Title:     "Building with Mizu",
		Slug:      "building-with-mizu",
		Excerpt:   "Create backends with Go and Mizu",
		Content:   "Mizu makes it easy...",
		Author:    "Bob",
		Tags:      []string{"mizu", "go"},
		CreatedAt: time.Now().AddDate(0, 0, -2),
	},
}

func setupRoutes(app *mizu.App) {
	// GET /api/posts
	app.GET("/api/posts", func(c *mizu.Ctx) error {
		return c.JSON(posts)
	})

	// GET /api/posts/:slug
	app.GET("/api/posts/:slug", func(c *mizu.Ctx) error {
		slug := c.Param("slug")
		for _, post := range posts {
			if post.Slug == slug {
				return c.JSON(post)
			}
		}
		return c.Status(404).JSON(map[string]string{"error": "Post not found"})
	})
}

Frontend Structure

frontend/src/routes/
├── +layout.svelte
├── +page.svelte
├── about/
│   └── +page.svelte
└── blog/
    ├── +layout.svelte
    ├── +page.svelte
    ├── +page.ts
    └── [slug]/
        ├── +page.svelte
        └── +page.ts

Root Layout

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import './global.css'
</script>

<div class="app">
  <header>
    <nav>
      <a href="/" class="logo">MyBlog</a>
      <div class="links">
        <a href="/blog">Blog</a>
        <a href="/about">About</a>
      </div>
    </nav>
  </header>

  <main>
    <slot />
  </main>

  <footer>
    <p>&copy; 2024 MyBlog. Built with SvelteKit and Mizu.</p>
  </footer>
</div>

<style>
  .app {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
  }

  header {
    background: #ff3e00;
    color: white;
  }

  nav {
    max-width: 1200px;
    margin: 0 auto;
    padding: 1rem 2rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .logo {
    font-size: 1.5rem;
    font-weight: bold;
    color: white;
    text-decoration: none;
  }

  .links {
    display: flex;
    gap: 2rem;
  }

  .links a {
    color: white;
    text-decoration: none;
  }

  .links a:hover {
    text-decoration: underline;
  }

  main {
    flex: 1;
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
    width: 100%;
  }

  footer {
    background: #f5f5f5;
    text-align: center;
    padding: 2rem;
    color: #666;
  }
</style>

Blog List Page

// src/routes/blog/+page.ts
export async function load({ fetch }) {
  const response = await fetch('/api/posts')
  const posts = await response.json()

  return {
    posts
  }
}
<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
  export let data
</script>

<svelte:head>
  <title>Blog - MyBlog</title>
  <meta name="description" content="Read our latest blog posts" />
</svelte:head>

<h1>Blog</h1>

<div class="posts">
  {#each data.posts as post}
    <article class="post-card">
      <h2>
        <a href="/blog/{post.slug}">{post.title}</a>
      </h2>
      <p class="meta">
        By {post.author}{new Date(post.createdAt).toLocaleDateString()}
      </p>
      <p class="excerpt">{post.excerpt}</p>
      <div class="tags">
        {#each post.tags as tag}
          <span class="tag">{tag}</span>
        {/each}
      </div>
    </article>
  {/each}
</div>

<style>
  h1 {
    margin-bottom: 2rem;
  }

  .posts {
    display: grid;
    gap: 2rem;
  }

  .post-card {
    padding: 1.5rem;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
  }

  .post-card h2 {
    margin: 0 0 0.5rem;
  }

  .post-card a {
    color: #ff3e00;
    text-decoration: none;
  }

  .post-card a:hover {
    text-decoration: underline;
  }

  .meta {
    color: #666;
    font-size: 0.875rem;
    margin-bottom: 1rem;
  }

  .excerpt {
    margin-bottom: 1rem;
  }

  .tags {
    display: flex;
    gap: 0.5rem;
  }

  .tag {
    background: #f5f5f5;
    padding: 0.25rem 0.75rem;
    border-radius: 4px;
    font-size: 0.875rem;
  }
</style>

Blog Post Page

// src/routes/blog/[slug]/+page.ts
import { error } from '@sveltejs/kit'

export async function load({ params, fetch }) {
  const response = await fetch(`/api/posts/${params.slug}`)

  if (!response.ok) {
    throw error(404, {
      message: 'Post not found'
    })
  }

  const post = await response.json()

  return {
    post
  }
}
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
  export let data

  $: post = data.post
</script>

<svelte:head>
  <title>{post.title} - MyBlog</title>
  <meta name="description" content={post.excerpt} />
</svelte:head>

<article class="post">
  <header>
    <h1>{post.title}</h1>
    <p class="meta">
      By {post.author}{new Date(post.createdAt).toLocaleDateString()}
    </p>
    <div class="tags">
      {#each post.tags as tag}
        <span class="tag">{tag}</span>
      {/each}
    </div>
  </header>

  <div class="content">
    {@html post.content}
  </div>

  <footer>
    <a href="/blog">← Back to blog</a>
  </footer>
</article>

<style>
  .post {
    max-width: 800px;
    margin: 0 auto;
  }

  header {
    margin-bottom: 2rem;
    padding-bottom: 2rem;
    border-bottom: 1px solid #e5e7eb;
  }

  h1 {
    margin-bottom: 0.5rem;
  }

  .meta {
    color: #666;
    margin-bottom: 1rem;
  }

  .tags {
    display: flex;
    gap: 0.5rem;
  }

  .tag {
    background: #f5f5f5;
    padding: 0.25rem 0.75rem;
    border-radius: 4px;
    font-size: 0.875rem;
  }

  .content {
    line-height: 1.8;
    margin-bottom: 3rem;
  }

  footer a {
    color: #ff3e00;
    text-decoration: none;
  }

  footer a:hover {
    text-decoration: underline;
  }
</style>

Development Workflow

Start Development

# Terminal 1: SvelteKit dev server
cd frontend
npm run dev

# Terminal 2: Mizu backend
go run cmd/server/main.go
Or use the Makefile:
make dev
The Makefile typically runs both servers concurrently.

Building for Production

make build
This:
  1. Builds SvelteKit (npm run build → creates build/)
  2. Builds Go binary with embedded build/
Run:
MIZU_ENV=production ./bin/server

Troubleshooting

Build Directory Not Found

Problem: Go can’t find the build/ directory. Solution: Make sure you’ve run npm run build in the frontend/ directory first:
cd frontend
npm run build
cd ..
go build -o bin/server cmd/server/main.go

HMR Not Working

Problem: Changes don’t hot-reload. Solution: Verify vite.config.ts:
server: {
  hmr: {
    clientPort: 3000  // Must match Mizu port
  }
}

404 on Page Refresh

Problem: Refreshing on /blog gives 404 in production. Solution: Ensure fallback: 'index.html' is set in svelte.config.js:
adapter: adapter({
  fallback: 'index.html'  // Enable SPA mode
})

Data Not Loading

Problem: load functions don’t fetch data. Solution: Make sure you’re using fetch from the load context:
// ❌ Wrong
export async function load() {
  const res = await fetch('/api/posts')  // Uses global fetch
}

// ✅ Correct
export async function load({ fetch }) {
  const res = await fetch('/api/posts')  // Uses SvelteKit's fetch
}

Route Params Not Working

Problem: $page.params is empty. Solution: Make sure you’re accessing params in a component under the dynamic route:
src/routes/blog/[slug]/+page.svelte  ✅ Has access to $page.params.slug
src/routes/blog/+page.svelte         ❌ No params here

Limitations with Static Adapter

When using the static adapter with Mizu: Works:
  • Client-side routing ✅
  • Data loading from APIs ✅
  • Forms (client-side) ✅
  • Stores and state ✅
  • Layouts and nested routes ✅
  • Dynamic routes with params ✅
  • Prefetching ✅
Doesn’t work:
  • Server-side rendering (SSR) ❌
  • SvelteKit API routes (+server.ts) ❌
  • Server-only load functions (+page.server.ts) ❌
  • Form actions (+page.server.ts) ❌
  • Hooks (hooks.server.ts) ❌
Workarounds:
  • Use Mizu API routes instead of SvelteKit endpoints
  • Use +page.ts (client) instead of +page.server.ts (server)
  • Handle forms client-side with fetch()

When to Choose SvelteKit

Choose SvelteKit + Mizu if:
  • You want file-based routing with Svelte
  • You like nested layouts
  • You want automatic code splitting per route
  • You’re comfortable with Go for your backend
  • You prefer static site generation
Choose vanilla Svelte + Mizu if:
  • You prefer manual routing (simpler)
  • You don’t need nested layouts
  • Smaller learning curve is important
  • You want minimal framework overhead
Choose full SvelteKit (without Mizu) if:
  • You need SSR (server-side rendering)
  • You want Node.js for your backend
  • You need SvelteKit API routes and actions
  • You want to use +page.server.ts files

Next Steps