A page has a stable id, a payload type, and a host path. The pack
never sees the path pattern — only the resolved path, params, and data.
If you have not stood the process up yet, start with
the host and
the pack.
This is the one-pack shape: you own the host and the frontend, and pass that one pack at render. The same registry is the contract other packs would implement if you later install them. See Page contracts.
@Serializable
data class HomePage(val greeting: String)
pages {
page<HomePage>("home", "/") {
head("Hello", description = "A greeting from the host.")
HomePage("hello")
}
}The registry is the source of truth for ids and payload serializers. Ktor (or
any host) walks it to bind routes. Typegen walks it to emit .d.ts — publish
that file for pack authors.
The pack file tree is keyed by page id, not URL. pages/home/ implements
home; pages/harbor/home/ implements harbor.home. Folders are id segments.
They are not routes.
The sample below is Svelte (page(), <Head />). Use the navbar frontend picker for the matching adapter API (usePage, injectKeelPage, and so on). See Framework adapters.
Implement home as pages/home/+page.tsx. Use usePage() (or Solid’s accessor) and <Head /> from that adapter package. Folders are page-id segments, not routes.
Implement home as pages/home/+page.vue. Use usePage() and the Vue Head SFC from @kolektiv/keel-vue.
Implement home as pages/home/+page.ts. Lit: extend KeelElement / usePage controller. Angular: injectKeelPage() and KeelHead. Id override file is +page.id.ts.
<script lang="ts">
import { page, Head } from "@kolektiv/keel-svelte"
import type { HomePage } from "@app/page-types"
const ctx = page<HomePage>()
</script>
<Head />
<h1>{ctx.data.greeting}</h1><script>
import { page, Head } from "@kolektiv/keel-svelte"
const ctx = page()
</script>
<Head />
<h1>{ctx.data.greeting}</h1><Head /> reads page().head — the same tags the host already wrote into the
document for crawlers. Pass title only to override. See
Document head and SEO.
Bootstrap reads #__keel_seed, imports entry, and calls mount(host, ctx).
The adapter generates that wrapper so you write ordinary components.
Use the TS/JS toggle on the block itself to pin a snippet, or the language control in the navbar to switch every example that has not been pinned.