Swapping a component: the registry
Replace the engine's product card everywhere by registering your own component under product:card.
Hook slots add content next to engine components. Sometimes you want to replace one instead: your own product card in every grid, slider and product list, without copying any of those pages.
For a few parts the engine does not import the component directly. It looks it up by name in the registry, a shared address book of name → component. ProductCard looks up product:card. Store your own component under that name, and every product card on the storefront becomes yours.
Your card next to the engine's
A real product from the store, drawn twice: once by the engine's ProductCard, once by a small card of your own. Here both are rendered directly; the registry is what would put yours in place of the engine's everywhere.
Loading a product…
// app/components/MyProductCard.tsx
import type { ProductCardProps } from '@salla.sa/twilight-theme-engine/components/product';
import { useMoney } from '@salla.sa/twilight-theme-engine/hooks/useMoney';
export function MyProductCard({ product }: ProductCardProps) {
const { format } = useMoney();
const price = product.is_on_sale && product.sale_price ? product.sale_price : product.price;
return (
<article style={{ display: 'grid', gap: 8, padding: 12, border: '2px solid', borderRadius: 16 }}>
<img
src={product.image?.url}
alt={product.image?.alt || product.name}
loading="lazy"
style={{ width: '100%', aspectRatio: '1 / 1', objectFit: 'cover', borderRadius: 12 }}
/>
<strong>{product.name}</strong>
<span style={{ fontSize: 18 }}>{format(price)}</span>
</article>
);
}
// app/router.tsx (excerpt)
import { registry } from '@salla.sa/twilight-theme-engine';
import { createRouter } from '@salla.sa/twilight-theme-engine/tanstack';
import { MyProductCard } from './components/MyProductCard';
import { routeTree } from './routeTree.gen';
// At module scope, before any page renders. For product:card, both calls:
registry.register('product:card', MyProductCard);
registry.override('product:card', MyProductCard);
export function getRouter() {
return createRouter(routeTree);
}
Why two calls
register stores a component under a name. override stores one too, but keeps whatever was there before as the original. ProductCard uses the registry only when an original exists, so product:card needs both calls. The lab below makes the same calls on a playground key and shows what ProductCard would do.
import { registry } from '@salla.sa/twilight-theme-engine';
import { MyProductCard } from './components/MyProductCard';
// app/router.tsx: at module scope, before createRouter()
registry.register('product:card', MyProductCard);
registry.override('product:card', MyProductCard);
// Where the engine renders <ProductCard product={…} /> (grids, sliders, listings),
// MyProductCard receives the same props, but only after both calls.
The registry for your own parts
// Your own swappable parts use the same registry, with a name of your own.
import { Component, registry } from '@salla.sa/twilight-theme-engine';
import { SeasonalPromo } from './SeasonalPromo';
registry.register('my-theme:promo', SeasonalPromo);
export function PromoArea() {
// Renders whatever is registered under the name, or the fallback.
return <Component name="my-theme:promo" title="Summer sale" fallback={null} />;
}
In engine terms
registryis oneComponentRegistryinstance exported from@salla.sa/twilight-theme-engine, withregister,override,resolve,getOriginal,has,list,listByPrefix,removeandclear(src/components/ComponentRegistry.ts).ProductCardresolvesproduct:cardin auseMemowith no dependencies, only whenregistry.getOriginal('product:card') !== null, and passes every prop through (src/components/product/ProductCard.tsx).ProductGalleryis looser: it renders whateverproduct:galleryresolves to, unless that isProductGalleryitself, soregisteralone is enough there.- Home-page blocks are registry entries too:
registerHomeComponents()from@salla.sa/twilight-theme-engine/routes/homeregisters each block underhome:<path>. - The registry is a module-level object: on the server one copy is shared by every request, so register once at startup, never per request or per render.
- Reference: registry, Component, registerComponents, ProductCard, registerHomeComponents.