<html lang="en">
<head></head>
<body>

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][.gitignore]" value="node_modules
.DS_Store
dist
dist-ssr
*.local
">
<input type="hidden" name="project[files][README.md]" value="# TanStack Router - Location Masking Example

An example demonstrating location masking for URL privacy.

- [TanStack Router Docs](https://tanstack.com/router)

## Start a new project based on this example

To start a new project based on this example, run:

```sh
npx gitpick TanStack/router/tree/main/examples/react/location-masking location-masking
```

## Getting Started

Install dependencies:

```sh
pnpm install
```

Start the development server:

```sh
pnpm dev
```

## Build

Build for production:

```sh
pnpm build
```

## About This Example

This example demonstrates:

- Location masking
- URL privacy
- Hiding sensitive route information
- Custom URL display
- Security patterns
">
<input type="hidden" name="project[files][index.html]" value="&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;Vite App&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;app&quot;&gt;&lt;/div&gt;
    &lt;script type=&quot;module&quot; src=&quot;/src/main.tsx&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
">
<input type="hidden" name="project[files][package.json]" value="{&quot;name&quot;:&quot;tanstack-router-react-example-location-masking&quot;,&quot;private&quot;:true,&quot;type&quot;:&quot;module&quot;,&quot;scripts&quot;:{&quot;dev&quot;:&quot;vite --port 3000&quot;,&quot;build&quot;:&quot;vite build &amp;&amp; tsc --noEmit&quot;,&quot;preview&quot;:&quot;vite preview&quot;,&quot;start&quot;:&quot;vite&quot;},&quot;dependencies&quot;:{&quot;@radix-ui/react-dialog&quot;:&quot;^1.1.6&quot;,&quot;@tailwindcss/vite&quot;:&quot;^4.1.18&quot;,&quot;@tanstack/react-query&quot;:&quot;^5.90.0&quot;,&quot;@tanstack/react-router&quot;:&quot;https://pkg.pr.new/TanStack/router/@tanstack/react-router@a8b8021139e1f21e979e462e14ed8c15d4ce0f92&quot;,&quot;@tanstack/react-router-devtools&quot;:&quot;https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@a8b8021139e1f21e979e462e14ed8c15d4ce0f92&quot;,&quot;react&quot;:&quot;^19.0.0&quot;,&quot;react-dom&quot;:&quot;^19.0.0&quot;,&quot;redaxios&quot;:&quot;^0.5.1&quot;,&quot;tailwindcss&quot;:&quot;^4.1.18&quot;},&quot;devDependencies&quot;:{&quot;@types/react&quot;:&quot;^19.0.8&quot;,&quot;@types/react-dom&quot;:&quot;^19.0.3&quot;,&quot;@vitejs/plugin-react&quot;:&quot;^4.3.4&quot;,&quot;typescript&quot;:&quot;^5.7.2&quot;,&quot;vite&quot;:&quot;^7.1.7&quot;}}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
  &quot;compilerOptions&quot;: {
    &quot;strict&quot;: true,
    &quot;esModuleInterop&quot;: true,
    &quot;jsx&quot;: &quot;react-jsx&quot;,
    &quot;target&quot;: &quot;ESNext&quot;,
    &quot;moduleResolution&quot;: &quot;Bundler&quot;,
    &quot;module&quot;: &quot;ESNext&quot;,
    &quot;lib&quot;: [&quot;DOM&quot;, &quot;DOM.Iterable&quot;, &quot;ES2022&quot;],
    &quot;skipLibCheck&quot;: true
  }
}
">
<input type="hidden" name="project[files][vite.config.js]" value="import { defineConfig } from &#39;vite&#39;
import react from &#39;@vitejs/plugin-react&#39;
import tailwindcss from &#39;@tailwindcss/vite&#39;

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [tailwindcss(), react()],
})
">
<input type="hidden" name="project[files][.vscode/settings.json]" value="{
  &quot;files.watcherExclude&quot;: {
    &quot;**/routeTree.gen.ts&quot;: true
  },
  &quot;search.exclude&quot;: {
    &quot;**/routeTree.gen.ts&quot;: true
  },
  &quot;files.readonlyInclude&quot;: {
    &quot;**/routeTree.gen.ts&quot;: true
  }
}
">
<input type="hidden" name="project[files][src/main.tsx]" value="import React from &#39;react&#39;
import ReactDOM from &#39;react-dom/client&#39;
import {
  ErrorComponent,
  Link,
  Outlet,
  RouterProvider,
  createRootRoute,
  createRoute,
  createRouteMask,
  createRouter,
  useNavigate,
  useRouterState,
} from &#39;@tanstack/react-router&#39;
import { TanStackRouterDevtools } from &#39;@tanstack/react-router-devtools&#39;
import * as Dialog from &#39;@radix-ui/react-dialog&#39;
import type { ErrorComponentProps } from &#39;@tanstack/react-router&#39;
import &#39;./styles.css&#39;

type PhotoType = {
  id: string
  title: string
  url: string
  thumbnailUrl: string
  albumId: string
}

class NotFoundError extends Error {}

const fetchPhotos = async () =&gt; {
  console.info(&#39;Fetching photos...&#39;)
  await new Promise((r) =&gt; setTimeout(r, 500))
  // Generate mock photos using picsum.photos since via.placeholder.com is down
  return Array.from({ length: 10 }, (_, i) =&gt; ({
    id: String(i + 1),
    title: `Photo ${i + 1}`,
    url: `https://picsum.photos/600/400?random=${i + 1}`,
    thumbnailUrl: `https://picsum.photos/200/200?random=${i + 1}`,
    albumId: &#39;1&#39;,
  }))
}

const fetchPhoto = async (photoId: string) =&gt; {
  console.info(`Fetching photo with id ${photoId}...`)
  await new Promise((r) =&gt; setTimeout(r, 500))

  // Simulate photo not found for invalid IDs
  const photoIdNum = parseInt(photoId, 10)
  if (isNaN(photoIdNum) || photoIdNum &lt; 1 || photoIdNum &gt; 10) {
    throw new NotFoundError(`Photo with id &quot;${photoId}&quot; not found!`)
  }

  // Generate mock photo using picsum.photos
  return {
    id: photoId,
    title: `Photo ${photoId}`,
    url: `https://picsum.photos/600/400?random=${photoId}`,
    thumbnailUrl: `https://picsum.photos/200/200?random=${photoId}`,
    albumId: &#39;1&#39;,
  }
}

type PhotoModal = {
  id: &#39;photo&#39;
  photoId: string
}

type ModalObject = PhotoModal

export function Spinner() {
  return (
    &lt;div className=&quot;animate-spin px-3 text-xl inline-flex items-center justify-center&quot;&gt;
      ⍥
    &lt;/div&gt;
  )
}

const rootRoute = createRootRoute({
  validateSearch: (search) =&gt;
    search as {
      modal?: ModalObject
    },
  component: RootComponent,
})

function RootComponent() {
  const status = useRouterState({ select: (s) =&gt; s.status })

  return (
    &lt;&gt;
      &lt;div className=&quot;p-2 flex gap-2 text-lg&quot;&gt;
        &lt;Link
          to=&quot;/&quot;
          activeProps={{
            className: &#39;font-bold&#39;,
          }}
          activeOptions={{ exact: true }}
        &gt;
          Home
        &lt;/Link&gt;{&#39; &#39;}
        &lt;Link
          to=&quot;/photos&quot;
          activeProps={{
            className: &#39;font-bold&#39;,
          }}
        &gt;
          Photos
        &lt;/Link&gt;{&#39; &#39;}
        {status === &#39;pending&#39; ? &lt;Spinner /&gt; : null}
      &lt;/div&gt;
      &lt;hr /&gt;
      &lt;Outlet /&gt;
      {/* Start rendering router matches */}
      &lt;TanStackRouterDevtools position=&quot;bottom-right&quot; /&gt;
    &lt;/&gt;
  )
}

function Modal(props: Dialog.DialogProps) {
  return (
    &lt;Dialog.Root open {...props}&gt;
      &lt;Dialog.Portal&gt;
        &lt;Dialog.Overlay className=&quot;fixed inset-0 bg-black/70&quot; /&gt;
        &lt;Dialog.DialogContent className=&quot;fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2&quot;&gt;
          {props.children}
        &lt;/Dialog.DialogContent&gt;
      &lt;/Dialog.Portal&gt;
    &lt;/Dialog.Root&gt;
  )
}

const indexRoute = createRoute({
  getParentRoute: () =&gt; rootRoute,
  path: &#39;/&#39;,
  component: () =&gt; {
    return (
      &lt;div className=&quot;p-2&quot;&gt;
        &lt;h3&gt;Welcome Home!&lt;/h3&gt;
      &lt;/div&gt;
    )
  },
})
const photosLayoutRoute = createRoute({
  getParentRoute: () =&gt; rootRoute,
  path: &#39;photos&#39;,
  loader: fetchPhotos,
  component: PhotosRoute,
})

function PhotosRoute() {
  const photos = photosLayoutRoute.useLoaderData()

  return (
    &lt;div className=&quot;p-2 space-y-2&quot;&gt;
      &lt;ul className=&quot;grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2&quot;&gt;
        {[
          ...photos,
          { id: &#39;i-do-not-exist&#39;, title: &#39;Missing Photo Test&#39;, url: &#39;&#39; },
        ].map((photo) =&gt; {
          return (
            &lt;li key={photo.id} className=&quot;&quot;&gt;
              &lt;Link
                to={photoModalRoute.to}
                params={{
                  photoId: photo.id,
                }}
                // If you want to use a mask, you can do so like this, but
                // it&#39;s generally safer to set up a route mask instead.
                // mask={{
                //   to: photoRoute.to,
                //   params: {
                //     photoId: photo.id,
                //   },
                // }}
                className=&quot;whitespace-nowrap border rounded-lg shadow-xs flex items-center hover:shadow-lg text-blue-600 hover:scale-[1.1] overflow-hidden transition-all&quot;
              &gt;
                &lt;img src={photo.url} alt={photo.title} className=&quot;max-w-full&quot; /&gt;
              &lt;/Link&gt;
            &lt;/li&gt;
          )
        })}
      &lt;/ul&gt;
      &lt;Outlet /&gt;
    &lt;/div&gt;
  )
}

const photoRoute = createRoute({
  getParentRoute: () =&gt; rootRoute,
  path: &#39;photos/$photoId&#39;,
  loader: async ({ params: { photoId } }) =&gt; fetchPhoto(photoId),
  errorComponent: PhotoErrorComponent,
  component: PhotoComponent,
})

function PhotoErrorComponent({ error }: ErrorComponentProps) {
  return (
    &lt;div className=&quot;p-4&quot;&gt;
      {(() =&gt; {
        if (error instanceof NotFoundError) {
          return &lt;div&gt;{error.message}&lt;/div&gt;
        }
        return &lt;ErrorComponent error={error} /&gt;
      })()}
    &lt;/div&gt;
  )
}

function PhotoComponent() {
  const photo = photoRoute.useLoaderData()

  return (
    &lt;div className=&quot;p-4&quot;&gt;
      &lt;Photo photo={photo} /&gt;
    &lt;/div&gt;
  )
}

const photoModalRoute = createRoute({
  getParentRoute: () =&gt; photosLayoutRoute,
  path: &#39;$photoId/modal&#39;,
  loader: async ({ params: { photoId } }) =&gt; fetchPhoto(photoId),
  errorComponent: PhotoModalErrorComponent,
  // pendingComponent: PhotoModalPendingComponent,
  component: PhotoModalComponent,
})

function PhotoModalErrorComponent({ error }: ErrorComponentProps) {
  const navigate = useNavigate()

  return (
    &lt;Modal
      onOpenChange={(open) =&gt; {
        if (!open) {
          navigate({
            to: photosLayoutRoute.to,
          })
        }
      }}
    &gt;
      &lt;div className=&quot;bg-gray-100 dark:bg-gray-800 p-2 rounded-lg&quot;&gt;
        {(() =&gt; {
          if (error instanceof NotFoundError) {
            return &lt;div&gt;{error.message}&lt;/div&gt;
          }
          return &lt;ErrorComponent error={error} /&gt;
        })()}
      &lt;/div&gt;
    &lt;/Modal&gt;
  )
}

function PhotoModalPendingComponent() {
  const navigate = useNavigate()

  return (
    &lt;Modal
      onOpenChange={(open) =&gt; {
        if (!open) {
          navigate({
            to: photosLayoutRoute.to,
          })
        }
      }}
    &gt;
      &lt;div className=&quot;bg-gray-100 dark:bg-gray-800 p-2 rounded-lg&quot;&gt;
        &lt;Spinner /&gt;
      &lt;/div&gt;
    &lt;/Modal&gt;
  )
}

function PhotoModalComponent() {
  const navigate = useNavigate()
  const photo = photoModalRoute.useLoaderData()

  return (
    &lt;Modal
      onOpenChange={(open) =&gt; {
        if (!open) {
          navigate({
            to: photosLayoutRoute.to,
          })
        }
      }}
    &gt;
      &lt;div className=&quot;bg-gray-100 dark:bg-gray-800 p-2 rounded-lg&quot;&gt;
        &lt;Link
          to=&quot;.&quot;
          target=&quot;_blank&quot;
          className=&quot;text-blue-600 hover:opacity-75 underline&quot;
        &gt;
          Open in new tab (to test de-masking)
        &lt;/Link&gt;
        &lt;Photo photo={photo} /&gt;
      &lt;/div&gt;
    &lt;/Modal&gt;
  )
}

function Photo({ photo }: { photo: PhotoType }) {
  return (
    &lt;div className=&quot;space-y-2&quot;&gt;
      &lt;h4 className=&quot;text-xl font-bold underline&quot;&gt;{photo.title}&lt;/h4&gt;
      &lt;div className=&quot;&quot;&gt;
        &lt;img src={photo.url} alt={photo.title} className=&quot;max-w-full&quot; /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

const routeTree = rootRoute.addChildren([
  photoRoute,
  photosLayoutRoute.addChildren([photoModalRoute]),
  indexRoute,
])

const photoModalToPhotoMask = createRouteMask({
  routeTree,
  from: &#39;/photos/$photoId/modal&#39;,
  to: &#39;/photos/$photoId&#39;,
  params: true,
})

// Set up a Router instance
const router = createRouter({
  routeTree,
  routeMasks: [photoModalToPhotoMask],
  defaultPreload: &#39;intent&#39;,
  scrollRestoration: true,
})

// Register things for typesafety
declare module &#39;@tanstack/react-router&#39; {
  interface Register {
    router: typeof router
  }
}

const rootElement = document.getElementById(&#39;app&#39;)!

if (!rootElement.innerHTML) {
  const root = ReactDOM.createRoot(rootElement)

  root.render(&lt;RouterProvider router={router} /&gt;)
}
">
<input type="hidden" name="project[files][src/styles.css]" value="@import &#39;tailwindcss&#39;;

@layer base {
  *,
  ::after,
  ::before,
  ::backdrop,
  ::file-selector-button {
    border-color: var(--color-gray-200, currentcolor);
  }
}

html {
  color-scheme: light dark;
}
* {
  @apply border-gray-200 dark:border-gray-800;
}
body {
  @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
}
">
<input type="hidden" name="project[description]" value="generated by https://pkg.pr.new">
<input type="hidden" name="project[template]" value="node">
<input type="hidden" name="project[title]" value="tanstack-router-react-example-location-masking">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>