URL rewrites allow you to transform URLs bidirectionally between what the browser displays and what the router interprets internally. This powerful feature enables patterns like locale prefixes, subdomain routing, legacy URL migration, and multi-tenant applications without duplicating routes or complicating your route tree.
URL rewrites are useful when you need to:
URL rewrites operate in two directions:
┌─────────────────────────────────────────────────────────────────┐
│ Browser URL Bar │
│ /en/about?q=test │
└─────────────────────────┬───────────────────────────────────────┘
│
▼ input rewrite
┌─────────────────────────────────────────────────────────────────┐
│ Router Internal URL │
│ /about?q=test │
│ │
│ (matches routes, runs loaders) │
└─────────────────────────┬───────────────────────────────────────┘
│
▼ output rewrite
┌─────────────────────────────────────────────────────────────────┐
│ Browser URL Bar │
│ /en/about?q=test │
└─────────────────────────────────────────────────────────────────┘
The router exposes two href properties on the location object:
Configure rewrites when creating your router:
import { createRouter } from '@tanstack/react-router'
const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => {
// Transform browser URL → router internal URL
// Return the modified URL, a new URL, or undefined to skip
return url
},
output: ({ url }) => {
// Transform router internal URL → browser URL
// Return the modified URL, a new URL, or undefined to skip
return url
},
},
})
The input and output functions receive a URL object and can:
Strip locale prefixes on input and add them back on output:
const locales = ['en', 'fr', 'es', 'de']
const defaultLocale = 'en'
// Get current locale (from cookie, localStorage, or detection)
function getLocale() {
return localStorage.getItem('locale') || defaultLocale
}
const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => {
// Check if pathname starts with a locale prefix
const segments = url.pathname.split('/').filter(Boolean)
const firstSegment = segments[0]
if (firstSegment && locales.includes(firstSegment)) {
// Strip the locale prefix: /en/about → /about
url.pathname = '/' + segments.slice(1).join('/') || '/'
}
return url
},
output: ({ url }) => {
const locale = getLocale()
// Add locale prefix: /about → /en/about
if (locale !== defaultLocale || true) {
// Always prefix, or conditionally skip default locale
url.pathname = `/${locale}${url.pathname === '/' ? '' : url.pathname}`
}
return url
},
},
})
For production i18n, consider using a library like Paraglide that provides localizeUrl and deLocalizeUrl functions. See the Internationalization guide for integration details.
Route subdomain requests to path-based routes:
const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => {
const subdomain = url.hostname.split('.')[0]
// admin.example.com/users → /admin/users
if (subdomain === 'admin') {
url.pathname = '/admin' + url.pathname
}
// api.example.com/v1/users → /api/v1/users
else if (subdomain === 'api') {
url.pathname = '/api' + url.pathname
}
return url
},
output: ({ url }) => {
// Reverse the transformation for link generation
if (url.pathname.startsWith('/admin')) {
url.hostname = 'admin.example.com'
url.pathname = url.pathname.replace(/^\/admin/, '') || '/'
} else if (url.pathname.startsWith('/api')) {
url.hostname = 'api.example.com'
url.pathname = url.pathname.replace(/^\/api/, '') || '/'
}
return url
},
},
})
Support old URLs while maintaining new route structure:
const legacyPaths: Record<string, string> = {
'/old-about': '/about',
'/old-contact': '/contact',
'/blog-posts': '/blog',
'/user-profile': '/account/profile',
}
const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => {
const newPath = legacyPaths[url.pathname]
if (newPath) {
url.pathname = newPath
}
return url
},
// No output rewrite needed - new URLs will be used going forward
},
})
Route tenant-specific domains to a unified route structure:
const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => {
// Extract tenant from subdomain: acme.app.com → acme
const parts = url.hostname.split('.')
if (parts.length >= 3) {
const tenant = parts[0]
// Inject tenant into the path: /dashboard → /tenant/acme/dashboard
url.pathname = `/tenant/${tenant}${url.pathname}`
}
return url
},
output: ({ url }) => {
// Extract tenant from path and move to subdomain
const match = url.pathname.match(/^\/tenant\/([^/]+)(.*)$/)
if (match) {
const [, tenant, rest] = match
url.hostname = `${tenant}.app.com`
url.pathname = rest || '/'
}
return url
},
},
})
Transform search parameters during rewrites:
const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => {
// Convert legacy search param format
// ?filter_status=active → ?status=active
const filterStatus = url.searchParams.get('filter_status')
if (filterStatus) {
url.searchParams.delete('filter_status')
url.searchParams.set('status', filterStatus)
}
return url
},
output: ({ url }) => {
// Optionally transform back for external display
return url
},
},
})
When you need multiple independent rewrite transformations, use composeRewrites to combine them:
import { composeRewrites } from '@tanstack/react-router'
const localeRewrite = {
input: ({ url }) => {
// Strip locale prefix
const match = url.pathname.match(/^\/(en|fr|es)(\/.*)$/)
if (match) {
url.pathname = match[2] || '/'
}
return url
},
output: ({ url }) => {
// Add locale prefix
url.pathname = `/en${url.pathname === '/' ? '' : url.pathname}`
return url
},
}
const legacyRewrite = {
input: ({ url }) => {
if (url.pathname === '/old-page') {
url.pathname = '/new-page'
}
return url
},
}
const router = createRouter({
routeTree,
rewrite: composeRewrites([localeRewrite, legacyRewrite]),
})
Order of operations:
This ensures that composed rewrites "unwrap" correctly. In the example above:
When you configure a basepath, the router internally implements it as a rewrite. If you also provide a custom rewrite, they are automatically composed together:
const router = createRouter({
routeTree,
basepath: '/app',
rewrite: {
input: ({ url }) => {
// This runs AFTER basepath is stripped
// Browser: /app/en/about → After basepath: /en/about → Your rewrite: /about
return url
},
output: ({ url }) => {
// This runs BEFORE basepath is added
// Your rewrite: /about → After your rewrite: /en/about → Basepath adds: /app/en/about
return url
},
},
})
The composition order ensures:
The <Link> component automatically applies output rewrites when generating href attributes:
// With locale rewrite configured (adds /en prefix)
<Link to="/about">About</Link>
// Renders: <a href="/en/about">About</a>
Programmatic navigation via navigate() or router.navigate() also respects rewrites:
const navigate = useNavigate()
// Navigates to /about internally, displays /en/about in browser
navigate({ to: '/about' })
When an output rewrite changes the origin (hostname), the <Link> component automatically renders a standard anchor tag instead of using client-side navigation:
// Rewrite that changes hostname for /admin paths
const router = createRouter({
routeTree,
rewrite: {
output: ({ url }) => {
if (url.pathname.startsWith('/admin')) {
url.hostname = 'admin.example.com'
url.pathname = url.pathname.replace(/^\/admin/, '') || '/'
}
return url
},
},
})
// This link will be a hard navigation (full page load)
<Link to="/admin/dashboard">Admin Dashboard</Link>
// Renders: <a href="https://admin.example.com/dashboard">Admin Dashboard</a>
The router's location object includes a publicHref property that contains the external URL (after output rewrite):
function MyComponent() {
const location = useLocation()
// Internal URL used for routing
console.log(location.href) // "/about"
// External URL shown in browser
console.log(location.publicHref) // "/en/about"
return (
<div>
{/* Use publicHref for sharing, canonical URLs, etc. */}
<ShareButton url={window.location.origin + location.publicHref} />
</div>
)
}
Use publicHref when you need the actual browser URL for:
URL rewrites apply on both client and server. When using TanStack Start:
Rewrites are applied when parsing incoming requests:
// router.tsx
export const router = createRouter({
routeTree,
rewrite: {
input: ({ url }) => deLocalizeUrl(url),
output: ({ url }) => localizeUrl(url),
},
})
The server handler will use the same rewrite configuration to parse incoming URLs and generate responses with the correct external URLs.
The router ensures that the server-rendered HTML and client hydration use consistent URLs. The publicHref is serialized during SSR so the client can hydrate with the correct external URL.
type LocationRewrite = {
/**
* Transform the URL before the router interprets it.
* Called when reading from browser history.
*/
input?: LocationRewriteFunction
/**
* Transform the URL before it's written to browser history.
* Called when generating links and committing navigation.
*/
output?: LocationRewriteFunction
}
type LocationRewriteFunction = (opts: { url: URL }) => undefined | string | URL
Parameters:
Returns:
import { composeRewrites } from '@tanstack/react-router'
function composeRewrites(rewrites: Array<LocationRewrite>): LocationRewrite
Combines multiple rewrite pairs into a single rewrite. Input rewrites execute in order, output rewrites execute in reverse order.
Example:
const composedRewrite = composeRewrites([
{ input: rewrite1Input, output: rewrite1Output },
{ input: rewrite2Input, output: rewrite2Output },
])
// Input execution order: rewrite1Input → rewrite2Input
// Output execution order: rewrite2Output → rewrite1Output
Complete working examples are available in the TanStack Router repository: