Create a new site

npm init capri my-capri-site -- -e svelte

This will download and install capri-js/capri/examples/svelte.

You can view a deployed version of the demo on GitHub pages, including the preview SPA.

Entry files

The client entry file is a regular Svelte single page app:

// src/main.tsx

import { ClientApp, Router } from "svelte-pilot";

import router from "./router.js";

new ClientApp({
  target: document.body,
  props: {

In this example we use svelte-pilot as it supports SSR and async data loading. This is what router.tsx looks like:

// src/router.tsx

import { Router } from "svelte-pilot";

import * as About from "./About.svelte";
import * as Home from "./Home.svelte";

const routes = [
    path: "/",
    component: Home,
    path: "/about",
    component: About,

export default new Router({
  base: import.meta.env.BASE_URL,
  mode: import.meta.env.SSR ? "server" : "client",
base option

If you deploy your site to "/" you can omit the base setting.

mode option

If omitted, the mode options defaults to typeof window === 'object' ? 'client' : 'server'. We specify it here, as our E2E tests run in jsdom where window is always defined.

// src/main.server.tsx

import { RenderFunction } from "@capri-js/svelte/server";
import { Router, ServerApp } from "svelte-pilot";

import router from "./router.js";

export async function render(url: string) {
  const matched = await router.handle(url);
  if (!matched) throw new Error(`No matching route: ${url}`);
  const { route, ssrState } = matched;
  const { head, html } = ServerApp.render({ router, route, ssrState });
  return {
    body: html,

Important: When you use the Router's base option, you have to provide an absolute URL with a protocol to router.handle. The actual host does not matter:



you can define interactive islands by naming your components *.island.svelte:

// src/Counter.island.svelte

  export let counter = 0;

  <button on:click={() => counter--}>-</button>
  <button on:click={() => counter++}>+</button>

  div {
    display: flex;
    gap: 0.5em;

Media queries

You can export an options object in the module context to hydrate an island as soon as a media query matches. The following example will hydrate once the viewport width gets below 700px:

<script context="module">
  export const options = {
    media: "(max-width:700px)",

  import { onMount } from "svelte";
  let content = "Resize your browser below 700px to hydrate this island.";
  onMount(() => {
    content = "The island has been hydrated.";


Data fetching

Svelte components can load data by exporting a load function in the module context.

// src/Profile.svelte

<script context="module">
  export async function load(props, route, ssrCtx) {
    // The data must be returned as `ssrState`:
    return {
      ssrState: await fetchUser(),

  async function fetchUser() {
    const res = await fetch("");
    return res.json();

  import { onMount } from "svelte";
  export let ssrState;

  // When running as SPA we have to fetch the data on mount:
  onMount(async () => {
    ssrState = await fetchUser();

<div>Hello {}!</div>
MIT Licensed | Copyright © 2022 | Impressum