Repository navigation
Releases: PuruVJ/ogygia
Release list
ogygia 0.8.5
Changed
- An island inside a live snippet an island renders follows the nested rule: it renders inline and hydrates with that island.
- An island sets
data-hydratedand firesogygia:hydratedonce the live snippets it renders have hydrated too. - A hole nested in an island renders inline as static HTML, like a lake: its component never ships to the browser, and an island inside it wakes on its own.
Fixed
- A hole inside a live snippet an island renders stays deferred: it ships its fallback, loads on its schedule and is never bundled into the island (it never loaded before).
- A live snippet that renders a template-root snippet, or reads a
<script module>import or value, an exported declaration or a$:declaration, no longer throwsReferenceErrorat render. - A lake inside a live snippet an island renders keeps its frozen region, so the island inside it wakes and the lake's HTML is no longer discarded beside other snippet content.
- A live snippet an island renders reads the page's contexts on the server and in the browser, as the island does.
- An island inside a frozen snippet wakes on its own.
- A snippet passed to an island on a Kit-hydrated page no longer causes a hydration mismatch.
- A hole on a Kit-hydrated page keeps its address and its landed HTML through Kit's hydration (a hole inside an island could go empty).
ogygia 0.8.4
Fixed
- ogygia always uses the app's SvelteKit: a second copy resolved from ogygia's own location (a linked ogygia whose peer resolved another Kit) left the devtools' server events, the freeze verdict and the page seed without their request.
- A hole's answer morphed into a custom element with a shadow root keeps the answer's order: a reordered or newly shown child no longer stays in or lands at the wrong place.
- A baked held region on a Kit-hydrated page no longer throws a hydration mismatch that emptied it: the region element is adopted as it stands, like a lake's.
- Islands inside a baked held region on a Kit-hydrated page wake on the runtime instead of being left to Kit, which never hydrates them.
- Custom elements inside a baked held region keep what the server's transform wrote (a closed dropdown no longer loses
hiddenand shifts the page).
ogygia 0.8.3
Added
ogygia/testing:record(actions, { blockNavigation: true })holds back any document load the actions cause, keeps the page and lists each innavigationswith what started it, seen as it began (the link, form or input being handled;approxwhere the browser cannot say).ogygia/testing:compare()andtoMarkdown()also report hydration per island instance, session counts (errors, shifts, slow interactions, blocked navigations, CLS) and vitals past a noise threshold (compare(base, head, { noise })), and take several runs a side ({ test, runs }): load signals by median, session signals only when every head run has them and no base run does.ogygia/testing: eachhydration()island has akeyper instance (Name#nin page order, or theislandKeyattribute on it or its nearest ancestor, across shadow roots) and its wake times (t0,done); session findings carrynames; a session'scounts.clsis a number, andtoMeetBudget({ cls })reads it.ogygia/testing:playwrightDriver(page, { onNavigationHeld })andturnOnDevtools(page, baseURL), the fixture's own pieces, for a test that holds its pages itself.ogygia/testing: every report names the page's testing engine (engine.version,engine.lean); against a site on an older ogygia,record()fills innavigationsfrom the loads it held, and one warning per site is logged for the whole run.ogygia/testing: achangedisland says whether only attributes differ (attributesOnly, with each inattributes) and whether input landed on it while it hydrated (inputDuringHydration);ignoreAttributesleaves chosen attribute changes out, andtoHydrateCleanly({ allowAttributeChanges, allowInput })lets them through.ogygia/testing:page().selfsays what measuring cost the page (script_ms, the testing engine'sbytes), and lean testing (lean: true, theog_testing=leancookie) runs the testing API and nothing else of devtools, so the page loads what a visitor's does.ogygia/testing:page().route(the SvelteKit route id),cpuFunctionsfor more than 15 ofpage().cpu.fns, and a session'sfailedrequests and slowest interaction per element (by_element).- Profiler:
record(…, { server: true })works across the instances of a serverless deployment: the window's signed id rides each of the session's requests, every instance writes them to the profiler store, and stop on any instance gathers them (window.instances); a window that fails leaves the session whole withserver: { error }. - Profiler: the JSON report names its
engine.version;?fields=(profile(path, { fields })) answers only the sections asked for;?weigh=fullweighs every file with no time budget, and each file it could not weigh says why (assets.missed_why). - A page with devtools switched on is sent
Document-Policy: js-profiling, so the browser's CPU sampler can run on it.
Changed
OgygiaDriver.evaluateis loosely typed, so a driver'spage.evaluatefits without a cast.- A pkg.pr.new preview is versioned
<next patch>-next.<commit>, never the release it was built after.
Fixed
record()whose page left for another document now says so, with the actions' own error ascause, instead of "serve with devtools".withOgygiaturns devtools on for the first page a test opens when there is nobaseURL; before, it was silently left off.- A page with devtools switched on is sent
private, no-storewithVary: Cookieand is never frozen, so a shared cache cannot hand its events to other visitors; a profiler beacon page too. - A hole's
Server-Timingreaches a browser that switched devtools on (it checked an old cookie value). - A profiler window whose stop never came frees its instance at its 60 s cap; before, that instance refused every later window.
- A file the profiler failed to weigh by a timeout is weighed again on the next profile; before, it stayed a miss for the life of the instance.
- Devtools' Page and Timeline tabs, and
page(), stay on the page after a router navigation that never swapped it (handed to the browser and held back, or overtaken); before, they reported no islands. - On Windows, ogygia knows its own files again: builds no longer register its devtools and profiler components as app modules or warn that its own marks are ignored.
- The dev dependency scan no longer fails when the profiler is on, so dependencies pre-bundle again;
devalueis now named through ogygia, so pnpm apps stop warning that it can't be resolved. - A click on a
load,idle,visibleor media island before it is live is no longer lost: the island wakes at once and the click is replayed when it is live, as on aninteractionisland;hydratedBy()then says'interaction'. - A Ctrl/Cmd/Shift- or middle-click on a link in an island that is not live yet opens the link as the browser would; before, it was canceled and replayed, and the replay could not open it.
0.8.2
Changed
- In a build with devtools, the launcher stays hidden until a browser turns devtools on (
?og-devtools=on, orogygia.devtools.on()in the console);?og-devtools=off,ogygia.devtools.off()or hide in the dock turn it off. The dev server is unchanged.
Fixed
- A hole's CSS now carries components taken through a package (its
exports, a TypeScript entry included) or a barrel — only the names imported — and an island inside a hole brings its own CSS, so what it renders after a click is styled.
0.8.1
v0.8.0 — passage
The passage release: the runtime and the compiler rebuilt on one identity primitive, regions that move instead of reset across navigations, a standalone compiler, a production SSR profiler, a devtools dock, a typed router, frozen pages and fragment federation. Underneath, one load scheduler, one DOM ownership model and one page model replace per-path rules.
Breaking changes
- Flags and experiments collapse onto one primitive,
flag();experiment,layer,allowOverrides,onExposure,batchExposures,.bucket,.onandroutes({ experiments })are removed.
Declare with a shape (flag('x'),flag('x', 10),flag('x', fn),flag('x', { control: 80, bold: 20 })), read by calling it (checkout(c)), branch withflag.pick(map)/flag.pick(c, map), read payloads withflag.value(c), configure once withdecide({ source, overrides, exposure, batch }), and pre-decide in a route-only shell withroutes({ flags: [...] }). kitMountis nowmount.kit.ogygia/rewriteis nowogygia/markup(no alias; change the import).transformHoleis removed;ogygia.handle({ transform })covers region answers and documents alike.regionTtlis nowregions: { ttl }.regions.preloadis removed; each island's whole graph preloads at its wake.
Added
- One load scheduler for every download ogygia starts: island code and background work wait for the page's
fetchpriority="high"resources (or the first input, or a 2.5 s cap), so islands no longer compete with the hero image. - Holes and regions a remote
query/commandrenders see their page'spage.url, route, params and server-loadpage.data(looked up through Kit, only when read), so a server-rendered header can refresh in place ("A lake that can refresh"). - One DOM ownership model for every ogygia DOM writer — who writes inside an element, its attributes, its children's order and their existence — so a web component's own DOM survives repairs, hole answers and navigations.
- SvelteKit 3 support, on the APIs late Kit 2 and Kit 3 share.
- The SSR profiler,
ogygia({ profiler }): CPU, network, heap and the browser half of a visit, with a 0–100 page score and on-demand mode (profiler: { onDemand: true }). - Devtools,
ogygia({ devtools }): an island-graph dock with Page, Hydration, Record, Bytes and Profiler tabs, every vital explained down to the island and line responsible. ogygia/testing: the devtools and the profiler as test primitives.ogygia/router: Kit's routing, programmatic and fully typed;document()renders a complete page from a handle.ogygia/internal/compiler: the compiler as a standalone, bundler-agnostic engine.- Frozen pages,
ogygia({ freeze: true }): a page renders once on write and is served as stored bytes until a publish thaws it (with edge stitching for per-visitor holes). - Fragment federation: one
federate(), a remote fragment is a region, and a publish thaws it everywhere. - The Ref hub: one identity layer for live classes, stores, functions, snippets and held regions that cross an island boundary.
- Transportable values: everything devalue rejects now crosses through one serialization seam; unbridgeable values fail at discovery, not in production.
- A navigation moves regions instead of resetting them (state-delta reconciler); opt-in server-delta navigation renders only what changed (
router: { serverDelta: true }). ogygia.handle({ transform }): server transforms that may reshape Svelte-owned markup, restored in the browser before hydration.ogygia/markupandscanRegions(html): the region round trip for an app that runs a third-party SSR pass over the page.- On-demand server islands:
render: 'deferred'+wake: 'interaction'fetch nothing until the visitor shows intent. prefetchwarms a deferred hole's HTML before it swaps.keepFallback(): a server island's answer that keeps the page's fallback.requestEvent(): Kit'sRequestEventinside a server island.isOgygiaPage(): tells shared code which world it runs in.- A lake survives Kit hydration on a
csr = truepage. ogygia({ barrels }): barrel imports become leaf imports at transform time.- Zero-config library islands: a dependency declares its island roots in its own
package.json. - The app's
hooks.client.tsinitruns oncsr = falsepages. - Server islands ship their component tree's CSS; region CSS obeys Kit's
inlineStyleThreshold. - Lazy islands download at background priority, so they never fight the first paint.
- Accessible SPA navigation (focus, announcements) and a reason on every page fetch.
when(): flags gate routes, andpickchooses infrastructure; OpenFeature interop (ogygia/flag/openfeature).- The build warns when island code reads a context nothing bridges.
- A recovered or healed island says why, and the discard warning names what Svelte threw.
- The ogygia MCP server grows to eleven tools, and the bundled AI skill is rewritten.
Changed
- A
<Region of={promise}>waits where the render can: awaited in the server render under Svelte's async mode, streamed as a late region on the server router, or resolved after hydration otherwise. - Islands inside a region's HTML always wake on the runtime,
csr = truepages included. - The page seed ships only when an island reads it, and only the
page.datakeys the page's islands read. - The remote seed ships only for remotes an island on the page can call.
- Island props reference the page seed instead of copying it, and ride at the end of the body with the module-preload hints.
- Island hints are
fetchpriority="low"; island wakes start after the page has painted. - The boot never loads Svelte and is four files fetched in one round trip; the runtime goes right before the page's own JavaScript.
- Duplicate stylesheet links are dropped.
- HTML is byte-identical across renders (window-aligned capability expiry, per-request slot ids).
- The server request path is one request store, one document pass and one walk, and stops allocating per item.
- A slow or hung page can no longer wedge the profiler or the site, and recordings coordinate across processes.
- The Vite plugin is a compiler, and region rendering is region-granular.
rolldownis no longer a dependency: the compiler parses and bundles through the app's Vite, keeping its 16 MB native binary out of production installs.
Fixed
- Island entries on pages below the root no longer 404 in builds (Kit's
asset()form comes from the build's Kit version). - A region moved by another script keeps its in-flight fetch and its woken island.
- Restored shadow roots keep their sheets across a router navigation.
- A preloaded
loadhole stays out of the batch instead of downloading twice. - Region URLs are pinned to their page, there is one region per hole across path depths, and a navigated island arrives whole in one swap.
- A registry's plain imports keep their CSS on
csr = falsepages; an island inside an island links its own CSS; held regions keep inline sheets. - A kept island adopts its server DOM instead of discarding it on every load.
- An island with children has the same fingerprint and bytes on every render.
- An island edited while it slept hydrates from its own server markup instead of re-rendering.
- A hole answer that is not the region's (a redirect, a whole document) is refused.
- A deferred hole survives Kit rebuilding a client-on document, and outlives a CDN-cached document.
- A hole's answer morphs over its fallback on every schedule, keeping a web component's attributes and state, unfocused edited fields, and focus visible to assistive tech.
- Region and hole HTML is parsed aware of declarative shadow DOM.
- A lake with no island host survives Kit giving up on the document, and a lake's island with props hydrates on a
csr = truepage. - An error page under a
csr = falsepage renders as the Kit-hydrated document it is. - A
csr = trueroute that imports a block registry no longer links every block's CSS and chunk, and acsr = falsepage links only the island CSS it renders. - Island entries and the runtime are content-hashed, so a deploy never runs the last build's island code.
- The runtime's hash module is no longer named
fingerprint.js, which ad blockers blocked. - A
visibleisland's code waits for the viewport, and a scroll-woken island no longer waits on the scheduler. - An island of ours on a Kit document waits until Kit has applied its page;
requestEvent()answers under Kit's own render too. goto()from an island on a Kit-booted page navigates through Kit, and a script module reached through an app alias gets the island's page.- The router keeps a destination page's inline
<style>sheets, followsdata-sveltekit-reload's grammar, navigates links inside web components in place, corrects the address bar after a server redirect, and keeps the live<body>on a fallback swap. - An unkeyed wrapper around an island survives a sibling inserted above it, and lakes restore by DOM position.
- A layout's csr world derives from the pages it serves, and a
csr = truepage under acsr = falselayout keeps the layout's chrome. kit.files.routes,kit.outDir,appDirandpaths.baseare honoured everywhere, and route walkers follow symlinks.- Island chunks are emitted only for islands the server bundle can reach.
- Region fingerprints no longer cost 20 ms per island; the profiler is free when it is not profiling.
$app/stores$pageworks inside every ogygia render root, and a server island'spage.urlis the referring page.- On a
csr = truepage a server island keeps its server-minted endpoint through Kit's hydration, and server-island HTML is absolutized for nested paths. wake: 'interaction'replays the waking click on the next frame, into the deepest target.- A
wake: 'none'lake in a+layout.svelteships its scoped CSS, a lake's or island's scoped CSS keeps its Svelte hash, and the fouc-css ...
v0.7.0 — placed islands (asRegion) + cross-island context
0.7.0 — 2026-08-20
Two arcs land together. Cross-island context becomes one model on Svelte's own getContext (with a drop-in setContext), and $page.data — streamed load promises and all — reaches islands on csr=false. And region authoring grows two ways to reach imports the import X with { … } form never could: import.meta.og.asRegion(Comp, options) marks any imported component (named / barrel, in .svelte, .ts, and .js), and a .ts registry with { wake } binding is now mountable — placeable by a third-party renderer (e.g. Builder.io's <svelte:component>), not only by region(). Plus a batch of correctness and build fixes.
Added
-
$page.data(+form/error/status) works inside islands — including STREAMED load
promises. An island reads$page.datathrough the$app/stateshim, but the handle can't reach
the resolved load data (Kit merges it locally inrender.js, never onRequestState), so
Region.sveltereads Kit's REAL page during SSR and records it; the handle seeds
data/form/error/statusinto the sameapplication/ogygia-pagescript islands already read
(boundary law: page.data crosses). A load may return a Promise at any level (Kit streaming) — dead on
csr=false, since Kit never defines its client resolver there. ogygia mirrors the mechanism with its
own registry: on a real browser navigation each promise is STAGED to a marker (a real pending Promise
on the client) and a<script>__ogygia_page_resolve(id, ok, value)</script>streams per promise AS
IT SETTLES — so the shell + pending{#await}paint immediately (FCP is not blocked on the slowest
promise) and each island's{#await page.data.x}resolves live, fast-first. Kit's own dead csr=false
resolve tail is drained server-side, so there's no__sveltekit_<hash> is not definedconsole error.
A REJECTED promise streams as an error and shows{#await …:catch}; a promise that resolves to a
value holding MORE promises re-defers them recursively (like Kit). A programmatic fetch (SPA/router —
noSec-Fetch-Mode: navigate) can't run streamed scripts, so those promises are awaited server-side
and revived as already-settled Promises —page.data.xstays a Promise on both paths, and a
rejection there never crashes the render. The app's universaltransporthook applies to the seed
too, so a load's CUSTOM types round-trip into islands — both plain and inside a streamed promise, not
just built-in devalue types. Guarded bytest/page-defer.test.ts(codec + settle + recursion +
rejection races + transport),e2e/page-data.ts(stream-level timings + browser hydrate) and
e2e/page-data-stress.ts(rejection, nested recursion, 12 staggered, non-navigate no-crash, custom
transport type plain + streamed). -
Drop-in
setContext— adopt with an import swap. Swapimport { setContext } from 'svelte'
forfrom 'ogygia'and an existing csr=false layout's context reaches child islands with NO other
change. It does exactly what Svelte's does (same-root + the SSR-nested tree), and on the server also
records each string-keyed value so the handle emits ONE page-level<script data-ogygia-provide-page>
marker every island seeds its owngetContext('key')from — closing the gap where a plain
setContextshows on the server but is gone on the client (child islands are separate hydration
roots). This is the FLAT page root: every island on the page inherits it; a live[ogygia.wire]
value still reunites to one instance, so a writer island bumping it repaints every reader. For
scoped/shadowed context use<Provide>, which wraps its subtree and beats the root on the same key.
node:async_hooksstays server-only (out of the client bundle). Guarded by the/ctx-setcontext
block ine2e/context.ts. -
A
.tsregistrywith { wake: … }binding is now MOUNTABLE. Marking a componentwith { wake }
in a.tsmodule (a registry / remote) used to yield a bare held descriptor — a value onlyregion()
could render. It now yields a real component (the wrapper, with the region metadata attached), so a
third-party renderer can PLACE it directly (e.g. Builder.io's<svelte:component this={component}>)
and get the<ogygia-region>shell — while its JS still loads only when the component is placed AND
its wake schedule fires. It stays a HELD, crossable island too (region()over a live/remote query is
unchanged), so this is a pure superset. Mirrors what a.sveltewith { wake }has always produced;
a.sveltePLACED island and a.tsHELD island keep distinct identities (one carries a server
endpoint, the other does not).region: 'raw'is unchanged. Threaded through one shared emitter so
the.svelteand.tspaths can't drift. -
import.meta.og.asRegion(Comp, options)— the barrel escape hatch for regions. The
import-attribute form (import X from './X.svelte' with { … }) only reaches a DEFAULT import of one
file, so a component you can only get through a barrel (import { Header } from '@design/system')
couldn't be a region.asRegionmarks ANY imported component — named or default — as a region. It
is a transparent escape hatch:optionsaccept EXACTLY what an import attribute accepts (no
more, no less — both funnel through one parser) and the output is identical, only importing the
component by its export name. Every mode works — a placed island (wake), a deferred/live server
hole (render), a lake (wake: 'none'), a held region (region: 'raw'), or a namedpreset:import { Header, Chart, Block } from '@design/system'; const HeaderIsland = import.meta.og.asRegion(Header, { wake: 'load' }); const ChartHole = import.meta.og.asRegion(Chart, { render: 'deferred', wake: 'visible' }); const BlockRaw = import.meta.og.asRegion(Block, { region: 'raw' }); // → region(BlockRaw, …)Region identity keys on
source#exportName, so two named exports of one barrel are distinct
regions. Tree-shaking is preserved — a region off a huge mixed barrel ships only its own
component, not the barrel. Compile construct — rewritten to a hoisted binding import; there is no
runtimeasRegion.Works in
.svelte,.ts, and.jsfiles. In a.sveltehost it mints a placed island; in a
.ts/.jsregistry or remote it mints a mountable held binding (wake) or a bare descriptor
(region: 'raw') — the same records thewith { … }form makes there, so a barrel component in a
.tsregistry (e.g. handed to Builder.io's<svelte:component>) is now a one-line macro.It is top-level only:
const Local = import.meta.og.asRegion(Comp, options)in the instance
<script>. Misuse is a loud build error — a call nested in a loop / function / block / expression,
in<script module>or markup; alet/varor multi-declarator binding; a non-imported or
namespace-imported first argument; or a component already marked with an import attribute (use one
mechanism, not both).
Changed (breaking)
- Unified cross-island context.
<Context of={ctx} value={v}>+ctx.get()(which walked the
DOM) is replaced by ONE provider and plain Svelte reads.<Provide values={obj | array}>writes
serialized values into the DOM (clsx-style: an object, or an array of objects merged left→right,
falsy skipped), and ogygia seeds each island'shydrate({ context })from the providers above it —
so a child island's owngetContext('key'), unchanged, reads a (csr=false) layout's context
across the island-root split.createContext<T>(key, default?)is now optional TYPED sugar over
the same string key: callable to make a<Provide>entry (theme('dark')→{ theme: 'dark' })
and.get()to read typed — a typed context andgetContext('sameKey')are the same value. The
old keylesscreateContext()and the<Context>component are gone. Values must be serializable,
like island props (a[ogygia.wire]value still reunites to one live instance across roots). The
e2e/context.tsmatrix (live reunite, defaults, nested shadowing, lakes, SPA nav, defer/visible)
passes on the new API, plus a raw-getContextisland proving the interop case.
Fixed
-
A
csr = falsesubtree under a csr=true ancestor layout now islands correctly. The transform
marks csr=true route hosts with a context flag so<Region>degrades to inline there — but Svelte
context flows to ALL descendants while Kit's csr option is per-node, so an option-less ROOT layout
(Kit default csr=true) leakedtrueinto acsr = falsechild subtree: everywake:island there
silently rendered inline — zero<ogygia-region>, no hydration, noonMount— on a page whose own
csr is false. The transform now injects the opposite marker (false) into every csr=false route
host, RESETTING the inherited flag exactly like Kit resolves options ({ ...parent, ...own }); a
csr=true host below the reset re-shadows withtrue, so mixed trees work in both directions. The
csr state threads plugin → transform as a tri-state (truehost /falsehost / non-route).
Guarded bytest/csr-false-reset.test.tsand the/mixed-rootfixture +e2e/csr-mixed-tree.ts. -
ogygia's own injected imports resolve to ogygia's own files, not the importer's package. The
transform injectsogygia/internal(Region / og_portable) andogygia/internal/serverinto a host
or a generated island wrapper. For a host that lives in a monorepo sub-package which doesn't itself
depend on ogygia, a bareogygia/internalfailed to resolve (Rolldown failed to resolve import "ogygia/internal" from ".../Toolbar.svelte"). The plugin now resolves those injected imports to
ogygia's OWN files directly — an absolute path underPKG_ROOT(where the plugin lives),
src/internal.tsin a source checkout ordist/internal.jsin a published install, chosen so...
v0.6.6
Patch: a portable snippet that captures several names from one import no longer breaks the build.
Fixed
- A
{#snippet}forwarded into an island could emit duplicate imports. When such a snippet's body captures multiple names from ONEimport { a, b, c } from './x', ogygia hoists the host import into the synth wrapper it generates for the crossing — but it pushed the whole statement once per captured name, so three names produced three identical import lines and the build failed withIdentifier 'a' has already been declared. The hoisted imports are now deduped: each statement is emitted once (it already declares all of its names), preserving the original import verbatim (a snippet-nested island'swith { wake: … }marks, and default / namespace forms, survive untouched). Reproduced and guarded bytest/portable-snippet-imports.test.ts.
v0.6.3
Patch: same-shell page transitions no longer stutter under a large sidebar.
Fixed
- A
DocsShellsidebar drowned its own page cross-fade. Each nav row carries a per-rowview-transition-name(so theog-nav-activechip glides beneath stable labels), which promotes every row to its own transition group. On a same-shell navigation the browser then ran the default group+fade for all of them — dozens per hop (~150 for a 30-row sidebar, ~300 for a large one) — even though the rows are identical and stationary. That flood starves the actual content cross-fade, which janks or reads as "no transition." The rows already share aphnavview-transition-class; every built-in theme now zeros those row animations (animation: none), so an identical row snaps invisibly while the chip keeps its slide and the root content fade is no longer drowned. Measured on the docs playground: 295 → 10 running animations, slide and fade both intact. Applied across every built-in theme.
v0.6.2
Patch: content bodies ship their own scoped CSS, a static server island no longer 404s on nav, and a shell-change page transition no longer stutters.
Fixed
-
Content-body CSS on
csr=falsepages. A.svx/.mdbody's own scoped<style>lived only in the server bundle (the leak-free corpus never enters the client graph), so it shipped on no page stylesheet and doc content rendered browser-default in production. The build nowsvelte-compiles each content module's scoped CSS from its post-mdsvex source (so:globalis resolved and the scoped hash matches the SSR'd HTML) and emits it as a content-addressed client asset, keyed in the region-deps handoff;Region.sveltelinks it via<link data-ogygia-region-css>. Per-page cost is only the CSS of the docs actually rendered, and it survives SPA nav. Covered bye2e/content-css.ts. -
Static server-island 404 on navigation. A
render: 'deferred'island with nowakehas no client module, butRegion.sveltestill emitted the region id as the DOMentry. The router's next-page module-warmer imports everyentry="…", so that bare id fetched/<id>→ 404 (only on nav — the warmer runs on prefetch). Static server islands now carryentry=""; the hole still fetches through its signed endpoint (minted from the id, unchanged). The same rule covers held deferred regions. -
Shell-change page-transition stutter. A
view-transition-namelifts its element out of the root cross-fade into a standalone group. Navigating between a page with a sidebar of many named nav rows and one with none left every name orphaned, each running a solo enter/exit over a holed-out root snapshot — a stutter, worst in dev. The router now folds orphanedview-transition-names (present on only one of the two pages) into the page cross-fade for that navigation, and restores them after. Names present on both pages (a same-shell highlight slide) are kept untouched.
Full stack green: 809 unit tests + 40 e2e checks.