diff --git a/e2e/react-router/basic-file-based/src/routeTree.gen.ts b/e2e/react-router/basic-file-based/src/routeTree.gen.ts
index 28197e4d98..d8f74a9b0f 100644
--- a/e2e/react-router/basic-file-based/src/routeTree.gen.ts
+++ b/e2e/react-router/basic-file-based/src/routeTree.gen.ts
@@ -33,6 +33,7 @@ import { Route as StructuralSharingEnabledRouteImport } from './routes/structura
import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default'
import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
+import { Route as PipeReferenceRouteImport } from './routes/pipe.$reference'
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
import { Route as groupLazyinsideRouteImport } from './routes/(group)/lazyinside'
import { Route as groupInsideRouteImport } from './routes/(group)/inside'
@@ -239,6 +240,11 @@ const PostsPostIdRoute = PostsPostIdRouteImport.update({
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
+const PipeReferenceRoute = PipeReferenceRouteImport.update({
+ id: '/pipe/$reference',
+ path: '/pipe/$reference',
+ getParentRoute: () => rootRouteImport,
+} as any)
const LayoutLayout2Route = LayoutLayout2RouteImport.update({
id: '/_layout-2',
getParentRoute: () => LayoutRoute,
@@ -726,6 +732,7 @@ export interface FileRoutesByFullPath {
'/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
'/inside': typeof groupInsideRoute
'/lazyinside': typeof groupLazyinsideRoute
+ '/pipe/$reference': typeof PipeReferenceRoute
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
'/search-params/default': typeof SearchParamsDefaultRoute
@@ -830,6 +837,7 @@ export interface FileRoutesByTo {
'/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute
'/inside': typeof groupInsideRoute
'/lazyinside': typeof groupLazyinsideRoute
+ '/pipe/$reference': typeof PipeReferenceRoute
'/posts/$postId': typeof PostsPostIdRoute
'/search-params/default': typeof SearchParamsDefaultRoute
'/structural-sharing/$enabled': typeof StructuralSharingEnabledRoute
@@ -932,6 +940,7 @@ export interface FileRoutesById {
'/(group)/inside': typeof groupInsideRoute
'/(group)/lazyinside': typeof groupLazyinsideRoute
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
+ '/pipe/$reference': typeof PipeReferenceRoute
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
'/search-params/default': typeof SearchParamsDefaultRoute
@@ -1040,6 +1049,7 @@ export interface FileRouteTypes {
| '/onlyrouteinside'
| '/inside'
| '/lazyinside'
+ | '/pipe/$reference'
| '/posts/$postId'
| '/redirect/$target'
| '/search-params/default'
@@ -1144,6 +1154,7 @@ export interface FileRouteTypes {
| '/onlyrouteinside'
| '/inside'
| '/lazyinside'
+ | '/pipe/$reference'
| '/posts/$postId'
| '/search-params/default'
| '/structural-sharing/$enabled'
@@ -1245,6 +1256,7 @@ export interface FileRouteTypes {
| '/(group)/inside'
| '/(group)/lazyinside'
| '/_layout/_layout-2'
+ | '/pipe/$reference'
| '/posts/$postId'
| '/redirect/$target'
| '/search-params/default'
@@ -1349,6 +1361,7 @@ export interface RootRouteChildren {
groupLayoutRoute: typeof groupLayoutRouteWithChildren
groupInsideRoute: typeof groupInsideRoute
groupLazyinsideRoute: typeof groupLazyinsideRoute
+ PipeReferenceRoute: typeof PipeReferenceRoute
RedirectTargetRoute: typeof RedirectTargetRouteWithChildren
StructuralSharingEnabledRoute: typeof StructuralSharingEnabledRoute
ParamsPsIndexRoute: typeof ParamsPsIndexRoute
@@ -1542,6 +1555,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
+ '/pipe/$reference': {
+ id: '/pipe/$reference'
+ path: '/pipe/$reference'
+ fullPath: '/pipe/$reference'
+ preLoaderRoute: typeof PipeReferenceRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/_layout/_layout-2': {
id: '/_layout/_layout-2'
path: ''
@@ -2605,6 +2625,7 @@ const rootRouteChildren: RootRouteChildren = {
groupLayoutRoute: groupLayoutRouteWithChildren,
groupInsideRoute: groupInsideRoute,
groupLazyinsideRoute: groupLazyinsideRoute,
+ PipeReferenceRoute: PipeReferenceRoute,
RedirectTargetRoute: RedirectTargetRouteWithChildren,
StructuralSharingEnabledRoute: StructuralSharingEnabledRoute,
ParamsPsIndexRoute: ParamsPsIndexRoute,
diff --git a/e2e/react-router/basic-file-based/src/routes/__root.tsx b/e2e/react-router/basic-file-based/src/routes/__root.tsx
index dde8139a6e..ff04634fcb 100644
--- a/e2e/react-router/basic-file-based/src/routes/__root.tsx
+++ b/e2e/react-router/basic-file-based/src/routes/__root.tsx
@@ -156,6 +156,15 @@ function RootComponent() {
>
Masks
{' '}
+
+ Dynamic Pipe Character
+ {' '}
Hello {params.reference}!
+}
diff --git a/e2e/react-router/basic-file-based/tests/app.spec.ts b/e2e/react-router/basic-file-based/tests/app.spec.ts
index f644c10042..43f429ad2a 100644
--- a/e2e/react-router/basic-file-based/tests/app.spec.ts
+++ b/e2e/react-router/basic-file-based/tests/app.spec.ts
@@ -394,3 +394,16 @@ test.describe('Pathless layout routes', () => {
await expect(page.locator('body')).toContainText('Not Found')
})
})
+
+test.describe('Special characters in route paths', () => {
+ test('should render route with pipe character in path', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/pipe/hello|world')
+
+ await expect(page.locator('body')).toContainText('Hello hello|world!')
+
+ expect(page.url()).toBe(`${baseURL}/pipe/hello%7Cworld`)
+ })
+})
diff --git a/e2e/react-start/basic/src/routeTree.gen.ts b/e2e/react-start/basic/src/routeTree.gen.ts
index 27d5e91145..2a72e41265 100644
--- a/e2e/react-start/basic/src/routeTree.gen.ts
+++ b/e2e/react-start/basic/src/routeTree.gen.ts
@@ -42,6 +42,7 @@ import { Route as RawStreamSsrMixedRouteImport } from './routes/raw-stream/ssr-m
import { Route as RawStreamSsrBinaryHintRouteImport } from './routes/raw-stream/ssr-binary-hint'
import { Route as RawStreamClientCallRouteImport } from './routes/raw-stream/client-call'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
+import { Route as PipeReferenceRouteImport } from './routes/pipe.$reference'
import { Route as NotFoundViaLoaderRouteImport } from './routes/not-found/via-loader'
import { Route as NotFoundViaBeforeLoadRouteImport } from './routes/not-found/via-beforeLoad'
import { Route as MultiCookieRedirectTargetRouteImport } from './routes/multi-cookie-redirect/target'
@@ -228,6 +229,11 @@ const PostsPostIdRoute = PostsPostIdRouteImport.update({
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
+const PipeReferenceRoute = PipeReferenceRouteImport.update({
+ id: '/pipe/$reference',
+ path: '/pipe/$reference',
+ getParentRoute: () => rootRouteImport,
+} as any)
const NotFoundViaLoaderRoute = NotFoundViaLoaderRouteImport.update({
id: '/via-loader',
path: '/via-loader',
@@ -343,6 +349,7 @@ export interface FileRoutesByFullPath {
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
+ '/pipe/$reference': typeof PipeReferenceRoute
'/posts/$postId': typeof PostsPostIdRoute
'/raw-stream/client-call': typeof RawStreamClientCallRoute
'/raw-stream/ssr-binary-hint': typeof RawStreamSsrBinaryHintRoute
@@ -389,6 +396,7 @@ export interface FileRoutesByTo {
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
+ '/pipe/$reference': typeof PipeReferenceRoute
'/posts/$postId': typeof PostsPostIdRoute
'/raw-stream/client-call': typeof RawStreamClientCallRoute
'/raw-stream/ssr-binary-hint': typeof RawStreamSsrBinaryHintRoute
@@ -441,6 +449,7 @@ export interface FileRoutesById {
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
+ '/pipe/$reference': typeof PipeReferenceRoute
'/posts/$postId': typeof PostsPostIdRoute
'/raw-stream/client-call': typeof RawStreamClientCallRoute
'/raw-stream/ssr-binary-hint': typeof RawStreamSsrBinaryHintRoute
@@ -494,6 +503,7 @@ export interface FileRouteTypes {
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
| '/not-found/via-loader'
+ | '/pipe/$reference'
| '/posts/$postId'
| '/raw-stream/client-call'
| '/raw-stream/ssr-binary-hint'
@@ -540,6 +550,7 @@ export interface FileRouteTypes {
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
| '/not-found/via-loader'
+ | '/pipe/$reference'
| '/posts/$postId'
| '/raw-stream/client-call'
| '/raw-stream/ssr-binary-hint'
@@ -591,6 +602,7 @@ export interface FileRouteTypes {
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
| '/not-found/via-loader'
+ | '/pipe/$reference'
| '/posts/$postId'
| '/raw-stream/client-call'
| '/raw-stream/ssr-binary-hint'
@@ -642,6 +654,7 @@ export interface RootRouteChildren {
Char45824Char54620Char48124Char44397Route: typeof Char45824Char54620Char48124Char44397Route
ApiUsersRoute: typeof ApiUsersRouteWithChildren
MultiCookieRedirectTargetRoute: typeof MultiCookieRedirectTargetRoute
+ PipeReferenceRoute: typeof PipeReferenceRoute
RedirectTargetRoute: typeof RedirectTargetRouteWithChildren
MultiCookieRedirectIndexRoute: typeof MultiCookieRedirectIndexRoute
RedirectIndexRoute: typeof RedirectIndexRoute
@@ -882,6 +895,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
+ '/pipe/$reference': {
+ id: '/pipe/$reference'
+ path: '/pipe/$reference'
+ fullPath: '/pipe/$reference'
+ preLoaderRoute: typeof PipeReferenceRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/not-found/via-loader': {
id: '/not-found/via-loader'
path: '/via-loader'
@@ -1184,6 +1204,7 @@ const rootRouteChildren: RootRouteChildren = {
Char45824Char54620Char48124Char44397Route,
ApiUsersRoute: ApiUsersRouteWithChildren,
MultiCookieRedirectTargetRoute: MultiCookieRedirectTargetRoute,
+ PipeReferenceRoute: PipeReferenceRoute,
RedirectTargetRoute: RedirectTargetRouteWithChildren,
MultiCookieRedirectIndexRoute: MultiCookieRedirectIndexRoute,
RedirectIndexRoute: RedirectIndexRoute,
diff --git a/e2e/react-start/basic/src/routes/__root.tsx b/e2e/react-start/basic/src/routes/__root.tsx
index e1862b499c..145b9ff76d 100644
--- a/e2e/react-start/basic/src/routes/__root.tsx
+++ b/e2e/react-start/basic/src/routes/__root.tsx
@@ -181,6 +181,15 @@ function RootDocument({ children }: { children: React.ReactNode }) {
>
Raw Stream
{' '}
+
+ Dynamic Pipe Character
+ {' '}
Hello {params.reference}!
+}
diff --git a/e2e/react-start/basic/tests/pipe-character-in-path.spec.ts b/e2e/react-start/basic/tests/pipe-character-in-path.spec.ts
new file mode 100644
index 0000000000..0c57126c64
--- /dev/null
+++ b/e2e/react-start/basic/tests/pipe-character-in-path.spec.ts
@@ -0,0 +1,24 @@
+import { expect } from '@playwright/test'
+
+import { test } from '@tanstack/router-e2e-utils'
+
+test('encodes pipe character in href for param link', async ({ page }) => {
+ await page.goto('/pipe/hello|world')
+
+ await expect(page.locator('body')).toContainText('Hello hello|world!')
+})
+
+test('direct navigation keeps encoded url after reload', async ({
+ page,
+ baseURL,
+}) => {
+ await page.goto('/pipe/hello|world')
+
+ await expect(page.locator('body')).toContainText('Hello hello|world!')
+ expect(page.url()).toBe(`${baseURL}/pipe/hello%7Cworld`)
+
+ await page.reload()
+
+ await expect(page.locator('body')).toContainText('Hello hello|world!')
+ expect(page.url()).toBe(`${baseURL}/pipe/hello%7Cworld`)
+})
diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts
index 859a900496..7f382fa6bd 100644
--- a/packages/router-core/src/path.ts
+++ b/packages/router-core/src/path.ts
@@ -19,10 +19,10 @@ export function joinPaths(paths: Array) {
)
}
-/** Remove repeated slashes from a path string. */
+/** Remove repeated slashes and replace | to %7C from a path string. */
export function cleanPath(path: string) {
// remove double slashes
- return path.replace(/\/{2,}/g, '/')
+ return path.replace(/\/{2,}/g, '/').replace(/\|/g, '%7C')
}
/** Trim leading slashes (except preserving root '/'). */