Skip to content
Moku LabsDocs
@moku-labs/web pages

router

Default. The route table: the typed route() builder, a specificity-sorted matcher, URL and file paths, the render mode.

router.mdllms.txt

What it does

Isomorphic default. router owns the route table. You write routes with the route() builder, register them in pluginConfigs.router.routes, and the router compiles them while createApp runs. build, head and spa read the compiled table through its API: matching, URLs, output files and the render mode.

How it works

Each route compiles to two matchers built on a plain RegExp: one with the locale slot filled by the configured locales, and one with the {lang:?} segment removed. A pathname is tried against the first, then the second, which injects the default locale. That is how /hello/ resolves to { slug: "hello", lang: "en" } and /uk/hello/ to { lang: "uk", slug: "hello" }.

The table is sorted by the number of dynamic segments, fewest first, with declaration order as the tie-break. {lang:?} does not count, so adding a locale prefix never changes priority. The same matching code runs at build and in the browser, so the two cannot disagree.

createApp validates the table and stops on: an empty map, a pattern that does not start with /, unbalanced braces, or more than one {lang:?} segment.

Types are a call-site matter. route() captures the pattern as a literal, so ctx.params is inferred, and .load() widens ctx.data. The route map in config is erased to Record<string, RouteDefinition>; the build gets the full definitions back from manifest().

Config

pluginConfigs.router.

OptionTypeDefaultDescription
routesRouteMapnoneRoute name → route(...). An import * as routes namespace works. The only way to register routes.

The render mode is not router config. It is the global createApp({ config: { mode } }), which the router exposes as mode().

API

Mounted at app.router, reachable with ctx.require(routerPlugin).

MethodSignatureDescription
match(pathname: string) => { params: Record<string, string>; route: RouteDefinition } | nullThe most specific match, or null.
toUrl(routeName: string, params: Record<string, string>) => stringFills the pattern. Throws on an unknown name.
entries() => readonly TypedRoute[]Every route with pattern, name, meta, toUrl, toFile, match, most specific first.
manifest() => readonly RouteDefinition[]The full definitions with handlers, in declaration order.
clientManifest() => readonly ClientRoute[]{ pattern, name, meta } for each route, frozen and JSON-safe, most specific first.
mode() => "ssg" | "spa" | "hybrid"The global render mode.

Helpers

Plain functions with no ctx. They run before createApp. Exported from both entries, and reachable as routerPlugin.route, routerPlugin.defineRoutes, routerPlugin.createUrls.

HelperSignatureDescription
route<P extends string>(pattern: P) => RouteBuilder<RouteState<P>>The fluent builder. The returned object is itself a RouteDefinition.
defineRoutes<T extends RouteMap>(routes: T) => TKeeps the literal type of the map for IntelliSense.
createUrls<T extends RouteMap>(routes: T, defaultLocale?: string) => Urls<T>An app-free URL builder: toUrl(name, params?), names typed to the map. Pass the default locale to get bare paths for it.

The builder methods are listed in the routing guide.

Events

Emits

None.

Listens to

None.

Depends on

  • site: the base URL.
  • i18n, optional: the locales and the default locale. Without it the router compiles for "en".

Example

scripts/routes.ts
import { createApp, createUrls, defineRoutes, route } from "@moku-labs/web";
import { h } from "preact";

const routes = defineRoutes({
  home: route("/{lang:?}/").render(() => h("h1", null, "Home")),
  post: route("/{lang:?}/{slug}/")
    .render(ctx => h("h1", null, ctx.params.slug))
    .meta({ section: "blog" }),
  about: route("/about/").render(() => h("h1", null, "About"))
});

const app = createApp({
  pluginConfigs: {
    site: { name: "My Blog", url: "https://blog.dev", author: "Ada", description: "Notes." },
    i18n: { locales: ["en", "uk"], defaultLocale: "en" },
    router: { routes }
  }
});

console.log(app.router.match("/uk/hello/")?.params); // { lang: "uk", slug: "hello" }
console.log(app.router.match("/hello/")?.params); // { slug: "hello", lang: "en" }
console.log(app.router.toUrl("post", { lang: "uk", slug: "hello" })); // "/uk/hello/"
console.log(app.router.mode()); // "hybrid"
console.log(createUrls(routes, "en").toUrl("post", { lang: "en", slug: "hello" })); // "/hello/"
  1. The same output as app.router.toUrl, with no app.
run
bun scripts/routes.ts
{ lang: "uk", slug: "hello" }
{ slug: "hello", lang: "en" }
/uk/hello/
hybrid
/hello/
  • Routes are registered once. There is no router.set(). An app that builds routes at run time composes the map before createApp.
  • No routes, no router. Without pluginConfigs.router.routes, every call throws [web] router: routes not registered.
  • toUrl throws on a typo. An unknown name is an error, not an empty link.
  • Only {lang:?} matches when absent. Another optional segment, such as {page:?}, matches only when it is present.
  • meta must be JSON. It ships to the browser in clientManifest(). No functions, symbols or class instances.