Docs

Routing

Register an Axum route, render a Page, and read request parameters through cx.

Open Markdown

Register a route

Start with an Axum Router. This route sends /shop/products/42 to the product handler:

Routes
use axum::{routing::get, Router};

let routes = Router::new()
    .route("/shop/products/:id", get(product));

The handler composes the document, shared shop layout, and selected Page:

Product handler
use proa::axum::RouteResponse;
use crate::{layouts::{RootDocument, ShopLayout}, pages::ProductPage};

async fn product() -> RouteResponse {
    RouteResponse::ssr(RootDocument {
        children: ShopLayout {
            children: ProductPage,
        },
    })
}

The file tree and explicitly composed layouts lead to one selected Page.

RootDocument and ShopLayout declare const ROLE: ComponentRole = ComponentRole::LAYOUT; ProductPage declares PAGE. See Layouts and Pages for their full implementations and document markup.

Install the router

Pass the router to FrameworkBuilder. Enable managed metadata with with_head:

App configuration
use proa::{HeadConfig, HeadUrlPolicy};
use proa::axum::FrameworkBuilder;

let head = HeadConfig::default()
    .with_urls(HeadUrlPolicy::new("https://acme.example", "/")?);

let app = FrameworkBuilder::new()
    .with_head(head)
    .with_dynamic_routes(routes)
    .build();

Use your configured public origin in place of https://acme.example. The framework makes Axum's matched path parameters available through cx before head and body rendering.

Read route parameters from cx

A segment beginning with : captures a path value:

RouteRequestRead through cx
/shop/products/:id/shop/products/42cx.param("id") → Some("42")
/blog/:slug/blog/red-shoescx.param("slug") → Some("red-shoes")

These are URL patterns, registered explicitly; Rust filenames organize the source code.

Here is the ProductPage used above. It reads the same id in its head and body:

src/pages/product.rs
use proa::{
    ComponentRole, DataLoader, SyncHeadOperation, WebContext, WebRenderSync,
    WriteError,
};
use proa::html_sync;

pub struct ProductPage;

impl<L: DataLoader> WebRenderSync<L> for ProductPage {
    const ROLE: ComponentRole = ComponentRole::PAGE;

    fn render_head(&self, cx: &mut WebContext<L>) -> impl SyncHeadOperation {
        let id = cx.param("id")
            .ok_or_else(|| WriteError::render("missing product id"))?;
        let title = format!("Product {id}");
        cx.head_mut().title(title)
    }

    fn render(self, cx: &mut WebContext<L>) -> Result<(), WriteError> {
        let params = cx.route_params().cloned().unwrap_or_default();
        let id = params.get("id")
            .ok_or_else(|| WriteError::render("missing product id"))?;

        html_sync! {
            <main data-product-page>
                <h1>"Product "{id}</h1>
            </main>
        }
        .render(cx)
    }
}

cx.param(name) returns Option<&str>. cx.route_params() exposes the shared collection. Clone that handle before rendering when the template needs to borrow a parameter while writing through cx; this does not copy parameter strings.

For /shop/products/42, ShopLayout's title_template("%s | Shop") formats the Page's dynamic title:

Selected output
<title>Product 42 | Shop</title>
<main data-product-page><h1>Product 42</h1></main>

The title goes in the document head and <main> in its body. Title templates work the same for :id and :slug: the Page supplies its title, often from a loader, and the nearest ancestor template formats it. A slug is not automatically turned into a display title. See Load metadata from data for an async example.

Query strings

Path captures and query strings are separate:

Inside a render method or head hook
let path = cx.request_location().map(|location| location.path());
let query = cx.request_location().and_then(|location| location.query());

For /shop/products/42?currency=usd, these return Some("/shop/products/42") and Some("currency=usd"). query() returns the raw query string. Use a query parser or Axum's typed Query extractor for decoded fields.

Add and nest routes

Chain .route(...) calls to add Pages. Use Axum's .nest(...) to share a URL prefix:

More routes
let shop = Router::new()
    .route("/", get(shop_index))
    .route("/products/:id", get(product));

let routes = Router::new()
    .route("/about", get(about))
    .nest("/shop", shop);

This registers /about, /shop, and /shop/products/:id; Axum maps the nested / route to the prefix itself. Each handler explicitly composes its layout and Page. URL nesting does not add HTML wrappers.

Route specifications

Use Proa's Route::endpoint when you also need route modes, static-path declarations, or route-specification metadata. It produces an Axum router you can merge with the rest of the app:

Route specification
use proa::axum::{Route, RouteMode};

let routes = Router::new().merge(
    Route::endpoint("/shop/products/:id", get(product))
        .with_route_mode(RouteMode::Server)
        .into_router(),
);

Server, Static, and ClientBrowser describe routes to tooling; they do not set component roles or compose layouts. The older Route::layout and Route::page APIs describe route metadata independently of render_head. See metadata migration when updating those documents.

Next steps

Search

Type at least 2 characters