A pack is a UI that implements page ids. It never owns URL patterns. Svelte was the first implementation of this contract; seven adapters now ship — see Framework adapters.
PageModule
Every page module exports:
export function mount(host: Element, ctx: PageContext<T>): void | Promise<void>
export function unmount(): void | Promise<void>
export function update?(ctx: PageContext<T>): void | Promise<void>
update runs when a visit keeps the same page id and preserveState is set
(partial reloads, validation 422s). Framework adapters generate this wrapper
so authors write ordinary files.
Bootstrap and hydration
The shell loads bootstrap.js as a module. Bootstrap reads #__keel_seed,
mounts the entry, and starts the router. The initial applySeed must not
steal focus or announce — that is for subsequent client navigations.
Client visits go to the page URL with X-Keel-Visit: true. Prefetch
dedupes in-flight GETs, caps the seed cache, and must ignore partial
responses.
manifest.json and .feb
A .feb is a zip whose root is manifest.json plus the files it names.
There is no nested pack directory. Format keel/1:
{
"format": "keel/1",
"id": "harbor",
"version": "0.1.0",
"framework": "svelte",
"host": "#__keel_root",
"pages": {
"harbor.home": {
"module": "pages/harbor.home.js",
"css": [
"assets/styles-4f3c2b1a.css"
]
}
},
"notFound": "pages/harbor.notFound.js"
}
keel-pack zips a Vite dist into that archive. The Vite plugin
(keelPack) discovers pages, writes the manifest, and calls a packager.
Page entry fields:
| Field | Required | Meaning |
|---|---|---|
module |
yes | Page module path, relative to the archive root |
css |
no | Stylesheet paths the page needs |
head |
no | Compiled head markup with {{…}} placeholders |
Layouts are not separate manifest entries. Adapters inline the page’s layout
chain, root-first, into the emitted entry module. The manifest carries the
notFound module alongside pages. The host substitutes the head
placeholders and renders it on the document GET. See
Document head and SEO.
RouterAdapter
RouterAdapter maps files to page ids — never URL params:
interface RouterAdapter {
name: string
discover(pagesDir: string): DiscoveredPage[]
entrySource(page: DiscoveredPage): string
}
routerFor(name) resolves the built-in adapter for framework:
svelte is the default; react, vue, solid, preact, lit, and
angular ship as codegen adapters. An unknown name throws with the supported
list. Pass router to use a custom adapter — it takes precedence over the
registry. New frameworks register an adapter factory; vite.ts does not
change.
Each non-Svelte adapter compiles a neutral +head.html instead of
+head.svelte, and uses +page.ts for the id override except Lit and
Angular, where the page itself is +page.ts, so the override is
+page.id.ts.
Packager and manifest hooks
keelPack({
id: "app",
version: "1.0.0",
framework: "svelte",
manifest: (base) => ({ ...base, /* host extension fields */ }),
packager: (opts) => { /* directory, tarball, CDN upload; default is packFeb */ },
})
packFeb remains the default packager. Point contract at typegen’s JSON
(keel/1 { pages }) so unknown ids fail the build; a .ts Pages
interface is still accepted as a fallback.
To start a pack from a running host:
pnpm exec keel-scaffold <origin> ./pack
See Typegen.
See Packs and manifests for the author-facing description and Host adapter for how a host loads and serves the archive.