# Product page (https://docs.salla.dev/twilight/react/routes/product)



<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 customer sees one product: gallery, details, the add-to-cart form, reviews and related products. It is the most important page of most stores and has the richest SEO data of any built-in route. Customise this route to add data to the loader, change the title, or wrap the page; to change only how a product looks in lists, [replace the product card](https://docs.salla.dev/twilight/react/recipes/replace-the-product-card).

## URL [#url]

| Pattern                  | Example              |
| ------------------------ | -------------------- |
| `/$slug/p{$id}`          | `/red-shirt/p123`    |
| `/{locale}/$slug/p{$id}` | `/ar/red-shirt/p123` |

### Params [#params]

| Name     | Type                  | Example     | Description                                                                                     |
| -------- | --------------------- | ----------- | ----------------------------------------------------------------------------------------------- |
| `slug`   | `string`              | `red-shirt` | For readable links and SEO. The loader never reads it, so a wrong slug still shows the product. |
| `id`     | `string`              | `123`       | The product id. In the URL it follows the letter `p` (`p123`); the router removes the `p`.      |
| `locale` | `string \| undefined` | `ar`        | Optional ([locale prefix](https://docs.salla.dev/twilight/react/routes/home#locale-prefix)).                                         |

There are no search params.

### Try it [#try-it]

This is a product as the engine renders it, running against the demo store. Switch the language or the width.

<RoutePreview path="/x/p677709975" pattern="/{locale}/$slug/p{$id}" />

## Route file [#route-file]

```tsx title="app/routes/$slug.p$id.tsx"
// @auto-generated
import { createFileRoute } from '@tanstack/react-router';
import { Product } from '@salla.sa/twilight-theme-engine/routes/product';
import type { ProductPageProps } from '@salla.sa/twilight-theme-engine/routes/product';
import { ProductDetailSkeleton } from '@salla.sa/twilight-theme-engine/skeleton';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';

export const Route = createFileRoute('/{-$locale}/$slug/p{$id}')({
  loader: ({ params }): Promise<ProductPageProps> =>
    Product.loader({ params: { id: params.id }, locale: params.locale }),
  head: withHead(Product),
  pendingComponent: () => <ProductDetailSkeleton />,
  component: ProductComponent,
});

function ProductComponent() {
  const data: ProductPageProps = Route.useLoaderData();
  return <Product.Component {...data} />;
}
```

To take the page over, write your own file (without the `// @auto-generated` line) and point the same path at it from `app/routes.ts`: `route('/$slug/p{$id}', 'product-single.tsx')`. The generated file stays but is no longer used. The [fork recipe](https://docs.salla.dev/twilight/react/recipes/fork-a-built-in-route) walks through it.

### A customised version [#a-customised-version]

This keeps the engine loader, merges in one extra field, extends the head, and renders your element around the engine page. `Product.loader` takes an optional second argument that receives the loaded data and returns fields to merge in.

```tsx title="app/routes/product-single.tsx"
import { createFileRoute } from '@tanstack/react-router';
import { Product } from '@salla.sa/twilight-theme-engine/routes/product';
import type { ProductPageProps } from '@salla.sa/twilight-theme-engine/routes/product';
import { ProductDetailSkeleton } from '@salla.sa/twilight-theme-engine/skeleton';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';

export const Route = createFileRoute('/{-$locale}/$slug/p{$id}')({
  loader: ({ params }): Promise<ProductPageProps & { badge: string }> =>
    Product.loader({ params: { id: params.id }, locale: params.locale }, (data) => ({
      badge: data.product.is_available ? 'In stock' : 'Sold out',
    })) as Promise<ProductPageProps & { badge: string }>,
  head: withHead(Product, (head, _ctx, data) => ({
    ...head,
    title: `${data.product.name} | Official store`,
  })),
  pendingComponent: () => <ProductDetailSkeleton />,
  component: ProductComponent,
});

function ProductComponent() {
  const { badge, ...data } = Route.useLoaderData();
  return (
    // key remounts the page when you move from one product to another
    <div key={data.product.id}>
      <p className="container">{badge}</p>
      <Product.Component {...data} />
    </div>
  );
}
```

## What it loads [#what-it-loads]

`ProductPageProps`:

| Field     | Type      | Description                                                                                |
| --------- | --------- | ------------------------------------------------------------------------------------------ |
| `product` | `Product` | The full product record.                                                                   |
| `page`    | `Page`    | `id`, `title`, `url`, `parent` (the category) and `breadcrumbs` (Home, category, product). |

It runs on the server for the first request and in the browser for later navigations. Related products, reviews, the offer banner and testimonials are not part of it: they load in the browser when scrolled into view. See [Loading data](https://docs.salla.dev/twilight/react/guide/loading-data), and [`useProduct`](https://docs.salla.dev/twilight/react/hooks/use-product) to keep the product in sync after variant or stock changes.

[Hook slots](https://docs.salla.dev/twilight/react/guide/hooks-and-slots), in page order: `product:start`, `product:details.start`, `product:single.description.start`, `product:single.description`, `product:details.end`, `product:single.form.start`, `product:single.form.end`, `product:single.description.end`, `product:related.start`, `product:related.end`, `product:end`. The three `product:single.description*` slots render once each with `{ product }` as context.

## Head and SEO [#head-and-seo]

`Product.head` builds, from the loaded product:

* Title is the product name; description is the product description without HTML, cut to 160 characters.
* Canonical and `og:url` are `product.url`, so the slug in the address bar never defines the canonical URL.
* Open Graph (`product`) and Twitter cards with the main image.
* A `Product` JSON-LD block: SKU, images, an `Offer` (price, currency, availability) and, when present, brand, rating, `gtin8` (only 8-digit GTINs) and `mpn`.
* Hreflang links for every store language.
* When the store has quick buy enabled, the quick-checkout script is added up front, because the Buy Now button is above the fold.

See [SEO and head](https://docs.salla.dev/twilight/react/guide/seo-and-head).

## Edge cases [#edge-cases]

* **Unknown product id.** Any API failure becomes a 404 error page. It is the plain error page, not the full [not-found page](https://docs.salla.dev/twilight/react/routes/not-found), so the `notfound:*` hook slots do not appear. Throw `notFound()` from your own loader if you need the full page.
* **Wrong slug.** No redirect: `/anything/p123` shows product 123. The canonical tag keeps search engines on one URL.
* **Short links.** `/ar/lvXvzln` does not match this route; the not-found page resolves it and redirects here ([SEO redirects](https://docs.salla.dev/twilight/react/routes/seo-redirects)).
* **Product to product navigation.** The route component is reused when only the params change. Set a `key` (as above) if your component keeps mount-once state.

## See also [#see-also]

* [Category page](https://docs.salla.dev/twilight/react/routes/category) and [Search](https://docs.salla.dev/twilight/react/routes/search)
* [Replace the product card](https://docs.salla.dev/twilight/react/recipes/replace-the-product-card)
* [Fork a built-in route](https://docs.salla.dev/twilight/react/recipes/fork-a-built-in-route)
* [Routes and pages](https://docs.salla.dev/twilight/react/guide/routes-and-pages)
