Skip to content

Start

Install & adopt

Add the package, register the Vite plugin before sveltekit(), wire the server handle, then convert routes one at a time without breaking existing Kit pages.

On this page

ogygia drops into an existing SvelteKit app. First you wire it in: the package, the Vite plugin, the server handle. Then you adopt it one route at a time, with everything else staying a normal Kit page.

Setup

The CLI is the fastest way in. It installs the package, registers the Vite plugin before sveltekit(), and wires the server handle and transport in one command.

npx ogygia init
pnpm dlx ogygia init

Add markdown content collections at the same time with npx ogygia init --markdown. If your project already exports a handle or transport, the CLI merges into them (it sequences your handle and spreads into your transport) instead of overwriting.

Manual setup

Prefer to wire it yourself? Install the package, register the Vite plugin before sveltekit(), and add the server handle.

npm i ogygia
pnpm add ogygia

vite.config.ts

ogygia() must run before sveltekit() (it also sets enforce: 'pre'). In monorepos it adds its package root to Vite’s server.fs.allow so absolute shim / runtime resolves are not blocked outside the app directory. For every option, see the API reference.

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

export default defineConfig({
  plugins: [ogygia(), sveltekit()] // order matters
});

hooks.server.ts

ogygia.handle() serves the signed island endpoint used by render: 'deferred' and render: 'live'. Compose it with sequence() if you already have handles. Override the path with ogygia.handle({ endpoint: '/my-islands' }) if you do not want the default clash-safe emoji route.

import * as ogygia from 'ogygia/server';

export const handle = ogygia.handle();

In production

Server islands and command / form POSTs go through Kit’s CSRF protection, so set ORIGIN on your deployment. Without it, those POSTs are rejected. This is one env var; you set it once.

TypeScript types

ogygia resolves a few build-time virtual modules (virtual:ogygia/*). Your editor and svelte-check need one reference line to see their types. Add a src/ogygia.d.ts:

/// <reference types="ogygia/types" />

That is the whole file. Without it, svelte-check flags the virtual imports as unresolved even though the build works. npx ogygia init writes this file for you.

Transport (held regions over the wire)

Held regions render inline for free. But a held region that crosses the wire, like a search remote or a live content source, needs the transport hook installed once in your universal hooks:

import * as ogygia from 'ogygia';
export const transport = { ...ogygia.transport };

Markdown, Shiki & mdsvex (optional)

For .svx / .md content collections, add the optional peers and turn markdown on under the same ogygia() surface, with no separate plugin:

pnpm add -D mdsvex shiki
import { sveltekit } from '@sveltejs/kit/vite';
import { ogygia } from 'ogygia/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    ogygia({
      content: { markdown: {} } // {} = defaults: Shiki + heading ids + collected headings
    }),
    sveltekit()
  ]
});

extensions / preprocess go in svelte.config.js, not inline on sveltekit(). That is the Kit v2 home both svelte-check and the editor’s Svelte language server read. Passing them inline makes Kit ignore svelte.config.js and hides them from svelte-check:

import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { ogygia } from 'ogygia/vite';

export default {
  extensions: ogygia.extensions(), // includes .svelte, adds .svx / .md
  preprocess: [vitePreprocess(), ...ogygia.preprocess()]
};

mdsvex / shiki stay optional peers. ogygia.preprocess() is synchronous and loads no mdsvex when markdown is off (mdsvex loads lazily on first use, erroring with an install hint if absent), and ogygia.extensions() only adds .svx / .md when it is on. Tune themes and headings under content: { markdown }. See content collections.

Adopt one route at a time

ogygia is incremental. You do not convert the whole app. You turn islands on one route at a time, and everything else stays a normal SvelteKit page.

When to use which mode

A mostly-static route → csr = false + islands (the biggest win). A route that is still a full interactive app → leave it csr = true; you can still drop an island into it. A whole site of static-first pages → all-islands mode. You are never forced to pick one globally.

One route

Opt a single route out of the Kit client and mark its islands. Neighbouring routes are untouched. (What csr = false buys you and why islands still work is covered in the Overview.)

export const csr = false;

The router is global

The SPA router is on by default, app-wide. There is no component to render. On routes still csr = true, Kit’s client owns navigation and ogygia does not intercept clicks, so the two coexist. Opt out entirely with ogygia({ router: false }). See Router.

Islands on a normal Kit page

You do not need csr = false to use an island. Marked imports work on a csr = true Kit page too. The component becomes an island, and the rest of the page hydrates the Kit way.

All-islands apps

At the other end, opt the whole app out of the Kit client. If every route is csr = false, Kit would normally skip the client build entirely. ogygia detects this and runs a standalone islands build so your island chunks still ship.

You do not need a keepalive route. When Kit would skip its client build, ogygia’s Vite plugin injects a hidden, URL-less route for the duration of the build and removes it afterward, so islands keep working with zero placeholder files in your project.

Do / don’t

DoDon’t
Start with your most static, highest-traffic route. That is where dropping the Kit client pays off most.Flip the whole app to csr = false on day one. Convert route by route and measure.
Register ogygia() before sveltekit(). Order matters, and the plugin sets enforce: 'pre'.Add a placeholder csr = true page just to keep an all-islands build alive. It is not needed.
Leave genuinely app-like routes on csr = true; ogygia coexists with them.Forget ORIGIN in production. Server-island and form POSTs need it.