Routing
The whole site runs inside one server callback. Routes are declared
with WebRoute and matched over two passes: a scan pass records named
routes so links can be built by name, then a render pass matches the request URL and
renders the page.
The server callback
The entry point ends by calling NetworkWeb::Server with a single
callback. Everything the site serves happens inside that callback. The server reads
the request URL, splits it into path segments, and runs the callback:
NetworkWeb::Server(function() {
// declare and match routes here
});
Declaring routes with WebRoute
Routes are declared with WebRoute. WebRoute::Dir descends
into a path segment, WebRoute::File matches all the way to the end of the
path and renders, and WebRoute::String captures a segment as a value.
A named anchor is declared with WebRoute::Named, which lets a link be
built later by name instead of by hardcoding a path.
Routes are matched first come first served. The first route that matches wins, so
specific routes are declared before any catch-all. A bare
/<permalink> route that matches almost anything is declared last so
it never shadows the named routes above it.
Two passes
The same route definitions run in two modes. The scan pass walks every route with
WebRoute::Scan and records each WebRoute::Named anchor.
It renders nothing: terminals and captured parameters do nothing during a scan. After
the scan, every named link can be resolved with WebRoute::GetNamed, so a
page rendered later can link to a route by name.
The render pass then matches the request URL against the routes and renders the one page that is hit. Because the scan already recorded every name, links on that page resolve even to routes that were never visited by this request.
A small example
Modelled on the real entry point: a scan pass records the named routes, then a render pass serves the public site, the member front end, the API, the admin, and last of all the catch-all permalink route:
NetworkWeb::Server(function() {
// Pass 1 (scan): record named routes so links resolve by name.
WebRoute::Scan(function() {
ClipMember::Route();
ClipSite::RoutePermalink();
});
// Pass 2 (render): match the request URL and render the page.
ClipSite::Route();
ClipMember::Route();
Api::Route();
Admin::Route('/admin');
// LAST: bare "/<permalink>" is registered after every named route,
// so first-match-wins never lets it shadow them.
ClipSite::RoutePermalink();
});
Each Route method is a module declaring its own routes with
WebRoute. The site composes them by order: the more specific a route, the
earlier it is declared.
Routing is declarative
Routing is a good example of declarative code. A module does not inspect the request
and hand-match it; it declares the routes it owns with WebRoute and leaves
the matching to the framework. The site composes those declarations by order and the
two passes work out the rest. You state which routes exist, not how each request is
dispatched. See Declarative code for the same idea
applied to building pages.
Routing catches exceptions at the top
The render pass is another top-level place where exceptions are caught, which keeps
page code to a single happy path. A page assumes the conditions it needs and throws
when they are not met, rather than guarding every step. The common case is a member
page that requires a login: it throws MemberNotLoggedInError, and the top
of the render pass catches it and redirects to the login page with
Ui::OverrideRedirect(MemberUrl::Login()).
Because that catch lives at the top, the page itself never checks whether a member is logged in before doing its work. It writes what the page should show for a logged-in member and lets the throw handle the exception, in the same spirit as the Api handlers described in The Api layer.
Next: The Api layer.