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:
i18n locale prefixes: Display /en/about in the browser but route to /about internally
Subdomain routing: Route admin.example.com/users to /admin/users internally
Legacy URL migration: Support old URLs like /old-path that map to new routes
Multi-tenant applications: Route tenant1.example.com to tenant-specific routes
Custom URL schemes: Transform any URL pattern to match your route structure
URL rewrites operate in two directions:
Input rewrite: Transforms the URL from the browser before the router interprets it
Output rewrite: Transforms the URL from the router before it's written to the browser
┌─────────────────────────────────────────────────────────────────┐
│ 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:
location.href - The internal URL (after input rewrite)
location.publicHref - The external URL displayed in the browser (after output rewrite)
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:
Mutate and return the same url object
Return a new URL instance
Return a full href string (will be parsed into a URL)
Return undefined to skip the rewrite
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:
Input rewrites: Execute in order (first to last)
Output rewrites: Execute in reverse order (last to first)
This ensures that composed rewrites "unwrap" correctly. In the example above:
Input: locale strips /en, then legacy redirects /old-page
Output: legacy runs first (no-op), then locale adds /en back
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:
Input: Basepath stripped first, then your rewrite runs
Output: Your rewrite runs first, then basepath added
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:
Social sharing
Canonical URLs
Analytics tracking
Copying links to clipboard
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
Optional
Configures bidirectional URL transformation between browser and router.
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 | URLParameters:
url: A URL object representing the current URL
Returns:
URL: The transformed URL object (can be the same mutated object or a new instance)
string: A full href string that will be parsed into a URL
undefined: Skip the rewrite, use the original URL
import { composeRewrites } from '@tanstack/react-router'
function composeRewrites(rewrites: Array<LocationRewrite>): LocationRewriteCombines 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 → rewrite1OutputComplete working examples are available in the TanStack Router repository: