# Tag page (https://docs.salla.dev/twilight/react/routes/tag)



<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 the products that carry one tag, after selecting a tag shown on a product. The engine has two routes for it with the same loader and result: the SEO form `/$slug/tag-{$id}` and the short form `/tags/$id`. Customise them to change the title or add content above the list; override both if you want the two URLs to look the same. This is the product tag; the blog has its [own tag page](https://docs.salla.dev/twilight/react/routes/blog-tag).

## URL [#url]

| Pattern                     | Example             |
| --------------------------- | ------------------- |
| `/$slug/tag-{$id}`          | `/summer/tag-88`    |
| `/{locale}/$slug/tag-{$id}` | `/ar/summer/tag-88` |
| `/tags/$id`                 | `/tags/88`          |
| `/{locale}/tags/$id`        | `/en/tags/88`       |

### Params [#params]

| Name     | Type                  | Example  | Description                                                               |
| -------- | --------------------- | -------- | ------------------------------------------------------------------------- |
| `slug`   | `string`              | `summer` | Slug form only. Not passed to the loader.                                 |
| `id`     | `string`              | `88`     | The tag id. Written `tag-88` in the slug form, plain `88` in `/tags/$id`. |
| `locale` | `string \| undefined` | `ar`     | Optional ([locale prefix](https://docs.salla.dev/twilight/react/routes/home#locale-prefix)).                   |

### Search params [#search-params]

| Name   | Type     | Example    | Description                                                                                                                                               |
| ------ | -------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sort` | `string` | `bestSell` | Optional. The loader uses `ourSuggest` when absent.                                                                                                       |
| `page` | `number` | `2`        | Optional. Values of 1 or less are dropped. The API pages only by its own cursor, so the loader ignores a number and the list starts from the first batch. |

## Route file [#route-file]

The engine writes two files, one per URL form, into its own `.twilight/` folder rather than `app/routes/`. First `$slug.tag-$id.tsx`:

```tsx title="$slug.tag-$id.tsx (served by the engine)"
// @auto-generated
import { createFileRoute } from '@tanstack/react-router';
import { ProductListing, listingSearch } from '@salla.sa/twilight-theme-engine/routes/product-listing';
import type { ProductListLoaderData } from '@salla.sa/twilight-theme-engine/routes/product-listing';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';

export const Route = createFileRoute('/{-$locale}/$slug/tag-{$id}')({
  ...listingSearch,
  loader: ({ deps, params }): Promise<ProductListLoaderData> =>
    ProductListing.loader({
      params: { source: 'tags', id: params.id },
      search: deps,
      locale: params.locale,
    }),
  head: withHead(ProductListing),
  pendingComponent: () => <ProductListing.Pending hideFilters />,
  component: ProductListingComponent,
});

function ProductListingComponent() {
  const data: ProductListLoaderData = Route.useLoaderData();
  return <ProductListing.Component {...data} />;
}
```

`tags.$id.tsx` is the same except for the path:

```tsx title="tags.$id.tsx (served by the engine)"
export const Route = createFileRoute('/{-$locale}/tags/$id')({
  // validateSearch, loaderDeps and loader exactly as above
  head: withHead(ProductListing),
  pendingComponent: () => <ProductListing.Pending hideFilters />,
  component: ProductListingComponent,
});
```

To take a page over, add a file of the same name to `app/routes/` without the `// @auto-generated` line, or write your own file and map the path in `app/routes.ts`: `route('/$slug/tag-{$id}', 'slug-tag-page.tsx')` and `route('/tags/$id', 'tags-page.tsx')`. See [Fork a built-in route](https://docs.salla.dev/twilight/react/recipes/fork-a-built-in-route). To restyle every product list at once, register a page as `product:list-page` instead; see [Replace the product list page](https://docs.salla.dev/twilight/react/recipes/replace-the-product-card#replace-the-whole-product-list-page).

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

This adds a suffix to the title and a line above the engine list.

```tsx title="app/routes/tag-page.tsx"
import { createFileRoute } from '@tanstack/react-router';
import {
  ProductListing,
  listingSearch,
  type ProductListLoaderData,
} from '@salla.sa/twilight-theme-engine/routes/product-listing';
import { withHead } from '@salla.sa/twilight-theme-engine/tanstack';

export const Route = createFileRoute('/{-$locale}/$slug/tag-{$id}')({
  ...listingSearch,
  loader: ({ deps, params }): Promise<ProductListLoaderData> =>
    ProductListing.loader({
      params: { source: 'tags', id: params.id },
      search: deps,
      locale: params.locale,
    }),
  head: withHead(ProductListing, (head, _ctx, data) => ({
    ...head,
    title: `${data.page.title} | Official store`,
  })),
  component: ListComponent,
});

function ListComponent() {
  const data: ProductListLoaderData = Route.useLoaderData();
  return (
    <>
      <p className="container">{data.products.length} products on this page</p>
      <ProductListing.Component {...data} />
    </>
  );
}
```

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

`ProductListLoaderData`, the same shape as on the [category page](https://docs.salla.dev/twilight/react/routes/category#what-it-loads). For tags:

* The loader requests the product list for the tag and takes the tag (`id`, `name`, `url`) from the first product's tags. That tag becomes `source.entity`.
* `query.filters` is `false`, so there is no filter panel.
* More products load in the browser as the customer scrolls.

Sort values are `ourSuggest`, `bestSell`, `topRated`, `priceFromLowToTop` and `priceFromTopToLow`. The page has the same [hook slots](https://docs.salla.dev/twilight/react/guide/hooks-and-slots) as every product list (`product:list.*`).

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

Title is the tag name, description is `Browse {name}`, canonical and `og:url` are the tag's own `url`, with hreflang links for every language. There is no social image.

## Edge cases [#edge-cases]

* **Empty or unknown tag.** The tag name comes from the first product, so a tag with no products cannot be shown. The loader throws a not-found error and the [not-found page](https://docs.salla.dev/twilight/react/routes/not-found) renders.
* **Request failure.** Unlike categories and brands, a failed request is not turned into a 404; the error page shows.
* **Two URLs, one list.** `/tags/88` and `/x/tag-88` render the same page. The canonical is the tag's own `url` from the API, which tells search engines which form to keep.
* **Wrong slug.** Ignored; no redirect.

## See also [#see-also]

* [Category page](https://docs.salla.dev/twilight/react/routes/category) and [Search](https://docs.salla.dev/twilight/react/routes/search)
* [Blog tag](https://docs.salla.dev/twilight/react/routes/blog-tag)
* [Fork a built-in route](https://docs.salla.dev/twilight/react/recipes/fork-a-built-in-route)
* [Routes overview](https://docs.salla.dev/twilight/react/routes)
