# Add a custom route (https://docs.salla.dev/twilight/react/routes/add-a-custom-route)



<Callout title="Framework: TanStack Start">
  This page is written for TanStack Start, the default and currently supported framework. [See what depends on the framework](https://docs.salla.dev/twilight/react/start/frameworks).
</Callout>

The engine already serves every [built-in page](https://docs.salla.dev/twilight/react/routes). Add a custom route when you need a URL it does not have, such as `/new-arrivals`, `/faq` or `/promotions/summer`. To change a page the engine already has, use [Fork a built-in route](https://docs.salla.dev/twilight/react/recipes/fork-a-built-in-route) instead.

Adding a route takes two edits: one line in `app/routes.ts` and one file in `app/routes/`.

### Declare the path [#declare-the-path]

`app/routes.ts` is the list of pages your theme adds or replaces. The second argument is a file name inside `app/routes/`.

```ts title="app/routes.ts"
import { route } from '@tanstack/virtual-file-routes';

export const routes = [
  route('/new-arrivals', 'new-arrivals.tsx'),
];
```

### Create the route file [#create-the-route-file]

Write the file the entry points to. The path in `createFileRoute` is the same URL with `{-$locale}` in front, so one route answers `/new-arrivals`, `/ar/new-arrivals` and `/en/new-arrivals`.

```tsx title="app/routes/new-arrivals.tsx"
import { createFileRoute } from '@tanstack/react-router';
import { useTranslation } from '@salla.sa/twilight-theme-engine/i18n';
import { product } from '@salla.sa/twilight-theme-engine/api/product';
import { getTwilightContext } from '@salla.sa/twilight-theme-engine/tanstack';
import { ProductCard } from '@salla.sa/twilight-theme-engine/product';

export const Route = createFileRoute('/{-$locale}/new-arrivals')({
  loader: () => {
    const { queryClient } = getTwilightContext();
    return queryClient.fetchQuery(product.queries.list({ source: 'latest', perPage: 12 }));
  },
  head: () => ({ meta: [{ title: 'New arrivals' }] }),
  component: NewArrivals,
});

function NewArrivals() {
  const { t } = useTranslation();
  const { items } = Route.useLoaderData();

  return (
    <div className="container">
      <h1>{t('pages.new_arrivals.title', 'New arrivals')}</h1>
      <div className="grid grid-cols-2 gap-4 md:grid-cols-4">
        {items.map((item) => (
          <ProductCard key={item.id} product={item} />
        ))}
      </div>
    </div>
  );
}
```

### Restart and open it [#restart-and-open-it]

The route tree (`app/routeTree.gen.ts`) is regenerated when the dev server starts. Restart it and open `http://localhost:5173/en/new-arrivals`. You should see the heading and a grid of 12 products. Add the `pages.new_arrivals.title` key to both files in `locales/` ([Add translation keys](https://docs.salla.dev/twilight/react/recipes/add-translation-keys)).

## The parts of a route file [#the-parts-of-a-route-file]

| Part                               | What it does                                                                                |
| ---------------------------------- | ------------------------------------------------------------------------------------------- |
| `createFileRoute('/{-$locale}/…')` | The URL. Always start with `{-$locale}` so the language prefix works.                       |
| `loader`                           | Fetches the data on the server for the first visit, and in the browser on later navigation. |
| `head`                             | Sets the page title and meta tags. See [SEO and head](https://docs.salla.dev/twilight/react/guide/seo-and-head).                 |
| `pendingComponent`                 | A skeleton shown while the loader runs. Optional.                                           |
| `component`                        | Reads `Route.useLoaderData()` and renders.                                                  |

## URLs with a value in them [#urls-with-a-value-in-them]

Put a `$` before the part that changes, in both the manifest and the route path:

```ts title="app/routes.ts"
route('/promotions/$id', 'promotions.$id.tsx'),
```

```tsx title="app/routes/promotions.$id.tsx"
import { createFileRoute } from '@tanstack/react-router';
import { product } from '@salla.sa/twilight-theme-engine/api/product';

export const Route = createFileRoute('/{-$locale}/promotions/$id')({
  loader: ({ params }) => product.findOrThrow(params.id),
  head: ({ loaderData }) => ({ meta: [{ title: loaderData?.name }] }),
  component: Promotion,
});

function Promotion() {
  const item = Route.useLoaderData();
  const { id } = Route.useParams();
  return <h1>{item.name} (#{id})</h1>;
}
```

`params` also holds `locale`, which is `undefined` when the URL has no language prefix. A value can be mixed with fixed text, like the engine's own `/$slug/p{$id}` for products.

## Query strings [#query-strings]

To read `?page=2`, describe the search params you accept with `validateSearch`:

```tsx title="app/routes/new-arrivals.tsx"
export const Route = createFileRoute('/{-$locale}/new-arrivals')({
  validateSearch: (search: Record<string, unknown>) => ({
    page: Number(search.page) || 1,
  }),
  loaderDeps: ({ search }) => ({ page: search.page }),
  loader: ({ deps }) => {
    const { queryClient } = getTwilightContext();
    return queryClient.fetchQuery(
      product.queries.list({ source: 'latest', perPage: 12, page: deps.page })
    );
  },
  // head and component as above (with the same imports)
});
```

`loaderDeps` is what makes the loader run again when `?page=` changes.

## Loading and missing data [#loading-and-missing-data]

* **While loading:** add `pendingComponent: () => <Skeleton />`. The engine ships skeletons for its own pages, see the [skeleton components](https://docs.salla.dev/twilight/react/guide/layout-and-components).
* **Not found:** the `...OrThrow` functions on the API modules (`product.findOrThrow`) turn a 404 or 410 answer into the engine's [not-found page](https://docs.salla.dev/twilight/react/routes/not-found). Any other error reaches the router's error page. See [Errors, timeouts and retries](https://docs.salla.dev/twilight/react/api/errors-timeouts-and-retries).
* **Signed-out customers:** a page that only works for signed-in customers should check `useUser().isLoggedIn` in its component and show a sign-in prompt instead of the content.

## Link to your page [#link-to-your-page]

Use the engine's `Link` so the language prefix is kept:

```tsx
import { Link } from '@salla.sa/twilight-theme-engine/common';

<Link to="/new-arrivals">New arrivals</Link>
```

## Add it to the menu [#add-it-to-the-menu]

A route is only a URL. A merchant adds it to a menu in the dashboard, or you link to it from a component in the header or footer ([Layout and components](https://docs.salla.dev/twilight/react/guide/layout-and-components)).

## Common mistakes [#common-mistakes]

| Symptom                                                  | Cause                                                                                                   |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| 404 right after adding the route                         | The route tree is stale. Restart the dev server.                                                        |
| Works at `/faq` but not `/ar/faq`                        | The `createFileRoute` path is missing `{-$locale}`.                                                     |
| The file disappears or changes on the next build         | It starts with `// @auto-generated`. Never put that marker in a file you wrote.                         |
| The path exists in the engine and your page is not shown | You replaced a built-in route by accident. See [Fork a built-in route](https://docs.salla.dev/twilight/react/recipes/fork-a-built-in-route). |
| Window or `localStorage` errors                          | The loader also runs on the server. Keep browser-only code in the component.                            |

## Next [#next]

* [Overview](https://docs.salla.dev/twilight/react/routes): every built-in route, to copy patterns from
* [SEO redirects](https://docs.salla.dev/twilight/react/routes/seo-redirects): send an old URL to a new one
* [Routes and pages](https://docs.salla.dev/twilight/react/guide/routes-and-pages): the concepts behind `app/routes.ts`
* [Add a page](https://docs.salla.dev/twilight/react/recipes/add-a-page): the short recipe
