Routing
Type-safe routes with parameters, middleware for auth guards, and nested layouts.
Defining routes
Routes are objects — not strings scattered across your codebase:
object Nav {
val home = Static("/")
val about = Static("/about")
val userProfile = Route1("/users/{id}")
val userPost = Route2("/users/{id}/posts/{postId}")
} Static is for routes with no parameters. Route1 through Route7 support 1-7 typed parameters.
Setting up the router
val app = kraftApp {
routing {
usePathStrategy() // Clean URLs: /users/123
// or useHashStrategy() for hash URLs: /#/users/123
mount(Nav.home) { HomePage() }
mount(Nav.about) { AboutPage() }
mount(Nav.userProfile) { route ->
val userId = route["id"]
UserProfilePage(userId)
}
catchAll { NotFoundPage() }
}
} The catchAll block handles any URL that doesn't match a defined route.
Rendering the current route
Drop RouterComponent() wherever you want the active page to appear:
class App(ctx: NoProps) : PureComponent(ctx) {
override fun VDom.render() {
NavBar()
div(classes = "content") {
RouterComponent() // Active page renders here
}
Footer()
}
} Navigation
Programmatic navigation
// Navigate to a static route
router.navToUri(Nav.about())
// Navigate with parameters
router.navToUri(Nav.userProfile("alice"))
// Navigate handling a click event
ui.button {
onClick { evt -> router.navToUri(evt, Nav.home()) }
+"Go home"
}
// When you pass the MouseEvent, Kraft detects modifier keys:
// Ctrl+Click (Cmd+Click on Mac) opens in a new tab,
// just like a native link. No platform-specific code needed.
// Go back
router.navBack()
// Replace current URL (no history entry)
router.replaceUri(Nav.home()) Link-style navigation with A.href(route)
For anchor tags, use the href helper on A — it takes a bound route and writes the
correct
URL into the href attribute:
ui.menu {
noui.item A { href(Nav.home()); +"Home" }
noui.item A { href(Nav.about()); +"About" }
noui.item A { href(Nav.userProfile("alice")); +"Alice" }
} A.href(route) for links — don't hand-write href =
"/about".
The helper asks the active router to render the URL using the configured path strategy, so the same code
produces
/about under usePathStrategy() and #/about under
useHashStrategy(). Hard-coded href strings bypass the strategy and break when you switch modes (or
when
the app is mounted under a non-root base path).
The helper lives in io.peekandpoke.kraft.routing:
/** Sets the href attribute of an anchor tag from a bound route. */
fun A.href(route: Route.Bound) {
val c = consumer as? VDomTagConsumer ?: error("Consumer must be a VDomTagConsumer")
val router = c.host.router
href = router.strategy.render(route)
} Because it sets a real href, the browser handles Ctrl/Cmd+Click, middle-click, right-click → "Open
in
new tab", and link previews natively. For clicks that should navigate in-place (and not reload the page),
combine
href with an onClick that calls router.navToUri(evt, route) — Kraft will
detect modifier keys and let the browser handle new-tab clicks:
a {
href(Nav.userProfile("alice"))
onClick { evt -> router.navToUri(evt, Nav.userProfile("alice")) }
+"Alice"
} Layouts
Wrap groups of routes in a shared layout:
routing {
layout({ content ->
div(classes = "app-shell") {
NavBar()
div(classes = "main") {
Sidebar()
div(classes = "content") { content() }
}
Footer()
}
}) {
mount(Nav.home) { HomePage() }
mount(Nav.about) { AboutPage() }
mount(Nav.userProfile) { route -> UserProfilePage(route["id"]) }
}
} The layout receives a content function — call it where you want the page to render. You can nest layouts too.
Middleware
Middleware intercepts navigation before a page renders. The most common use case: auth guards.
routing {
// Public routes
mount(Nav.login) { LoginPage() }
// Protected routes
middleware({ ctx ->
if (AppState.auth.isLoggedIn) {
RouterMiddlewareResult.Proceed
} else {
RouterMiddlewareResult.Redirect(Nav.login())
}
}) {
layout({ LoggedInLayout(it) }) {
mount(Nav.dashboard) { DashboardPage() }
mount(Nav.profile) { ProfilePage() }
}
}
catchAll { NotFoundPage() }
} Middleware returns one of:
RouterMiddlewareResult.Proceed— allow navigationRouterMiddlewareResult.Redirect(uri)— redirect to another route
Reacting to route changes
Subscribe to the router's current stream from any component:
class BreadCrumb(ctx: NoProps) : PureComponent(ctx) {
private val currentRoute by subscribingTo(router.current)
override fun VDom.render() {
ui.breadcrumb {
div(classes = "section") { +"Home" }
div(classes = "divider") { +"/" }
div(classes = "active section") {
+currentRoute.uri
}
}
}
} Setting the page title
Kraft provides a PageTitle component:
class AboutPage(ctx: NoProps) : PureComponent(ctx) {
override fun VDom.render() {
PageTitle("About Us")
ui.segment {
// page content
}
}
} Real-world example
Here's the routing setup from the funktor-demo admin app:
object Nav {
val auth = AuthFrontendRoutes()
val dashboard = Static("")
val profile = Static("/profile")
}
fun RootRouterBuilder.mountNav(authState: AuthState<AdminUserModel>) {
// Auth module mounts its own routes (login, logout, etc.)
authState.mount(this)
// Auth middleware protects everything below
val authMiddleware = authState.routerMiddleWare(Nav.auth.login())
middleware(authMiddleware) {
layout({ LoggedInLayout(it) }) {
mount(Nav.dashboard) { DashboardPage() }
mount(Nav.profile) { ProfilePage() }
}
}
catchAll { NotFoundPage() }
} This pattern — defining routes as objects, mounting them with middleware and layouts — is how we structure all our apps.