Brand page

The product listing for one brand, with the brand banner and logo.

Framework: TanStack Start

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

The customer sees one brand's banner, logo and description, with its products below. They reach it from the brands index or a brand link on a product. Customise this route to add a canonical tag, change the title, or render the list with your own component.

URL

PatternExample
/brands/$id/brands/77
/{locale}/brands/$id/en/brands/77

The short form /$slug/brand-{$id} redirects here (Brand products).

Params

NameTypeExampleDescription
idstring77The brand id. There is no slug segment.
localestring | undefinedenOptional (locale prefix).

Search params

NameTypeExampleDescription
sortstringbestSellOptional. The loader uses ourSuggest when absent.
pagenumber2Optional. 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.

Try it

This is a brand as the engine renders it, running against the demo store. Switch the language or the width. Change the id to open another one.

/ar/brands/1146128145Open
Route
/{locale}/brands/$id
This address becomes
  • locale = ar
  • path = /brands/1146128145

Route file

The engine writes this file into its own .twilight/ folder, not into app/routes/:

brands.$id.tsx (served by the engine)
// @auto-generated
import { createFileRoute } from '@tanstack/react-router';
import { ProductListing } 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}/brands/$id')({
  validateSearch: (search: Record<string, unknown>) => {
    const page = Number(search.page) || 1;
    const sort = (search.sort as string) || undefined;
    return { ...(page > 1 ? { page } : {}), ...(sort ? { sort } : {}) };
  },
  loaderDeps: ({ search }) => ({ page: search.page, sort: search.sort }),
  loader: ({ deps, params }): Promise<ProductListLoaderData> =>
    ProductListing.loader({
      params: { source: 'brands', id: params.id },
      search: { page: deps.page, sort: deps.sort },
      locale: params.locale,
    }),
  head: withHead(ProductListing),
  pendingComponent: () => <ProductListing.Pending hideFilters />,
  component: ProductListingComponent,
});

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

To take the page over, add app/routes/brands.$id.tsx without the // @auto-generated line, or write your own file and map the path in app/routes.ts: route('/brands/$id', 'brands-page.tsx'). See 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.

A customised version

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

app/routes/brand-page.tsx
import { createFileRoute } from '@tanstack/react-router';
import {
  ProductListing,
  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}/brands/$id')({
  validateSearch: (search: Record<string, unknown>) => {
    const page = Number(search.page) || 1;
    const sort = (search.sort as string) || undefined;
    return { ...(page > 1 ? { page } : {}), ...(sort ? { sort } : {}) };
  },
  loaderDeps: ({ search }) => ({ page: search.page, sort: search.sort }),
  loader: ({ deps, params }): Promise<ProductListLoaderData> =>
    ProductListing.loader({
      params: { source: 'brands', id: params.id },
      search: { page: deps.page, sort: deps.sort },
      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

ProductListLoaderData, the same shape as on the category page. For brands:

  • The loader fetches the brand and its first products together on the server. source.entity is the Brand (name, logo, banner, description, url).
  • query.filters is false, so there is no filter panel.
  • Breadcrumbs are Home, then the brand name.
  • The page draws the banner, logo, name and description (when set) above the list.

The page has the same hook slots as every product list (product:list.*).

Head and SEO

Title is the brand name and description is Browse {name}, with Open Graph and Twitter cards and hreflang links. Unlike categories and tags, there is no canonical tag and no social image. Add them with a withHead extension if you need them.

Edge cases

  • Unknown brand. If a request fails or the brand comes back empty, the loader throws a not-found error and the not-found page renders.
  • Brand without products. The list shows its empty state. There is no 404.
  • Short URLs. /nike/brand-77 redirects here and keeps the locale.

See also

On this page