---
title: Linking and navigating
description: Navigate between Proa pages with anchors, optional prefetching, and Turbo Drive.
---

Use an anchor tag.

```rust filename="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.

## 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:

```rust filename="src/components/nav.rs"
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](/docs/framework/deployment#serving-behind-a-url-path-prefix)
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](/docs/core/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>`:

```rust filename="src/app/layout.rs" {4}
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](https://turbo.hotwired.dev/handbook/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:

```shell
pnpm add @hotwired/turbo
```

```javascript filename="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:

```rust filename="src/components/nav.rs" {3-5}
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](https://turbo.hotwired.dev/handbook/installing) and [application lifecycle](https://turbo.hotwired.dev/handbook/building) 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:

```rust filename="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");
});
```

| 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](/docs/rsjs/queries-mutations).

## Linking to another origin

Add `rel="noopener noreferrer"` whenever you open a new tab:

```rust filename="src/components/external.rs"
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](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/noopener)
and [Escaping and raw HTML](/docs/core/escaping).

## Next steps

- [Routes and layouts](/docs/framework/routing)
  - Define route trees, layouts, and static paths.
- [Streaming SSR](/docs/core/streaming-ssr)
  - Flush the page shell first and stream slow sections as they resolve.
- [Queries and mutations](/docs/rsjs/queries-mutations)
  - Seed browser queries from server-rendered data, refetch them, and run invalidating mutations.
- [Browser APIs](/docs/rsjs/browser-apis)
  - Reach browser capabilities through the analyzed RSJS surface.
