Docs
Linking and navigating
Navigate between Proa pages with anchors, optional prefetching, and Turbo Drive.
Use an anchor tag.
html_sync! {
<nav>
<a href="/blog">"Blog"</a>
<a href="/about">"About"</a>
</nav>
}
There is no <Link> component or client router to configure. Proa pages are server-rendered HTML, so normal document navigation is the baseline. Prefetching and Turbo Drive are optional enhancements to those same anchors; neither changes your routes.
Deployments under a path prefix
When serving behind a prefix-stripping proxy, configure a runtime base path and
use cx.url("/blog") for your anchors, forms, and asset attributes:
let blog = cx.url("/blog");
let about = cx.url("/about");
html_sync! {
<nav>
<a href={blog}>"Blog"</a>
<a href={about}>"About"</a>
</nav>
}
.render(cx)
cx.url(...) explicitly applies the configured base path and writes directly
into the output buffer without an intermediate String. Plain URL strings
stay unchanged. External URLs, protocol-relative URLs, document-relative paths,
queries, and fragments pass through the helper unchanged. Pass an unprefixed
application path so the base path is applied once.
This also works in streamed components. In an Axum handler, use
Redirect::to(&proa::axum::app_url("/").to_string()) for an
application-root redirect. See URL prefix deployments
for startup configuration and island asset serving.
Why there is no Link component
Client-side routing avoids a full document reload, but it does not eliminate network requests for server data. Proa keeps routing and rendering on the server, where a route can return useful HTML immediately.
A framework-specific link and router add a JavaScript bundle, a scroll and focus restoration model to get right, and a second source of truth for what page you are on.
A plain anchor has none of that, and it works before JavaScript loads, with JavaScript disabled, and in a crawler.
| You want | Do this |
|---|---|
| Navigate to another page | <a href="/path"> |
| Prefetch a likely next page | <link rel="prefetch" href="/path"> |
| Add SPA-like page transitions | Add Turbo Drive; keep the same anchors and routes |
| Restore scroll on back/forward navigation | Browser history restoration, or the navigation library's restoration policy |
| Show progress on a slow page | Stream it. See Streaming SSR |
| Update the URL without navigating | RSJS history helpers, below |
| Preserve state across a navigation | Reconsider, that state probably belongs in the URL |
Good to know: Prefetching and Turbo can make the transition feel faster, but they do not make slow server work disappear. Stream a slow section so the destination can return its shell immediately.
Prefetching a likely next page
For a high-confidence next destination, add a document prefetch hint to the current page's <head>:
html_sync! {
<html lang="en">
<head>
<link rel="prefetch" href="/checkout" />
</head>
<body>{content}</body>
</html>
}
This is a low-priority browser hint, not a Proa-specific navigation system. The browser may ignore it, and cache headers on the response affect whether the prefetched document can be reused. Use it for a small number of likely, same-site destinations rather than every link. A prefetch is a real GET request, so GET routes must remain safe and free of side effects.
SPA-like navigation with Turbo Drive
Turbo Drive is not bundled with Proa. It is a backend-agnostic progressive enhancement that works with Proa's existing HTML responses. It intercepts eligible same-origin links, fetches the destination document, replaces the <body>, merges the <head>, and updates browser history. The server still owns routing and renders every destination; if JavaScript is unavailable, the anchors continue to perform normal document navigation.
Install Turbo and import it once from your browser entry module:
pnpm add @hotwired/turbo
import "@hotwired/turbo";
Bundle that entry module with your application's JavaScript and load it from the document <head>. Importing Turbo starts Drive; Proa routes and anchor markup do not need to change.
Turbo 8 prefetches eligible links shortly after hover by default. You can preload a particularly important destination when the page loads, disable hover prefetch for an expensive destination, or opt a link out of Turbo entirely:
html_sync! {
<nav>
<a href="/dashboard" data-turbo-preload>"Dashboard"</a>
<a href="/reports" data-turbo-prefetch="false">"Reports"</a>
<a href="/downloads/archive.zip" data-turbo="false">"Download archive"</a>
</nav>
}
Turbo changes the page lifecycle, so custom browser code that runs after every navigation should listen for turbo:load instead of only DOMContentLoaded. Keep the application bundle in <head>. RSJS observes detached island roots and tears down their resources when Turbo replaces the body; the destination response's body scripts hydrate its new islands. Test any state you intentionally preserve across forward, back, and cached Turbo visits. See Turbo's installation and application lifecycle guidance for the integration details.
Updating the URL from an island
Filtering, sorting, and tab state belong in the URL so they survive a reload and a shared link. RSJS exposes the browser history API for exactly this:
use rsjs::{event_handler, rsjs};
let sort_ascending = event_handler(|_: rsjs::MouseEvent<rsjs::elem::Button>| {
rsjs::history::push_state("?sort=asc");
});
| Helper | Effect |
|---|---|
history::push_state(url) | Adds a history entry. The user can go back |
history::replace_state(url) | Replaces the current entry. No back step |
history::back() / history::forward() | Moves through the stack |
history::go(delta) | Jumps by a relative offset |
Use push_state when the change is a place the user might want to return to, and replace_state when it is a correction to where they already are.
None of these fetch anything. They update the address bar; the page keeps whatever the server already rendered. To reflect new data, refetch it with a query.
Linking to another origin
Add rel="noopener noreferrer" whenever you open a new tab:
html_sync! {
<a href="https://example.com" target="_blank" rel="noopener noreferrer">"Docs"</a>
}
Modern browsers imply noopener for target="_blank". Writing it explicitly
keeps the policy visible; noreferrer also suppresses the Referer header, which
you can omit when referral information is wanted. See MDN: noopener
and Escaping and raw HTML.
Next steps
- Routes and layouts
- Define route trees, layouts, and static paths.
- Streaming SSR
- Flush the page shell first and stream slow sections as they resolve.
- Queries and mutations
- Seed browser queries from server-rendered data, refetch them, and run invalidating mutations.
- Browser APIs
- Reach browser capabilities through the analyzed RSJS surface.