Docs

Linking and navigating

Navigate between Proa pages with anchors, optional prefetching, and Turbo Drive.

Open Markdown

Use an anchor tag.

src/components/nav.rs
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.

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 wantDo this
Navigate to another page<a href="/path">
Prefetch a likely next page<link rel="prefetch" href="/path">
Add SPA-like page transitionsAdd Turbo Drive; keep the same anchors and routes
Keep a scroll position across navigationCSS overflow-anchor, or an island that restores it
Show progress on a slow pageStream it. See Streaming SSR
Update the URL without navigatingRSJS history helpers, below
Preserve state across a navigationReconsider, 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>:

src/app/layout.rs
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
src/client.js
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:

src/components/nav.rs
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:

src/components/sort_products.rs
use rsjs::{event_handler, rsjs};

let sort_ascending = event_handler(|_: rsjs::MouseEvent<rsjs::elem::Button>| {
    rsjs::history::push_state("?sort=asc");
});
HelperEffect
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:

src/components/external.rs
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

Search

Type at least 2 characters