ogygia
v0.7.0
Published
Astro-style SSR islands for SvelteKit (no Kit patches)
Readme
No Kit client bootstrap — the shared runtime is a custom element plus the features your app actually uses (a load-only app is ~8 KB min+brotli; the SPA router, lakes, live regions, and persistence are bundled only when used). Mark components with an import attribute; only those get their own JS. Each hydrate island’s module URL is written onto <ogygia-region entry> (Astro-style), so the sticky runtime does not grow with app-wide island count. Not a new framework — it sits on SvelteKit.
Install
pnpm add ogygia// vite.config.ts — ogygia MUST come before sveltekit()
import { sveltekit } from '@sveltejs/kit/vite';
import { ogygia } from 'ogygia/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [ogygia(), sveltekit()]
});// src/routes/+layout.ts
export const csr = false;Dev HMR still works with csr = false — soft updates for CSS and shared modules, full reload for route shells and island entry components. No extra setup; see the docs.
Example
<script>
import Counter from '$lib/Counter.svelte' with { wake: 'load' };
import Chart from '$lib/Chart.svelte' with { wake: 'visible' };
import Greeting from '$lib/Greeting.svelte' with { render: 'deferred', wake: 'load' };
// Counter is a portable island binding — lists / dynamic <Comp /> work too
const Dyn = Counter;
</script>
<Counter start={10} />
<Dyn start={1} />
<Chart />
<Greeting name="world">
{#snippet ogygiaFallback()}<p>loading…</p>{/snippet}
</Greeting>That’s an island (JS on load), the same island used dynamically, a below-the-fold island, and a server island (HTML fetched later). Lakes, presets, the SPA router, remotes, and the rest live in the docs:
Dynamic import() + region attributes
Not supported — ogygia fails the build if it sees
import('./X.svelte', { with: { hydrate: 'load' } }) (same for defer / preset). Vite strips those attributes; runtimes reject unknown keys; islands need a static import … with { … } so SSR can emit a shell.
For a chunk that downloads only after a click, use a host island and plain
await import('./Widget.svelte') (no region attributes) — that mounts a regular component, not a second island. Docs: pesky patterns · playground demo.
Trust boundaries and design constraints: INVARIANTS.md in the monorepo root.
Page data
$page.data — your load data, plus url / params / route / status / form / error — reads inside an island exactly like a Kit page:
<script>
import { page } from '$app/state';
</script>
<p>Bonjour {page.data.user.name} — {page.url.pathname}</p>A load that returns a promise streams into the island: the shell and the island’s {#await} pending branch paint immediately (first paint is never blocked on the slowest promise), and each promise resolves live as it settles. Rejections show {:catch}; custom transport types round-trip. Docs: page data.
Shared state & context
Each island is its own hydration root, so a value handed between islands crosses a serialization boundary. A live class opts in with a wire codec, and every island that receives it shares one live instance — mutate it in one, the others repaint:
<script>
import Count from './Count.svelte' with { wake: 'load' };
import Add from './Add.svelte' with { wake: 'visible' };
import { Cart } from './cart.svelte.js'; // a class with a static `wire` codec
const cart = new Cart();
</script>
<Count {cart} />
<Add {cart} />To reach islands scattered down a subtree without prop drilling, provide it once as context — or adopt cross-island context in an existing csr = false layout by swapping import { setContext } from 'svelte' to from 'ogygia'. Docs: state & context.
Live regions
A region made from a with { region: 'raw' } import is awaitable. Await it on the server (or just
yield it from a query.live — the language awaits it) and it renders to HTML there and travels with
its markup, so the client swaps it in with no fetch:
export const dashboard = query.live(v.string(), async function* (id) {
for await (const stats of feed(id)) yield region(StatCard, { stats });
});<Region of={dashboard(id).current} />Static dual regions (region: 'raw', no wake) morph in place across ticks; interactive ones
(region: 'raw' + wake) keep-alive (new props pushed into the mounted island, local state
intact). LiveView, with Svelte components.
Single-flight navigation
Each load-scheduled server-island hole fetches its own signed HTML on load. On a SPA navigation the router prescans the incoming page and pulls all its holes down one batch request — no fetch-per-hole waterfall. Holes stream back out of order (fast-first) as each settles, and a command that returns a region repaints in a single flight — one round trip that both mutates and repaints.
Content collections
RF-native content collections ship inside ogygia — content() mints Kit prerender / query /
query.live remotes, and get(id) hands back { id, data, headings, body } where body is a
region you render with <Region>. Import
from ogygia/content; configure markdown via ogygia({ content: { markdown } }). mdsvex / shiki
are optional peers.
License
MIT
