Reference
API & config
Every public export by entry point, the import attributes, and every option of the ogygia() Vite plugin.
On this page
- ogygia
- Import attributes
- Pre-hydration scripts
- ogygia/vite
- visible
- router
- barrels
- presets
- prefetch: warm a deferred hole before it swaps
- maxAge: caching a deferred hole
- importKeys
- rateLimit
- sessionCookie
- ttl
- maxPageAge
- OGYGIA_SECRET
- ogygia/server
- ogygia/markup
- ogygia/app
- @ogygia/content
- @ogygia/content/vite
- ogygia/events · ogygia/insights
- Plugin packages
This page lists everything ogygia exposes, grouped by import path, along with every option of the ogygia() Vite plugin. Where a symbol has its own page, the entry links to it.
ogygia
This is the runtime surface. You can import the namespace (import * as ogygia from 'ogygia') or named exports.
| Export | Kind | What it is |
|---|---|---|
Boundary | component | Error boundary for island subtrees. |
Region | component | Renders a held region: <Region of={x} />. |
region | function | Mints a held region: region(Component, props). |
isRegion | function | Type guard for a held region value. |
transport | hook value | The Kit transport entry that lets a held region cross the wire. |
wire | macro | Declare the codec of a transportable class with static wire = import.meta.og.wire({ encode, decode }). It is a compile-time macro, not an importable symbol. See shared state. |
script | function | An inline <script> string: a self-contained function (run), or JSON / JSON-LD / an import map / speculation rules, with typed attributes. See Pre-hydration scripts. |
preload | function | Warm the HTML of a deferred/live region now, before its binder wakes: preload(region(…)). See held regions. |
preference | function | A persisted, site-wide client choice that flips a CSS state with no shipped JS beyond one pre-paint script: the theme / code-language / tabs primitive. |
createContext | function | Typed sugar over a string context key: createContext('key'), callable for a <Provide> entry, .get() to read. See state & context. |
Provide | component | Provide context values to islands below it across the DOM: <Provide values={[ctx(v)]}>. |
setContext | function | Drop-in for the setContext of Svelte, which also bridges string-keyed values to child islands (the flat page root). |
hydratedBy | function | Which schedule woke this hydration root (for example 'interaction'). Call it during setup, like getContext. |
Types: RegionValue, AwaitableRegion, InlineRegion, DualRegion, DeferredRegion, TransportCodec.
Import attributes
These are written on a component import rather than called:
| Attribute | Makes | Values |
|---|---|---|
with { wake } | the schedule: when an island’s JS runs (or when a live region revalidates) | 'load' · 'idle' · 'visible' · 'interaction' · '(media)' · 'none' |
with { fetch } | the schedule of a render: 'deferred' hole: when its HTML arrives | 'load' (default) · 'idle' · 'visible' · 'interaction' · '(media)' |
with { render } | the delivery mode | 'static' (default) · 'deferred' (server island) · 'live' (revalidates) |
with { region } | a held, server-chosen region marker | 'raw' |
with { keep } | keep the live island across SPA navigation | a name |
with { preset } | a named preset from plugin config | any preset name |
render sets the mode and wake the schedule. A render: 'deferred' hole takes fetch for its schedule instead of wake, and the old pairing of render: 'deferred' with wake fails the build. Regions explains the concept.
Pre-hydration scripts
A csr=false page has no client until its islands wake, so anything that must run before the first paint has to be a plain inline <script> in the HTML. That includes setting the theme (to avoid a dark-mode flash), starting a deferred font, or reading a flag early. Writing that string by hand is awkward, because a literal </script> inside a Svelte component closes the component’s own script, which is why people resort to tricks like String.fromCharCode(60).
script() removes that problem. It takes ONE plain object containing exactly one payload key alongside the tag’s attributes. A bare function followed by arguments is the shortcut for a plain classic script, so script(fn, a, b) means { run: fn, args: [a, b] }. What you get back is a <script>…</script> string, and since it is only a string, you {@html} it wherever you want the tag, usually in <svelte:head>.
<script>
import * as ogygia from 'ogygia';
import fontUrl from './mono.css?url';
</script>
<svelte:head>
<!-- No-flash theme, runs before paint (the shortcut) -->
{@html ogygia.script(() => {
try {
const t = localStorage.getItem('theme');
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
} catch {}
})}
<!-- Deferred font: the hashed URL is closed-over data, so pass it in `args` -->
{@html ogygia.script({
run: (href) => {
addEventListener('load', () => {
const l = document.createElement('link');
l.rel = 'stylesheet';
l.href = href;
document.head.appendChild(l);
}, { once: true });
},
args: [fontUrl],
type: 'module'
})}
<!-- Data: structured data for search engines -->
{@html ogygia.script({ ld: { '@context': 'https://schema.org', '@type': 'Article', headline } })}
</svelte:head>| Payload key | Tag | What it is |
|---|---|---|
run (+ args) | <script> or <script type="module"> | A self-contained function, called with args. |
json | type="application/json" | Data for code on the page (JSON.parse(el.textContent)). |
ld | type="application/ld+json" | Structured data for search engines. |
importmap | type="importmap" | Bare specifiers for the module scripts of the page. |
speculation | type="speculationrules" | The pages the browser may prefetch or prerender. |
Attributes go in the same object. id, nonce and any data-* attribute work on every kind of script. true writes a bare attribute, while false, null and undefined leave it out, and all values are escaped. Because the object can be spread, a preset is a plain object: script({ ...base, run }).
A run script is CLASSIC by default, which means it runs where it stands, before the parser continues, so in <head> it runs before the first paint. type: 'module' gives it its own scope and import(), and it then runs after the document is parsed. A module also accepts async: true (run as soon as it is ready) and imports, the modules it needs by name and URL (imports: { carousel: url }). These are written as static import * as lines before the call, and run receives them as ONE object first, followed by its args: run: ({ carousel }, n) => …. Declare a module there and never import() it inside run, because a bundler rewrites code that it compiles (Vite’s SSR transform turns import() into a helper the browser does not have) but cannot touch a string, and script() throws when it detects such a rewrite. The first paint does not wait for a module, so code that must apply before paint should stay a classic script. async and defer have no effect on an inline classic script, so TypeScript rejects them there, and blocking="render" is not offered because it does not hold the paint for an inline module either. TypeScript also rejects args that don’t match the parameters of run, an argument that JSON would alter (a Date, a Map, a function), and a data script that contains code or sets its own type.
The function is inlined through Function.prototype.toString, so it must be self-contained: it can use browser globals only, with no imports and no closed-over variables, because those do not exist at runtime. An arrow function, a function, or a method on the object ({ run() { … } }) all work. Values you would otherwise close over go in args, where they are JSON-serialized and passed to the function as parameters. Every < in an argument or a data payload is escaped, and in the code every </script and every <!-- is escaped (a <!-- followed by <script would make the page swallow everything after the tag), so nothing can break out of the tag.
ogygia/vite
import { ogygia } from 'ogygia/vite';ogygia(options) is the Vite plugin, and it runs before sveltekit(). Every option is optional. The top level of the options configures the region system itself: its defaults (visible), its named presets (presets), and how long its holes’ URLs live (ttl, maxPageAge). The client router is configured alongside them.
ogygia({
visible: { margin: '120px' },
presets: {
demo: { wake: 'visible', margin: '200px' },
frozenLive: { render: 'live', wake: 'load' }
}
})Every feature that lives in its own package (content, flags, postrender, federation, the server router, devtools) is a separate Vite plugin, listed beside ogygia() in the plugins array of your Vite config, where ogygia() finds it: plugins: [ogygia(), flags(), postrender({ ttl: 600 }), sveltekit()]. Each plugin keeps its own settings, so a region preset can never hold content config, or the reverse. See Plugins.
The old regions: { … } wrapper is a build error that names the new form; its keys moved unchanged to the top level.
visible
Sets the default rootMargin for visible islands and server islands, so that they start loading slightly before they enter the viewport.
ogygia({ visible: { margin: '200px' } })There is no preload setting. The page carries each island’s chunk list as data, and the runtime preloads an island’s whole graph when it wakes, after the page has painted (see how the bytes arrive). The old preload key is a build error.
router
The SPA router is on by default for the whole app (see SPA router). This option is the only place router config lives; there is no <Router/> component.
ogygia() // router on, view transitions on, form continuity on
ogygia({ router: { viewTransitions: false } }) // SPA nav, but no view transitions
ogygia({ router: { forms: false } }) // SPA nav, but form fields don't survive navigation
ogygia({ router: false }) // opt out of the SPA router entirelyrouter: false tree-shakes the router out of the generated runtime, so a load-only app never ships it. It also removes form continuity, since there is then no SPA navigation for form fields to survive. A single page can opt out of view transitions with <meta name="ogygia-router" content="plain"> in its head, and the page’s tag takes precedence over the global default.
barrels
This option turns barrel imports into leaf imports at transform time. A barrel is a module that only re-exports other modules (an index.ts, a package’s components.ts, an export * from fan-out). Importing one name from it drags in the whole graph: in dev every module behind it is transformed and served, in a build the chunk graph follows the import graph before tree-shaking, and a package that is not sideEffects: false keeps all of it. With barrels on, import { Button } from '$lib/components' is rewritten to import Button from '…/components/Button.svelte' before Svelte or ogygia see the file, so the graph contains only the leaves the file actually uses.
ogygia({ barrels: true }) // every pure barrel in the project
ogygia({ barrels: { packages: ['@acme/ui'] } }) // dependencies too, by name / glob / regex
ogygia({ barrels: { force: ['$lib/registry'] } }) // an impure barrel: leaves moved, `import 'barrel'` kept
ogygia({ barrels: { keep: [/stores\/index\.ts$/] } }) // never touch theseWhat it does and does not do:
- Project modules need no listing. Every file outside
node_modulesis a candidate, and purity decides. A barrel is pure when every top-level statement is an import, a re-export, a type orexport {}. A module with code of its own next to its re-exports is left alone unless it is listed inforce, in which case its re-exported names move and a bareimport 'barrel'is kept so that its own code still runs. - Dependencies are opt-in through
packages, because Vite pre-bundles bare imports in dev, and a leaf import that skips the optimizer while another importer gets the pre-bundled copy produces two instances of one module. List a package when you know it is safe (for example an ESM component library that is excluded fromoptimizeDeps). - Nested barrels,
export *fan-outs, rename chains andimport … ; export { … }bindings are followed to the real leaf. A name that twoexport *statements disagree on, a name the barrel does not export, and a name behind something that cannot be parsed (JSON, CommonJS, a?rawimport) stay on the barrel import. - Import attributes and
typemodifiers are carried over (with { type: 'json' },import { type T }), and sourcemaps are emitted. - Islands are left untouched. An import that carries region marks (
with { wake },render,preset,region, or the app’simportKeys) is never rewritten, so a marked import means exactly what it meant before. A barrel binding is marked throughasRegionor a mark in the barrel, as before, and an import whose binding feedsimport.meta.og.asRegion(X)is also left alone, because the island is keyed on that import. import * as ns from 'barrel',import 'barrel'and dynamic imports are left as written.
Options: packages, force, keep (matchers: a package name or specifier prefix, a * glob, a regex, or a function of the resolved id), importers: { include, exclude } (which files are rewritten; default project files), followPackages (walk export * into dependencies while mapping a barrel; default true), report (the end-of-build report: true = the eight biggest barrels and a count of the rest, 'all' = every barrel, false = silent), debug (log every rewrite).
The same pass is also exported as a standalone Vite plugin that any app can use, whether or not it uses ogygia:
import { debarrel } from 'ogygia/vite';
export default defineConfig({ plugins: [debarrel({ packages: ['@acme/ui'] }), svelte()] });presets
Presets are named bundles of strategy and options that an import references with preset: 'name'. A preset uses the same grammar as an inline import, namely render (the mode), wake (an island’s schedule) or fetch (a hole’s schedule), plus the tuning options that are not allowed inline (margin, maxAge, …):
ogygia({
presets: {
chart: { render: 'static', wake: 'visible', margin: '200px' },
pricing: { render: 'deferred', fetch: 'load', maxAge: '1h' },
ticker: { render: 'live', wake: 'idle', maxAge: '10m' }
}
})<script>
import Chart from './Chart.svelte' with { preset: 'chart' };
</script>Fields: render ('static' | 'deferred' | 'live'), wake (an island’s schedule), fetch (a hole’s schedule), margin, maxAge, onExpire, revalidate, keep, prefetch.
prefetch: warm a deferred hole before it swaps
A hole’s fetch marks a single moment, when its HTML is fetched and swapped in, and prefetch separates the two. The HTML is warmed into the frame store on the prefetch schedule ('load' | 'idle' | 'visible' | a media query), while the swap still waits for fetch. An on-demand hole (fetch: 'interaction') therefore already has its bytes before the first hover, and the gesture uses the warm frame instead of paying for a round trip to the origin:
ogygia({ presets: { menu: { render: 'deferred', fetch: 'interaction', prefetch: 'idle', maxAge: '1h' } } })<script>
import CountryPanel from './CountryPanel.svelte' with { render: 'deferred', fetch: 'interaction', prefetch: 'idle' };
</script>prefetch can be written inline or in a preset. It applies only to deferred holes, since an island’s JS is preloaded by the runtime when it wakes rather than by this option. It is never 'interaction', because that is the swap and therefore fetch’s job, and it is never the hole’s own fetch schedule, which would add nothing. Without prefetch, an interaction hole keeps its default behaviour, where the first hover fetches and swaps in one step.
maxAge: caching a deferred hole
A render: 'deferred' hole is dynamic by default: its endpoint is served with Cache-Control: no-store, so it renders again on every request (for a per-request clock or a personalized figure, for example). maxAge opts a hole into a browser cache instead, given as seconds (a number) or as a duration string ('30s' | '5m' | '1h'):
ogygia({ presets: { pricing: { render: 'deferred', fetch: 'load', maxAge: '1h' } } })That hole’s endpoint is then served with Cache-Control: private, max-age=3600. The maxAge is signed into the hole’s URL, so a harvested capability cannot be redirected to a longer cache. maxAge: 0, or leaving it out, keeps the hole dynamic. With render: 'live', maxAge instead sets how stale the client lets the region get before revalidating.
A bare number changes units by context
On a deferred hole, maxAge: 60 means 60 seconds, because it becomes an HTTP max-age, which is measured in seconds. On a live / remount lake, maxAge: 60 means 60 milliseconds, because it is client-side staleness and is measured in ms like every JS timer. The same key therefore uses two different clocks. Duration strings ('30s', '5m', '1h') mean the same thing everywhere, so prefer them and the problem goes away.
importKeys
Extra import-attribute keys to treat as ogygia directives, for interoperability with other tooling that reads import attributes.
rateLimit
Protects the signed island endpoint against abuse by limiting requests per window per client.
sessionCookie
The name and options of the session cookie that ogygia uses to scope signed capabilities.
ttl
How long a signed region capability URL stays valid, in seconds (default 3600, clamped to 60–86400). Keep it short, so that a harvested URL expires.
Holes outlive it where they need to. Prerendered and postrendered pages sign their holes for the life of the deploy. On a page whose load marks it as shared-cacheable, for example with setHeaders({ 'cache-control': 'public, s-maxage=604800' }), an anonymous hole is signed for the page’s cache lifetime. A hole on an older cached copy renews itself when it wakes, because the endpoint re-signs a URL it issued (for anonymous holes only) for up to maxPageAge after it expired. A cache-control header that a handle sets after the render is invisible when the URL is signed, so set it from a load.
maxPageAge
The maximum age, in seconds, at which a cached copy of a page can still get its holes (default 8 days). It sets how long after a hole’s URL expired the hole can still renew itself. Beyond that, the hole keeps its fallback and its request is answered with 403 and x-ogygia-deny: renew-window. 0 turns renewal off.
Raise it when your CDN keeps pages for longer than 8 days and the page does not say so in its cache-control (because the TTL is set in the CDN’s own configuration):
ogygia({ maxPageAge: 30 * 24 * 3600 });It is not the same as a longer ttl. A renewed URL still lives only for ttl, and a hole bound to a session never renews. A renewable URL replays the props signed into it, so limit a hole’s props to routing keys (a locale, a page) and read anything sensitive at render time.
OGYGIA_SECRET
The HMAC secret that signs render: 'deferred' capability URLs. Set it in the environment to keep signatures stable across a deployment; otherwise a key is generated per build. Every deployment that serves your pages or your islands endpoint needs the same value at runtime, because a hole signed with one key and checked against another is refused (x-ogygia-deny: foreign-key).
A hole’s URL signs the component it renders, so a page cached across a deploy keeps its holes as long as their components still exist, regardless of a changed preset, wake or build-time OGYGIA_SECRET. A hole whose component was moved, renamed or removed is refused (x-ogygia-deny: unknown-id) and keeps its fallback.
Server islands and render: 'live' need the server handle installed to serve the endpoint.
ogygia/server
import * as ogygia from 'ogygia/server';
export const handle = ogygia.handle();handle(options) is the SvelteKit server handle. It serves the signed region endpoint (per-hole loads and the single-flight navigation batch) and injects client seeds.
| Option | What it does |
|---|---|
endpoint | The region endpoint’s path. Default /__ogygia__. |
transform(html, { kind, csr, event }) | Post-processes every document (kind: 'document') and every region answer (kind: 'region'), and may reshape Svelte-owned markup: ogygia restores it before anything hydrates. See Server transforms. |
ogygia/markup
import { scanRegions, liftRegions, restoreRegions, transformMarkup, restore } from 'ogygia/markup';This entry point has no dependency on Kit and no imports, so it runs in the non-SvelteKit half of a monorepo or in a test.
| Export | What it is |
|---|---|
scanRegions(html) | Yields every region in a document (island, lake or hole), with its span and attributes. |
liftRegions(html) · restoreRegions(shell, regions) | Lift the regions out around a foreign SSR pass, and splice them back with the marks the pass stamped. |
transformMarkup(html, transform, { kind, csr, dev }) | The server round trip of handle({ transform }): mark, your transform, settle. |
checkScript(check) | The dev check’s payload (settled.check with dev: true), to put in front of the markup so restore checks each host. |
localizeMarks(block) | Renumbers a block’s marks from 0 for a render cache key; .back(rendered) puts the document’s ids back. |
restore(root) | The browser half, on a parsed document or fragment (jsdom included). |
ogygia/app
Replacements for $app/navigation that are safe to use in islands, because a csr=false page has no Kit client:
goto · invalidate · invalidateAll · preloadData · preloadCode · beforeNavigate · afterNavigate · disableScrollHandling
Use these instead of $app/navigation inside islands. See SPA router.
@ogygia/content
import { content, markdown, json, blocks, glob, mapRaw } from '@ogygia/content';| Export | What it is |
|---|---|
content({ loader, schema, relations }) | Defines a collection. Data is inferred from schema, Meta from the loader, with no annotations. refs / get / groups; wire remotes (list, live) through withRemotes(). See Content collections. |
markdown · json · blocks | Source builders: content({ loader: markdown(import.meta.glob(…)) }). markdown provides meta.headings. |
blocks.resolve(tree, registry) | Server-side helper: turns a block tree that you hold into region nodes, for a no-collection recomposer recipe. See Blocks. |
glob · mapRaw · defineSource | Source primitives to build custom loaders (a CMS, an API). See Content collections. |
parseSchema | Lower-level helper. parseFrontmatter lives on @ogygia/content/markdown. |
An entry is { id, data, meta, body, rel, backlinks }, where data is the schema output, meta is derived from the source (for example { headings }), and body is a held region. Types: Entry, ContentEntry, ContentHandle, Source, Heading, SchemaLike.
The package’s other entry points:
| Entry point | What it is |
|---|---|
@ogygia/content/server | The Kit remotes: withRemotes(collection) for a collection’s list and live remotes (see Data for the wire), and remotes(site) for a site’s (see Remotes). It imports $app/server, so use it from a .remote.ts. |
@ogygia/content/vite | The content() plugin (below), and preprocess() / extensions() for the svelte config. |
@ogygia/content/markdown | The markdown pipeline’s own helpers, such as parseFrontmatter. |
@ogygia/content/docs-shell | The DocsShell component. See Shells & components. |
@ogygia/content/slot | SiteSlot, the component that compiled markdown routes overridable elements through. Compiled code imports it; you do not. |
@ogygia/content/theme.css · /shell.css | The stock theme tokens and the shell’s styles, both opt-in imports. Other token themes are under @ogygia/content/themes/*. See Shells & components. |
@ogygia/content/types | The macro types (import.meta.og.*), referenced from src/ogygia.d.ts. |
@ogygia/content/vite
import { content } from '@ogygia/content/vite';
plugins: [ogygia(), content({ markdown: {} }), sveltekit()]content(options) is the Vite plugin of the @ogygia/content package (pnpm add @ogygia/content), listed beside ogygia(). It brings the content macros (import.meta.og.loader, og.code, og.md, og.regions), and the markdown option turns on the markdown pipeline (mdsvex, Shiki and heading collection) for .svx / .md files. See Content collections.
markdown is the pipeline’s configuration and the defaults for every markdown file. Setting it, even to an empty {}, turns markdown on.
presets are named markdown variants, using the same grammar as the presets of every other subsystem. A loader macro opts a whole collection in with { preset: 'name' }, and that collection’s files then compile through the preset’s config, merged over markdown key by key. Each opted-in file becomes its own module variant, so the same file used by another collection (under another preset, or none) renders independently and nothing conflicts.
content({
markdown: { overrides: true }, // the defaults
presets: { plain: { markdown: { overrides: false } } } // a named variant
})// blog.server.ts — the blog corpus compiles through the `plain` preset
loader: import.meta.og.loader.folder('../content/blog', { preset: 'plain' })presets requires markdown to be set, since that is the base each preset merges over. The name must be a literal string, and an unknown name is a build error that lists the configured names.
watchDirs and hmrDebounceMs tune the dev server’s content reload. When a file under src/content changes, the plugin invalidates the modules that read it and every loaded .remote.ts, then reloads the page. watchDirs adds more folders to watch (resolved against the working directory), for content kept outside src/content. Vite already follows import.meta.glob dependencies, so you rarely need it. hmrDebounceMs is how long, in ms, the plugin waits for more changes before it reloads (default 50).
content({ markdown: {}, watchDirs: ['../shared-content'], hmrDebounceMs: 200 })ogygia/events · ogygia/insights
ogygia/events is the event layer: the typed stream of what the framework did (island wakes, server renders, navigations), plus the bus and sink helpers (add_sink, snapshot, clear). It is compiled in only when a plugin asks for it, as the devtools plugin does. ogygia/insights is the analysis engine shared by the dock, the profiler and the test helpers.
Plugin packages
Each feature below is a separate package that you install and list once beside ogygia() in your Vite config, as described in Plugins.
| Package | Plugin | Imports in app code |
|---|---|---|
@ogygia/content | content({ markdown, presets }) | @ogygia/content, /server, /markdown, /docs-shell, /slot, /theme.css, /shell.css, /types |
@ogygia/flags | flags() | flag, decide from @ogygia/flags; openfeature, ofrep from @ogygia/flags/openfeature |
@ogygia/postrender | postrender({ ttl, default }) | postrender and the store and CDN adapters from @ogygia/postrender |
@ogygia/federation | federation() | federate, mount from @ogygia/federation |
@ogygia/router | router() | routes, page, … from @ogygia/router; api from @ogygia/router/api |
@ogygia/devtools | devtools({ dock, profiler }) | @ogygia/devtools (event sinks), /report, /profiler, /profiler/client, /profiler/storage/* |
@ogygia/testing | none (a test helper) | @ogygia/testing |
@ogygia/cli | none (the command) | npx ogygia …; see CLI |