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.

Search Document