Proute
Proute generates Elm Land-inspired routing code for Lustre apps, including SPAs and server components, from Gleam page file paths.
The source of truth is the page tree:
src/public/pages/home_.gleam
src/public/pages/games.gleam
src/public/pages/games/id_.gleam
src/public/pages/teams/slug_.gleam
Mounts are derived from config. output_root defaults to
src/generated/proute, so apps only set it when they want a different generated
module subtree:
[proute]
pages_root = "src/server"
[[proute.mounts]]
name = "public"
route_root = "/"
[[proute.mounts]]
name = "admin"
Each mount gets a conventional page shared-state type. For the example above,
public defaults to public/page_shared_state.PublicPageSharedState and
admin defaults to admin/page_shared_state.AdminPageSharedState. Set
page_shared_state_type on a mount only when the app keeps that type somewhere
else.
That config resolves to:
public pages = src/server/public/pages
public routes = src/generated/proute/public/routes.gleam
public glue = src/generated/proute/public/pages.gleam
public input = src/generated/proute/public/page_input.gleam
public root = /
admin pages = src/server/admin/pages
admin routes = src/generated/proute/admin/routes.gleam
admin glue = src/generated/proute/admin/pages.gleam
admin input = src/generated/proute/admin/page_input.gleam
admin root = /admin
Generated route modules expose a route type, path parser, path builders, absolute URL builders with an explicit origin, and route-specific helpers:
pub type Route {
Home
Games
GamesId(id: String)
TeamsSlug(slug: String)
NotFound
}
pub fn parse_path(path: String) -> Route
pub fn route_to_path(route: Route) -> String
pub fn route_to_url(route route: Route, origin origin: String) -> String
pub fn games_id_path(id id: String) -> String
Example
Rally Scoreboard is the canonical example for
Proute in a Rally app. It uses Proute-generated public and admin mounts under
src/generated/proute/**, with page shared state, generated route params, and
Rally-generated load, SSR, browser, and broadcast glue layered on top.
Inspiration
Proute is inspired by Elm Land-style file routes and by sporto/gleam-roundabout. Roundabout’s generated path helpers and validation discipline are especially good ideas. Proute keeps a different source of truth: page files instead of a route DSL.
Elm Land’s page conventions are the main
routing inspiration: page files map to URL paths, trailing underscores mark
dynamic routes, home_ represents the mount root, and not_found_ reserves the
custom 404 page. all_ is reserved for future catch-all routes, but it is not
generated yet. Elm Land's generated
Route helpers also shape the goal:
removed pages should break callers at compile time.
Page modules must expose the conventional Lustre API that generated
pages.gleam calls:
pub type Model
pub type Message
pub fn initial_model(page_shared_state, query_params) -> Model
pub fn update(model, msg) -> #(Model, Effect(Message))
pub fn view(model) -> Element(Message)
Dynamic routes receive generated route params between page shared state and query params:
pub fn initial_model(page_shared_state, route_params, query_params) -> Model
Pages may also expose init with the same inputs when they need page-specific
startup effects:
pub fn init(page_shared_state, query_params) -> #(Model, Effect(Message))
pub fn init(page_shared_state, route_params, query_params) -> #(Model, Effect(Message))
Use init for client-side escape hatches such as browser APIs, local storage,
focus, measurement, or page-local event listeners. Most Rally pages should omit
it and let generated load glue layer data onto initial_model.
Pages that need app dependencies during update may use:
pub fn update(page_shared_state, model, msg) -> #(Model, Effect(Message))
Generated-code snapshot tests are part of the intended implementation approach. They make the generated API reviewable and keep accidental output churn visible.