Repository: TanStack/router (2025-07-19T15:47:50Z)

Issue:
Redirect loop with optional path params, layout route and index route
### Which project does this relate to?

Router

### Describe the bug

I'm falling into an infinite redirect loop using the following directory structure:

```
├── src/
│   ├── routes/
│   │   ├── {-$locale}/
│   │   │   ├── index.tsx
│   │   │   └── route.tsx
│   │   └── `__root.tsx`
```

Not posting codesandbox link here beacause it breaks the browser

### Your Example Website or App

https://github.com/SashkaHavr/tsrouter-optional-path-params-repro

### Steps to Reproduce the Bug or Issue

1. `pnpm install`
2. `pnpm dev`
3. Visit http://localhost:3000/
4. Look at server console output

***Result:*** "render /{-$locale}/route.tsx" is infinitelly spammed in console and page is never opened

### Expected behavior

Index page with default locale is opened

### Screenshots or Videos

_No response_

### Platform

- Router / Start Version: 1.128.4/1.128.5
- OS: Windows
- Browser: Chrome
- Bundler: vite
- Bundler Version: 7.0.5


### Additional context

**Workaround:**

1. Move `beforeLoad` from `/src/routes/{-$locale}/route.tsx` into `/src/routes/{-$locale}/index.tsx`
2. Delete `/src/routes/{-$locale}/route.tsx`

***Result:*** Index page with default locale is opened


Discussion:
I'm experiencing the same, I think it might be introduced by https://github.com/TanStack/router/pull/4685. Or it was there before and covered for me by the other issue.

It can be seen in this [reproduction](https://stackblitz.com/edit/tanstack-router-yifxmvlx?file=src%2Froutes%2Fadmin-levels.tsx). Open the console prior to clicking "Broken Path".

<img width="1133" height="421" alt="Image" src="https://github.com/user-attachments/assets/4a2bd691-2f3f-4b8d-b33d-57ab30dfe708" />



Patch:
diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts
index 221b4e37c09..1c710d0d336 100644
--- a/packages/router-core/src/path.ts
+++ b/packages/router-core/src/path.ts
@@ -437,6 +437,9 @@ export function interpolatePath({
 
         // Check if optional parameter is missing or undefined
         if (!(key in params) || params[key] == null) {
+          if (leaveWildcards) {
+            return `${segmentPrefix}${key}${segmentSuffix}`
+          }
           // For optional params with prefix/suffix, keep the prefix/suffix but omit the param
           if (segmentPrefix || segmentSuffix) {
             return `${segmentPrefix}${segmentSuffix}`


Test patch:
diff --git a/packages/react-router/tests/optional-path-params.test.tsx b/packages/react-router/tests/optional-path-params.test.tsx
index 532891cb593..da632651463 100644
--- a/packages/react-router/tests/optional-path-params.test.tsx
+++ b/packages/react-router/tests/optional-path-params.test.tsx
@@ -143,6 +143,86 @@ describe('React Router - Optional Path Parameters', () => {
         expect(JSON.parse(paramsElement.textContent!)).toEqual(expectedParams)
       },
     )
+
+    it.each([
+      {
+        path: '/',
+        expectedLocale: 'en' as const,
+      },
+      {
+        path: '/en',
+        expectedLocale: 'en' as const,
+      },
+      {
+        path: '/fr',
+        expectedLocale: 'fr' as const,
+      },
+    ])(
+      'should correctly render matches with and without optional parameter',
+      async ({ path, expectedLocale }) => {
+        const content = {
+          en: {
+            title: 'About Us',
+            description: 'Learn more about our company.',
+          },
+          fr: {
+            title: 'À Propos',
+            description: 'En savoir plus sur notre entreprise.',
+          },
+          es: {
+            title: 'Acerca de',
+            description: 'Conoce más sobre nuestra empresa.',
+          },
+        } as const
+
+        const rootRoute = createRootRoute()
+        const localeRoute = createRoute({
+          getParentRoute: () => rootRoute,
+          path: '/{-$locale}',
+          beforeLoad: ({ params }) => {
+            const currentLocale = (params.locale ||
+              'en') as keyof typeof content
+
+            return {
+              content: content[currentLocale],
+            }
+          },
+        })
+
+        const indexRoute = createRoute({
+          getParentRoute: () => localeRoute,
+          path: '/',
+          component: () => {
+            const { content } = indexRoute.useRouteContext()
+            return (
+              <div data-testid="index-content">
+                <h1 data-testid="index-title">{content.title}</h1>
+                <p data-testid="index-description">{content.description}</p>
+              </div>
+            )
+          },
+        })
+
+        window.history.replaceState({}, '', path)
+
+        const router = createRouter({
+          routeTree: rootRoute.addChildren([
+            localeRoute.addChildren([indexRoute]),
+          ]),
+        })
+
+        render(<RouterProvider router={router} />)
+        await act(() => router.load())
+
+        await screen.findByTestId('index-content')
+        const titleElement = screen.getByTestId('index-title')
+        const descriptionElement = screen.getByTestId('index-description')
+        expect(titleElement).toHaveTextContent(content[expectedLocale].title)
+        expect(descriptionElement).toHaveTextContent(
+          content[expectedLocale].description,
+        )
+      },
+    )
   })
 
   describe('Link component with optional parameters', () => {