The host registry is the contract: a stable page id, a payload type, and a path the host owns. Packs implement ids. They never own URLs.
One pack is your app UI. Many packs are installed themes against the same types. The call site passes the one that renders; the API does not change.
@KeelType("harbor.home")
@Serializable
data class HomePage(
val viewer: UserRef?,
val feed: List<FeedItem>,
)
@KeelType("harbor.user")
@Serializable
data class UserPage(
val user: UserRef,
val messages: List<FeedItem>,
)
pages {
page<HomePage>("harbor.home", "/") { HomePage(Board.viewer(call)?.toRef(), Board.feed()) }
page<UserPage>("harbor.user", "/u/{id}") { call ->
UserPage(Board.user(params.getValue("id"))!!.toRef(), Board.messagesFor(params.getValue("id")))
}
}
Typegen walks @KeelType / @KeelAction and emits .d.ts plus JSON.
Gradle generateKeelTypes is the offline scan. Pack authors depend on that
file — not on your running server.
export interface HomePage { viewer: UserRef | null; feed: FeedItem[] }
export interface UserPage { user: UserRef; messages: FeedItem[] }
export interface HarborPages {
"harbor.home": HomePage
"harbor.user": UserPage
"harbor.notFound": NotFoundPage
}
export type HarborPageId = keyof HarborPages
export interface HarborActions {
"harbor.setName": { in: SetNameIn; out: SetNameOut }
"harbor.postMessage": { in: PostMessageIn; out: PostMessageOut }
}
A page is keyed by id: pages/harbor/home/+page.svelte implements
harbor.home. The file extension follows the adapter, and the page is still
just a function of the typed seed.
<script lang="ts">
import { Head, Link, page } from "@kolektiv/keel-svelte"
import type { HomePage } from "../../../lib/page-types"
const ctx = page<HomePage>()
</script>
<Head />
<h1>Harbor</h1>
{#each ctx.data.feed as item (item.id)}
<p>
<Link href={`/u/${item.userId}`}>{item.displayName}</Link>
{item.body}
</p>
{/each}<script>
import { Head, Link, page } from "@kolektiv/keel-svelte"
const ctx = page()
</script>
<Head />
<h1>Harbor</h1>
{#each ctx.data.feed as item (item.id)}
<p>
<Link href={`/u/${item.userId}`}>{item.displayName}</Link>
{item.body}
</p>
{/each}import { Head, Link, usePage } from "@kolektiv/keel-react"
import type { HomePage } from "../../../lib/page-types"
export default function Home() {
const seed = usePage<HomePage>()
return (
<>
<Head />
<h1>Harbor</h1>
{seed.data.feed.map((item) => (
<p key={item.id}>
<Link href={`/u/${item.userId}`}>{item.displayName}</Link>
{item.body}
</p>
))}
</>
)
}import { Head, Link, usePage } from "@kolektiv/keel-react"
export default function Home() {
const seed = usePage()
return (
<>
<Head />
<h1>Harbor</h1>
{seed.data.feed.map((item) => (
<p key={item.id}>
<Link href={`/u/${item.userId}`}>{item.displayName}</Link>
{item.body}
</p>
))}
</>
)
}<script setup lang="ts">
import { Head, Link, usePage } from "@kolektiv/keel-vue"
import type { HomePage } from "../../../lib/page-types"
const seed = usePage<HomePage>()
</script>
<template>
<Head />
<h1>Harbor</h1>
<p v-for="item in seed.data.feed" :key="item.id">
<Link :href="`/u/${item.userId}`">{{ item.displayName }}</Link>
{{ item.body }}
</p>
</template><script setup>
import { Head, Link, usePage } from "@kolektiv/keel-vue"
const seed = usePage()
</script>
<template>
<Head />
<h1>Harbor</h1>
<p v-for="item in seed.data.feed" :key="item.id">
<Link :href="`/u/${item.userId}`">{{ item.displayName }}</Link>
{{ item.body }}
</p>
</template>import { Head, Link, usePage } from "@kolektiv/keel-solid"
import type { HomePage } from "../../../lib/page-types"
export default function Home() {
const seed = usePage<HomePage>()
return (
<>
<Head />
<h1>Harbor</h1>
{seed().data.feed.map((item) => (
<p>
<Link href={`/u/${item.userId}`}>{item.displayName}</Link>
{item.body}
</p>
))}
</>
)
}import { Head, Link, usePage } from "@kolektiv/keel-solid"
export default function Home() {
const seed = usePage()
return (
<>
<Head />
<h1>Harbor</h1>
{seed().data.feed.map((item) => (
<p>
<Link href={`/u/${item.userId}`}>{item.displayName}</Link>
{item.body}
</p>
))}
</>
)
}import { Head, Link, usePage } from "@kolektiv/keel-preact"
import type { HomePage } from "../../../lib/page-types"
export default function Home() {
const seed = usePage<HomePage>()
return (
<>
<Head />
<h1>Harbor</h1>
{seed.data.feed.map((item) => (
<p key={item.id}>
<Link href={`/u/${item.userId}`}>{item.displayName}</Link>
{item.body}
</p>
))}
</>
)
}import { Head, Link, usePage } from "@kolektiv/keel-preact"
export default function Home() {
const seed = usePage()
return (
<>
<Head />
<h1>Harbor</h1>
{seed.data.feed.map((item) => (
<p key={item.id}>
<Link href={`/u/${item.userId}`}>{item.displayName}</Link>
{item.body}
</p>
))}
</>
)
}import { html } from "lit"
import { KeelElement } from "@kolektiv/keel-lit"
import type { HomePage } from "../../../lib/page-types"
export default class Home extends KeelElement<HomePage> {
render() {
return html`
<keel-head></keel-head>
<h1>Harbor</h1>
${this.page.data.feed.map(
(item) => html`<p>
<keel-link href="/u/${item.userId}">${item.displayName}</keel-link>
${item.body}
</p>`,
)}
`
}
}import { html } from "lit"
import { KeelElement } from "@kolektiv/keel-lit"
export default class Home extends KeelElement {
render() {
return html`
<keel-head></keel-head>
<h1>Harbor</h1>
${this.page.data.feed.map(
(item) => html`<p>
<keel-link href="/u/${item.userId}">${item.displayName}</keel-link>
${item.body}
</p>`,
)}
`
}
}import { Component } from "@angular/core"
import { KeelHead, KeelLink, injectKeelPage } from "@kolektiv/keel-angular"
import type { HomePage } from "../../../lib/page-types"
@Component({
selector: "app-harbor-home",
standalone: true,
imports: [KeelHead, KeelLink],
template: `
<keel-head />
<h1>Harbor</h1>
@for (item of page().data.feed; track item.id) {
<p>
<keel-link href="/u/{{ item.userId }}">{{ item.displayName }}</keel-link>
{{ item.body }}
</p>
}
`,
})
export default class Home {
readonly page = injectKeelPage<HomePage>()
}import { Component } from "@angular/core"
import { KeelHead, KeelLink, injectKeelPage } from "@kolektiv/keel-angular"
@Component({
selector: "app-harbor-home",
standalone: true,
imports: [KeelHead, KeelLink],
template: `
<keel-head />
<h1>Harbor</h1>
@for (item of page().data.feed; track item.id) {
<p>
<keel-link href="/u/{{ item.userId }}">{{ item.displayName }}</keel-link>
{{ item.body }}
</p>
}
`,
})
export default class Home {
readonly page = injectKeelPage()
}A pack declares which ids it implements. It may implement a subset —
omitting harbor.user is valid, and the call site passes the pack that
covers the page.
{
"format": "keel/1",
"id": "harbor",
"version": "0.1.0",
"framework": "svelte",
"pages": {
"harbor.home": { "module": "pages/harbor.home.js" },
"harbor.user": { "module": "pages/harbor.user.js" },
"harbor.notFound": { "module": "pages/harbor.notFound.js" }
}
}