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.tsis 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/.
import { route } from '@tanstack/virtual-file-routes';
export const routes = [route('/faq', 'faq.tsx')];Create that file. Keep the {-$locale} prefix in the path.
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-generatedline from the route's file inapp/routes/. For a route the engine serves from.twilight/, add a file of the same name (for exampleapp/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:
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
| Part | What it does |
|---|---|
loader | Fetches the page data on the server, and again on in-app navigation. |
head | Sets the page title and meta tags. withHead(Module) reuses the engine's. |
pendingComponent | A skeleton shown while the loader runs. |
component | Reads 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.