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.
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 |
| Keep a scroll position across navigation | CSS overflow-anchor, or an island that restores it |
| 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:
npm install @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>
}
Without it, the opened page can reach back through window.opener and redirect your tab. See 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.