Navigation
Link between pages with the router's own Link, let VitNode write the locale prefix, and navigate from code after a form submits.
Inside the app, use TanStack Router's Link. It builds the href, applies the
locale prefix, and preloads the destination on hover. You write the logical
path; the router writes the language.
Example
import { Link } from '@tanstack/react-router'
const DiscoverLink = () => <Link to="/discover">Discover</Link>That renders /discover for an English reader and /pl/discover for a Polish
one. Same component, same to, no branch and no useLocale() call.
One route, two URLs
/discover and /pl/discover are one route. The router's rewrite is what
makes that work, and it runs in both directions:
| Stage | Value |
|---|---|
| The address bar | /pl/discover |
What the route tree matches (rewrite.input) | /discover |
| What you write | to="/discover" |
What React renders (rewrite.output) | /pl/discover |
input is why no route file anywhere in the app mentions a locale - there is no
routes/pl/ directory and there is not going to be one. output reads the
locale off the router's own current location rather than off window, so the
href rendered during SSR is byte-identical to the one rendered after hydration.
So write the logical path. Two things that look reasonable and are not:
- Building the prefix yourself.
to={`/${locale}/discover`}does not double up, becauselocalizeUrlde-localizes before it prefixes and is therefore idempotent. What it does instead is discard the prefix you wrote and re-apply the reader's current locale, so an English reader following your/pl/discoverlink lands on/discover. Not honoured; overwritten. - Naming a prefixed route.
to="/pl/discover"is not a path in the route tree, so the router's typedtorefuses it.
`/admin` and `/api` carry no prefix at all
Both sit outside the localized URL space, so nothing is stripped from them and
nothing is added. /pl/admin/core is therefore a mistake rather than a Polish
page, and the app's request middleware 308-redirects it to /admin/core while
storing pl in the language cookie - so the AdminCP still renders in the
language the visitor just asked for. The same middleware canonicalises
/en/discover to /discover, because the default locale is unprefixed and
two indexable URLs for one page is one too many.
An unknown prefix is a 404, not a fallback
Only a prefix the app would itself emit gets stripped, so /xx/discover
reaches the route tree intact and matches nothing. That is deliberate: a
silent fallback would serve the same page at infinitely many URLs.
Update query state from a plugin page
Plugin route props expose a narrow navigate function for filters, sorting, and
pagination on the page already being viewed. It keeps the plugin independent of
the host router’s entire route tree.
import type { PluginRoutePageProps } from '@vitnode/core/routing'
interface CatalogSearch {
page: number
}
const CatalogPage = ({
navigate,
search,
}: PluginRoutePageProps<undefined, CatalogSearch>) => (
<button
onClick={() =>
void navigate({
resetScroll: false,
search: { page: search.page + 1 },
})
}
type="button"
>
Next page
</button>
)
export default CatalogPagePlugin route modules
A plugin route should not import the host router. Use its navigate prop for
same-page filters, sort order, and pagination. For links to another internal
screen, let the host render a link component; a plain <a> is for another
origin only.
Keep plugins portable
navigate only changes this plugin page's query string. That small boundary
is intentional—and saves future hosts from router spaghetti.