Routes and pages

How a page exists in your theme, and how to add one or take over a built-in one.

Framework: TanStack Start

This page is written for TanStack Start, the default and currently supported framework. See what depends on the framework.

Read this when you want a page the engine does not have, or want to change one it does.

A route connects a URL to a page. The engine ships a route for every storefront page (home, product, category, cart, blog, account and more), so a new theme already has all of them. You only write route files for pages you add or take over.

Where routes live

Two things work together in your theme:

  • app/routes.ts is the manifest. It lists the pages you add or replace.
  • app/routes/ holds the route files those entries point to.

At build time the engine plugin merges your manifest with its built-in routes and writes app/routeTree.gen.ts. Most files in app/routes/ are generated copies of the engine's routes, marked with a // @auto-generated first line. Of those, only __root.tsx is meant to be edited by hand. Some built-in routes add no file there at all: every product list (search, categories, tags, brands, /products?source=…), the product reviews page and the redirects from old Twig addresses are served from the engine's own .twilight/ folder.

Never put // @auto-generated at the top of a route file you wrote. That marker tells the plugin the file is disposable, and the next build overwrites it.

Every URL starts with the locale

Every storefront route path begins with {-$locale}. It is an optional URL segment that holds the language, so one route serves /ar/cart and /en/cart. Languages and RTL explains what the engine does with it.

Add a page

Add an entry to the manifest. The second argument is a file name inside app/routes/.

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

export const routes = [route('/faq', 'faq.tsx')];

Create that file. Keep the {-$locale} prefix in the path.

app/routes/faq.tsx
import { createFileRoute } from '@tanstack/react-router';

export const Route = createFileRoute('/{-$locale}/faq')({
  component: () => <div>FAQ</div>,
});

Your page is now served at /ar/faq and /en/faq. If it returns a 404, restart the dev server so the route tree is regenerated. The Add a page recipe has the full walkthrough.

Replace a built-in page

There are two ways, and both keep the engine's path:

  • Take the file over. Delete the // @auto-generated line from the route's file in app/routes/. For a route the engine serves from .twilight/, add a file of the same name (for example app/routes/search.tsx) without that line. The build uses your file instead of the engine's.
  • Point the path at another file with a manifest entry, such as route('/cart', 'cart-page.tsx'). The theme entry replaces the engine's.

The Raed starter theme takes over three files this way: app/routes/cart.tsx, $slug.p$id.tsx (the product page) and $slug.page-$id.tsx (a CMS page). Its manifest is empty. They reuse the engine's data and head, and swap only the component. Here is the cart one, shortened:

app/routes/cart.tsx
import { createFileRoute } from '@tanstack/react-router';
import { Cart } from '@salla.sa/twilight-theme-engine/routes/cart';
import { CartSkeleton } from '@salla.sa/twilight-theme-engine/skeleton';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';
import { CartPage } from '../components/cart/CartPage';

export const Route = createFileRoute('/{-$locale}/cart')({
  loader: ({ params }) => Cart.loader({ locale: params.locale }),
  head: withHead(Cart),
  pendingComponent: () => <CartSkeleton />,
  component: CartComponent,
});

function CartComponent() {
  const data = Route.useLoaderData();
  return <CartPage {...data} />;
}

The four parts of a route

PartWhat it does
loaderFetches the page data on the server, and again on in-app navigation.
headSets the page title and meta tags. withHead(Module) reuses the engine's.
pendingComponentA skeleton shown while the loader runs.
componentReads Route.useLoaderData() and renders.

Each built-in page is a module with loader, head and Component, imported from @salla.sa/twilight-theme-engine/routes/<page>. That is why a fork can keep the engine's loader and replace only the markup.

Replacing a page means you now own it, including future engine fixes to it. If you only want to add something to the page, use a slot instead. The fork recipe shows the safe way.

To restyle every product list, fork no route: register your page as product:list-page, as Raed does in app/router.tsx. See Replace the whole product list page.

All built-in routes are listed in the Routes tab.

Next: Loading data, to fill your route with store data.

On this page