Replace the product card

Use your own product card in every list, slider and grid the engine renders.

Framework: TanStack Start

Registration below lives in app/router.tsx, which is specific to TanStack Start. See what depends on the framework.

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 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

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

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

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

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

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

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 so currency and digits follow the locale.

Replace the whole product list page

The product lists work the same way one level up. Every list the engine serves (category, search, tag, brand and 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:

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

On this page