@koppajs/koppajs-router
v0.1.2
Published
Reusable History API router primitives for KoppaJS applications.
Maintainers
Readme
Purpose
The package exists to provide a deterministic, route-config-driven router runtime for KoppaJS-style applications.
It is intentionally narrow:
- the package owns path normalization, route matching, redirect following, browser history updates, outlet rendering, metadata updates, active-link state, and scroll restoration
- the consuming application owns route content, custom element registration, page copy, analytics, and business-specific navigation rules
Repository Classification
repo_type: reusable packageruntime_responsibility: browser router runtimebuild_time_responsibility: TypeScript build, linting, unit tests, tarball smoke testing, and release validationui_surface: none inside the packagematurity_level: pre-1.0, contract-stabilizing
Installation
pnpm add @koppajs/koppajs-routernpm install @koppajs/koppajs-routerConsumer requirements:
- a browser environment with the DOM and History API
- an ESM-capable runtime or bundler
- consumer-owned registration of every referenced
componentTag title,description, andcomponentTagon every final renderable route
Local repository requirements:
- Node.js >= 22
- pnpm >= 10.17.1
Public Contract
The public surface is deliberately small.
Main runtime surface:
KoppajsRouterRouteDefinition,ResolvedRoute,RedirectedRoute, and related route typesnormalizePath(),normalizeHash(),normalizeBasePath()toHref()andfromLocationPathname()resolveRoute()andresolveRouteByName()setActiveRouteLinks()andsetDocumentDescription()DEFAULT_ROUTE_LINK_SELECTORDEFAULT_ROUTE_CHANGE_EVENT_NAMEKOPPAJS_ROUTE_CHANGE_EVENT
Runtime instance helpers:
router.resolve()/router.resolveByName()router.navigate()/router.navigateByName()router.hrefFor()router.getCurrentPath()/router.getCurrentRoute()
Contract constraints:
- flat and nested route definitions are both supported
- route names must be unique
- unmatched paths throw unless the consumer declares an explicit
path: "*"route - static segments outrank dynamic segments, and dynamic segments outrank
* - active-link matching is path-based and ignores query and hash state
- route records are treated as immutable input data
Ownership Boundaries
Package-owned concerns:
- route registry compilation and matching
- named-route resolution and param interpolation
- base-path translation between app paths and browser hrefs
- redirect following with bounded depth
- outlet rendering, title updates, description updates, and route-change events
- active-link synchronization and scroll handling
Consumer-owned concerns:
- route definitions and route metadata
- custom element implementations referenced by
componentTag - application copy and page composition
- analytics, business rules, and link exclusion policy
- browser-level end-to-end coverage in real application shells
Usage
import { KoppajsRouter, type RouteDefinition } from "@koppajs/koppajs-router";
const routes = [
{
path: "/",
name: "home",
title: "Home",
description: "Landing page",
componentTag: "home-page",
},
{
path: "/services",
name: "services",
title: "Services",
description: "Services overview",
componentTag: "services-page",
children: [
{
path: ":slug",
name: "service-detail",
title: "Service detail",
description: "Service detail page",
componentTag: "service-detail-page",
},
],
},
{
path: "/guides",
name: "guides",
redirectTo: {
name: "guides-introduction",
},
children: [
{
path: "introduction",
name: "guides-introduction",
title: "Introduction",
description: "Guide introduction",
componentTag: "guides-introduction-page",
},
],
},
{
path: "*",
name: "not-found",
title: "Not found",
description: "Missing page",
componentTag: "not-found-page",
},
] satisfies readonly RouteDefinition[];
const outlet = document.querySelector<HTMLElement>("#app-outlet");
if (!outlet) {
throw new Error("App outlet not found.");
}
const router = new KoppajsRouter({
routes,
outlet,
root: document,
basePath: import.meta.env.BASE_URL,
shouldSetActiveState: (link) => !link.classList.contains("nav-link--cta"),
});
router.init();
router.navigate({
name: "service-detail",
params: { slug: "accessibility-audit" },
query: { ref: "nav" },
hash: "contact-entry",
});Runtime Behavior
Startup flow:
- the router builds a route registry from the supplied route definitions
init()seeds a router-specific history-state key, attaches delegated click andpopstatelisteners, and starts route-link observation- the current browser location is resolved and rendered into the outlet
Navigation flow:
- a direct path or named target is resolved into a normalized
ResolvedRoute - the browser URL is translated through the configured
basePath - history state, outlet content, metadata, active links, and the route-change event are updated together
- scroll behavior is applied after render, including anchor navigation and saved-history restoration
Runtime Options
Browser seam options:
root,document,window
Routing and URL options:
basePathrouteChangeEventNamescrollBehavior
Active-link options:
linkSelectoractiveClassNameactiveAttributeNameshouldSetActiveState
Important nuance:
- the default delegated-link selector is
a[data-route] - active-link and click handling derive the route target from
data-routewhen present and otherwise fall back tohref - if you want href-only links to participate in delegated handling, provide a
selector such as
a[data-nav]ora[href^="/"]explicitly
Route Matching Rules
- static path segments win over dynamic
:paramsegments, even if declared later - dynamic
:paramsegments win over*catch-all routes - catch-all routes preserve the unmatched browser path in
pathandfullPath - unmatched paths never fall back to the first route silently
- redirects inherit params, query, and hash unless the redirect target replaces them
Build And Distribution
- source lives in
src/ pnpm run buildemits the publishable package todist/- the package manifest exports
dist/index.jsanddist/index.d.ts pnpm run check:packageverifies that manifest entrypoints and build output stay alignedpnpm run test:packagepacks the tarball, installs it into a clean temporary consumer, and imports the published entrypointpnpm run checkis the main local quality gatepnpm run validateis the CI and release validation contract; it runscheckplus the tarball-consumer smoke test
Local verification commands:
pnpm install
pnpm run check
pnpm run validateEcosystem Fit
This package is the routing runtime layer in the KoppaJS ecosystem.
koppajs-coreand application shells can depend on it without inheriting website-specific route content- consumer applications keep ownership of route tables, component registration, and integration-level browser behavior
- this repository intentionally does not include a demo app, Playwright suite, or UI surface of its own
Architecture & Governance
Project intent, contributor rules, and documentation contracts live in the local repo meta layer:
- AI_CONSTITUTION.md
- ARCHITECTURE.md
- DECISION_HIERARCHY.md
- DEVELOPMENT_RULES.md
- TESTING_STRATEGY.md
- RELEASE.md
- ROADMAP.md
- CHANGELOG.md
- CONTRIBUTING.md
- CODE_OF_CONDUCT.md
- docs/specs/README.md
- docs/specs/repository-documentation-contract.md
- docs/meta/README.md
- docs/quality/README.md
The file-shape contract for README.md, CHANGELOG.md, CODE_OF_CONDUCT.md, and CONTRIBUTING.md is defined in docs/specs/repository-documentation-contract.md.
Run the local document guard before committing:
pnpm run check:docsCommunity & Contribution
Issues and pull requests are welcome:
https://github.com/koppajs/koppajs-router/issues
Contributor workflow details live in CONTRIBUTING.md.
Community expectations live in CODE_OF_CONDUCT.md.
License
Apache License 2.0 — © 2026 KoppaJS, Bastian Bensch
