Routing
Routes come from files. Everything about them except the current path is decided at compile time.
The route table
doneFiles under windows/<name>/pages/** become routes:
| File | Route |
|---|---|
pages/index.tsx | / |
pages/layout.tsx | layout |
pages/products.tsx | products |
pages/products/new.tsx | products/new |
pages/products/$id.tsx | products/$id |
index names the directory it sits in, so pages/products/index.tsx also produces
products — and that collision is reported as a duplicate rather than resolved by a
rule.
A $ prefix makes a segment a parameter. Static segments beat parameters at the same
depth, so products/new matches before products/$id.
bun run routes # print the table
useRouter
doneRead access to the window's current route.
const router = useRouter();
<div>You are at {router.path}</div>;
router.path
doneThe active route, as an ordinary signal — the window's own, by identity.
Interpolating it is a text binding that follows navigation, and anything derived from
it is a computed like any other.
router.matches
done<button className={cn("link", { active: router.matches("layout") })}>Layout</button>
True while the active route is path, or nested under it. Prefix-aware:
matches("products") holds on products/new too, because a nav entry names a
section.
:::danger Do not use ===
// Wrong. Always false, for ever.
router.path === "layout";
router.path is a signal, so comparing it to a string is false at build time and
the nav compiles clean and never highlights. Use matches, or — for exact equality —
compare in the window's own module where the route signal lives:
export const onNewProduct = computed(() => route === "products/new");
That works because the reactive rewrite runs on your module. :::
useRouter() is read-only, deliberately. Anything derived belongs in the window's own
module as a computed, because a computed() created inside a component has no
export name for the generated module to import.
Calling it in a window that did not pass route to <Window> is an error naming the
fix.
useRoute
doneexport default function ProductDetail() {
const route = useRoute("products/$id");
// ...
}
The string must match the file's own path under pages/, because it is what types
args and nothing else verifies it. A mismatch is an error naming both paths — it
means the file moved and the string did not.
:::warning Params are not bindings yet done
The recorders exist but the emitter does not read them, so args.id does not yet
produce a live binding. Track it in API.md.
:::
Href
doneEach window gets an Href union generated from its route table, so a typo in a static
segment is a type error.
A static route contributes a string literal; a parameter contributes ${string}, so
both "products/1" and a template literal check.
:::note A limit worth knowing before it surprises you
${string} spans slashes, so the type also accepts products/a/b/c. TypeScript
catches typos; the compiler catches shape.
Worse: a parameter in the first segment — pages/$slug.tsx — contributes a bare
${string}, which absorbs every other member and makes that window's Href equal to
string. The type then checks nothing.
:::
Navigating
done — (2026-08-20) — `src/runtime/navigate.ts`, exported from `dziry`. The host installs the window's route signal at launch; `navigate(path)` writes it (same-path early-out), `back()` reads the one-entry history — one entry by decision, so a second `back()` oscillates. `navigate("…")` literals in captured handler sources are checked against the route table like hrefs (`deadNavigations` in build.ts); module-level handler bodies cross the boundary as names and are the `Href` type's to check. Before the window is up, `navigate()` warns and does nothing — modules are also imported by the compilerThere is no navigate() yet. Today a window exports one handler per destination:
export const route = signal("/");
let previous = "/";
function go(path: string): void {
if (path === route) return;
previous = route;
route.set(path);
}
export const goLayout = () => go("layout");
export const back = () => go(previous);
That is not boilerplate to apologise for: a click handler has to be a module-level
export, because the artifact imports it by name, and onClick={() => go("layout")}
at a call site would be a closure with nowhere to live.
navigate needs the matcher and a way to pass an argument to a compiled handler.
Until then the repetition is visible rather than hidden behind something that does not
work. History is currently one entry deep, by decision.
Types
import type { Args, Route, Router } from "dziry";
Args<P> maps a path's parameter names to strings — Args<"products/$id"> is
{ id: string }.