ADR-0001 · No optional params
Reject [id?]. Use <router-slot> with default content in the parent route instead. A "missing" param has an implicit default that leaks routing into components.
emroute is built on a single conviction: the platform is the framework. Where other routers wrap browser primitives in abstractions — VDOM, hooks, signals, JSX — emroute leans directly on URLPattern, Custom Elements, the Navigation API, setHTMLUnsafe, adoptedStyleSheets, @scope, @layer.
The filesystem is the route config. Content is the page. One component serves browsers, LLMs, and curl. Concerns the router doesn't own — caching, optional params, script execution, CSS encapsulation — are pushed back to where they belong. When abstraction is unavoidable, it picks one rather than two. The result is a framework whose "magic" is just standards a developer can recognize.
Reject [id?]. Use <router-slot> with default content in the parent route instead. A "missing" param has an implicit default that leaks routing into components.
A flat file matches exactly. A directory index becomes a catch-all. Reuse the existing flat-vs-directory distinction rather than inventing [...rest].
Every route renders three ways from one component — /app/, /html/, /md/. Browsers, LLMs, and CLIs each get the format they want, with no duplication.
Routes are defined by filesystem convention. [id] → :id, directory structure → hierarchy, .ts > .html > .md precedence. Adding a route means adding a file.
Widget extends Component. One class hierarchy, one custom element implementation, one rendering pipeline. The only difference is audience and tag prefix.
URLPattern, Custom Elements, Navigation API, fetch, AbortController, template literals. No VDOM, no JSX, no framework. Works in Deno, Node, Bun, any browser.
A page can be just .page.md with no JS. The default component provides a fallback chain so static content needs zero code. Escalate to .page.ts when you need logic.
The router does not cache getData() results. Caching is a state-management concern, not a routing concern — components own their data lifecycle.
<script>Don't auto-activate <script> tags injected via innerHTML. Per spec they don't run, and widgets already provide a clean activation path.
Widget params are plain HTML attributes (<widget-counter start="0">), kebab→camelCase, each value JSON.parse'd with string fallback. Not verbose data-params='{...}'.
Components render into light DOM. SSR resolves widgets server-side by calling their getData() + render methods. /md/ routes get fully-rendered widgets with zero JS.
Drop the SpaMode enum from the router. Server behavior, bundling, and routing scope are orthogonal concerns. Conventions over configuration: the filesystem determines the archetype.
Two entry points for one overlay system: declarative commandfor/command plus popover/<dialog> (zero JS), and a programmatic OverlayService for dynamic cases.
Use the Navigation API exclusively. One navigate event replaces ~50 lines of click interception, composedPath traversal, and popstate handling.
setHTMLUnsafeReplace innerHTML with setHTMLUnsafe() for SPA slot content and widget shadow roots. Parses Declarative Shadow DOM templates and accepts TrustedHTML for future CSP.
Reject CSS Houdini APIs. The production-ready ones don't fix emroute's real CSS pain points; the useful ones are Chromium-only. Lean on @scope, @layer, :defined instead.
Publish to npm, target Bun as primary runtime. JSR's design (publish-time graph freezing, no peer deps) is structurally incompatible with what a framework needs.
Developer-only components live in widgets/ with private: true, hidden from CMS enumeration but sharing the full widget pipeline. elements/ shrinks to plain HTMLElement escape hatch.
Use adoptedStyleSheets for widget CSS, wrapped in @layer emroute. Stylesheets survive setHTMLUnsafe() re-renders; one sheet object serves N widget instances.
Living document tracking newer browser APIs to adopt selectively — container queries (shipped in 1.11.0), :has(), AbortSignal.timeout(), CloseWatcher, CSS anchor positioning.
Client-side navigation replaces the entire routed subtree, root through leaf — even unchanged layout levels. No partial re-render, no router-tracked DOM identity. Widgets survive via two existing mechanisms: internal state never reaches the router, and shell-level placement survives real route changes.
<router-slot>The shell emits <router-slot> before the import map and app script, not after — so SSR content reaches the parser and paints before any script processing pauses it. Inline <script type="module"> in page content can't use bare specifiers; widgets and custom elements are unaffected.
anchor attribute and :target highlightingHighlighting a widget is native browser behavior, not a JS API. An anchor="foo" attribute renders as id="anchor-foo"; navigating to #anchor-foo scrolls it into view and :target styles it — zero JavaScript, works in every SPA mode including none.
Each decision links to its full record. The originals live in doc/architecture/ on GitHub.