Docs
Route responses
Return synchronous, buffered async, or streaming SSR.
Handlers return RouteResponse when they want Proa's framework finalizer to render a component and finalize the document.
Use ordinary Axum responses for JSON APIs, form handlers, early status codes, redirects, and handler-level errors. See Route handlers for endpoint patterns and Status, redirects, and errors for 404 pages, response headers, streaming constraints, and public error bodies.
Synchronous SSR
Use RouteResponse::ssr for ordinary WebRenderSync components.
use proa::axum::RouteResponse;
pub async fn handler() -> RouteResponse {
RouteResponse::ssr(HomePage)
}
Write ordinary page components as WebRenderSync<L>; the framework supplies
the owned RenderBuffer response writer.
Async SSR
Use RouteResponse::ssr_async for components that implement WebRender and do not use a loader. The full render completes before the response begins, so root errors, status, and headers remain available to the HTTP layer.
pub async fn handler() -> RouteResponse {
RouteResponse::ssr_async(PageWithAwait)
}
For typed data loaders, return impl IntoResponse and pass the loader explicitly:
use std::sync::Arc;
pub async fn handler() -> impl axum::response::IntoResponse {
let loader = Arc::new(MyLoader::new());
RouteResponse::ssr_async_with_loader(Page, loader)
}
Prefer typed loaders when the concrete loader type is known. Use the *_dyn constructors only at real dynamic boundaries.
See Data loading and streaming for loader keys, cache hints, request-local dedupe, and preloading.
Streaming SSR
Use ssr_stream or ssr_stream_with_loader when the page should flush work incrementally.
pub async fn handler() -> impl axum::response::IntoResponse {
let loader = Arc::new(MyLoader::new());
RouteResponse::ssr_stream_with_loader(StreamedPage, loader)
}
Streaming responses default to out-of-order chunked rendering. Use with_render_mode when a route needs a different render mode.
Use Data loading and streaming for Suspense boundaries, PreparedLoader, and streamed data patterns.
RouteResponse::ssr_stream* is the HTML response surface. For generated text/markdown responses, use proa::stream::render_md_in_order_to_flusher or InOrderMarkdownDriver from a custom handler/adapter and set the response content type yourself. See Streaming Markdown Output.
Responses outside the render pipeline
Already-rendered HTML is an ordinary HTTP response, not a render plan. Return Axum's Html response directly:
use axum::response::Html;
pub async fn handler() -> Html<&'static str> {
Html("<main>Already rendered</main>")
}
If a compile-time HTML fragment must participate in Proa finalization or browser navigation, treat it as a renderable node instead of a response kind:
RouteResponse::ssr(proa::static_html(b"<main>Static fragment</main>"))
Serve browser-rendered applications through Axum file services and configure their fallback routing explicitly. RouteResponse handles deferred server rendering.
Rendering without a server
RouteResponse::render_to_bytes finalizes a response into one HTML document with no HTTP server, no island manifest, and no async runtime. It applies the same managed head and RSJS runtime tags the Axum adapter would, so static exports, snapshot tests, and plain fn main programs render the exact bytes a route serves.
use proa::HeadConfig;
use proa::framework::RouteResponse;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let html = RouteResponse::ssr(RootDocument { children: HomePage })
.with_head(HeadConfig::default())
.render_to_bytes()?;
std::fs::write("./output/index.html", html)?;
Ok(())
}
render_to_string returns the same document as a String, and the proa::axum::RouteResponse wrapper exposes both methods. What differs from a served request:
- Async plans render buffered.
Suspensecontent appears inline rather than as a fallback plus a later swap, and the streaming driver is never involved. - There is no request. The base path is the origin root, route parameters are absent, and
render_metadata_headresolves to nothing. Pages that read those should go through the built router withtower::ServiceExt::oneshot. - Headers are discarded, and a status outside 2xx is returned as
FrameworkError::StaticStatusinstead of being written out as a page. - Call it from synchronous code. Inside async code, await
into_framework_response; an async plan called from a current-thread tokio runtime returnsFrameworkError::StaticRenderInsideRuntimerather than deadlocking.
Programs that only render this way can depend on proa_framework_core with default-features = false, which leaves tokio out of the build. Eager proa_stream preloads need a tokio runtime and are the one feature that does not work there.