# Replace the product card (https://docs.salla.dev/twilight/react/recipes/replace-the-product-card)



<Callout title="Framework: TanStack Start">
  Registration below lives in `app/router.tsx`, which is specific to TanStack Start. [See what depends on the framework](https://docs.salla.dev/twilight/react/start/frameworks).
</Callout>

Use this when you want one product card design everywhere a product is shown.

**Goal:** the engine's listing pages and home blocks all render `MyProductCard`.

The engine's [`ProductCard`](https://docs.salla.dev/twilight/react/components/product-card) checks a component registry for the key `product:card` and renders the override if one exists. Listing pages and the engine's product home blocks all render that same `ProductCard`, so one override covers them. Pages you write should import `ProductCard` from `@salla.sa/twilight-theme-engine/product` (not from a deep path) so they get it too.

### Write the card [#write-the-card]

It receives the engine's `ProductCardProps` (`product`, `layout`, `className`, ...).

```tsx title="app/components/product/MyProductCard.tsx"
import type { ProductCardProps } from '@salla.sa/twilight-theme-engine/product';
import { useMoney } from '@salla.sa/twilight-theme-engine/hooks/useMoney';

export function MyProductCard({ product, className }: ProductCardProps) {
  const { format } = useMoney();
  const onSale = product.is_on_sale && product.sale_price;
  return (
    <div className={`s-product-card-entry ${className ?? ''}`}>
      <a href={product.url}>
        <img src={product.image?.url} alt={product.image?.alt ?? product.name} loading="lazy" />
        <h3>{product.name}</h3>
      </a>
      <span>{format(onSale ? product.sale_price : product.price)}</span>
    </div>
  );
}
```

### Register, then override [#register-then-override]

Both calls are needed (see the first trap). Do it at module scope.

```tsx title="app/router.tsx"
import { registry } from '@salla.sa/twilight-theme-engine';
import { MyProductCard } from './components/product/MyProductCard';

registry.register('product:card', MyProductCard);
registry.override('product:card', MyProductCard);
```

## Check it worked [#check-it-worked]

Open a category page and the home page: every product uses your markup.

<Callout type="warn" title="Traps">
  * **`override` alone does nothing.** The engine only switches when the registry has an original entry, and `override` records one only if the key already exists. Without a prior `register`, the built-in card keeps rendering.
  * **Do not render the engine card inside your override.** It resolves to your component again and loops forever. A wrapper around the engine card works only for pages that import it directly, as the reference theme's `ProductCard` does for its own listing pages.
  * **Register before the first render.** The engine reads the override once per card, so late registration is ignored by cards already mounted.
  * **Cover what the card does.** The built-in card handles the wishlist, add-to-cart (`salla-add-product-button`), out-of-stock and badges. Drop them and you drop the feature.
  * **Format raw prices** with [useMoney](https://docs.salla.dev/twilight/react/hooks/use-money) so currency and digits follow the locale.
</Callout>

## Replace the whole product list page [#replace-the-whole-product-list-page]

The product lists work the same way one level up. Every list the engine serves ([category](https://docs.salla.dev/twilight/react/routes/category), [search](https://docs.salla.dev/twilight/react/routes/search), [tag](https://docs.salla.dev/twilight/react/routes/tag), [brand](https://docs.salla.dev/twilight/react/routes/brand) and [products](https://docs.salla.dev/twilight/react/routes/products)) renders whatever is registered as `product:list-page`, and shows `product:list-skeleton` while it loads. Register your own page once instead of replacing each route:

```tsx title="app/router.tsx"
import { lazy } from 'react';
import { registry } from '@salla.sa/twilight-theme-engine';
import { MyListSkeleton } from './components/product/MyListSkeleton';

// Lazy, so the page loads with a product list instead of with every page.
const MyListPage = lazy(() =>
  import('./components/product/MyListPage').then((m) => ({ default: m.MyListPage }))
);

registry.register('product:list-page', MyListPage);
registry.register('product:list-skeleton', MyListSkeleton);
```

* **The page** receives the loader data, `ProductListLoaderData` from `@salla.sa/twilight-theme-engine/routes/product-listing`: `page`, `source`, `query`, `products`, `pagination` and, for categories and search, `filters`. Branch on `source.type` where the lists should differ.
* **The skeleton** receives `{ hideFilters }`, which is `true` on every list but categories and search.
* **A plain `register` is enough** for these two keys; there is no `override` step. Without them, the engine's own `ProductListPage` and page skeleton render.
* **Keep what the engine page does** if you need it: the `product:list.*` hook slots, infinite scroll from `pagination.next`, the sort menu and the filters. The reference theme's `app/components/product/ProductListPage.tsx` is a complete example.

## Next [#next]

* [ProductCard](https://docs.salla.dev/twilight/react/components/product-card): the props and layouts you can reuse.
* [Add a home block](https://docs.salla.dev/twilight/react/recipes/add-a-home-block) and [Fork a built-in route](https://docs.salla.dev/twilight/react/recipes/fork-a-built-in-route) when you need more than the card.
