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" }
}
Always use 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 navigation
  • RouterMiddlewareResult.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.

It's a client-side SPA router. No server-side rendering, no SEO magic. If you need those, Kraft is probably not the right tool. It's built for admin panels, dashboards, and internal tools where SEO doesn't matter.