Routing

Vibe has no opinion on routing. It's a view layer — URLs belong to you, your server, or whatever router you like. The one exception: turning on SPA mode in the compiler, which has to own navigation to work — and then it uses Vibe's built-in router.

That gives you exactly three setups, from zero machinery to fully generated:

Rule of thumb: start with none. Flip "spa": true the day you want an SPA. Reach for the by-hand router only when you need routes or navigation behavior the compiler wouldn't derive.

1. No router — URLs from the file tree

A multi-page Vibe app doesn't route on the client at all. The pages/ tree is the URL space, and every link is a normal browser navigation:

These conventions are the whole story in MPA mode — and they're the same conventions SPA mode reads later, which is why switching modes never touches your pages.

2. Vibe's router, by hand

@ape-egg/vibe/spa is a small standalone module: a route table, setupSpa() to claim link clicks and history, and a reactive $.page your markup can bind — most importantly as a component outlet:

The router claims same-origin, unmodified, untargeted link clicks whose pathname matches a real route — pushState, a fresh $.page ({ path, route, params, src, title }), a document.title swap. Everything else navigates natively: other origins, modified clicks, download links, hash anchors, and unrouted paths. Routed and unrouted pages coexist in one app.

resolve() is pure — it maps anything with a pathname against the table and returns the page object or null. A '*' entry is the declared no-match fallback for deep links and popstate; it never claims a click. With no match and no '*', $.page.src stays unset and the outlet mounts nothing — there is no built-in 404. And with a custom onNavigate instead of an outlet, it's a pure router with no Vibe at all.

3. "spa": true — generated

The compiler flag composes option 2 for you: it reads the same file-tree conventions from option 1, generates the route table (most-specific-first, page titles harvested), compiles each page into a lazy-loaded fragment, and emits a shell that boots the built-in router against them. Your pages don't change; your links don't change; there is no routing code to write or maintain.

Everything about that mode — fragments, the persistent layout, how navigation commits — lives on SPA / MPA.