Skip to main content

README.md

# 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:

```text
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:

```toml
[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:

```text
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:

```gleam
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](https://github.com/pairshaped/rally-scoreboard-example) 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](https://github.com/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](https://elm.land/concepts/pages.html) 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](https://elm.land/concepts/route.html) 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:

```gleam
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:

```gleam
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:

```gleam
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:

```gleam
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.