Add content to a slot
Inject your own UI at a named point in the engine's pages without forking them.
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 a banner, badge or widget at a fixed point on a page and do not want to fork the page.
Goal: show a "free returns" note under the add-to-cart form on the product page.
Engine pages render <HookSlot name="..." /> at fixed points. You register a handler for a slot name in hookRegistry. The slot calls every handler, highest priority first, and renders what they return. Each handler gets a context with the theme data (twilight) plus whatever the page passes in. More: Hooks and slots, HookSlot, useHook.
Register a handler
import { hookRegistry, HookName, type HookContext } from '@salla.sa/twilight-theme-engine/hooks';
import type { Product } from '@salla.sa/twilight-theme-engine/types';
export function registerThemeHooks() {
hookRegistry.register(
HookName.PRODUCT_DESCRIPTION_END,
(context: HookContext) => {
const { product } = context as { product?: Product };
if (!product) return null; // guard in case the slot is rendered without one
return <p className="returns-note">Free returns within 14 days</p>;
},
50 // priority: higher runs first, default 50
);
}Call it once at module scope
import { registerThemeHooks } from './hooks';
registerThemeHooks();Check it worked
Open a product page: the note appears under the add-to-cart form. Open the same page in the other language and it is still there (translate the text with t() in a real theme).
Slot names
These are rendered by the engine's pages and layout:
| Area | Slots |
|---|---|
| Page shell | body:start, body:end, header:start, header:end, footer:start, footer:end |
| Home | home:start, home:content, home:end |
| Product | product:start, product:single.description.start, product:single.description, product:single.description.end, product:single.form.start, product:single.form.end, product:related.start, product:related.end, product:end |
| Listings | product:list.start, product:list.items.start, product:list.items.end, product:list.end |
| Cart | cart:start, cart:items.start, cart:items.end, cart:summary.start, cart:summary.end, cart:end |
Blog, brands, thank-you and not-found pages have slots too, such as blog:tag.start, brands:index.items.start, thank-you:start and notfound:start.
Traps
- Not every
HookNameentry is rendered.HOMEPAGE_SLIDER,HOMEPAGE_BANNERSandHOMEPAGE_FEATURED(homepage:*) have no slot in the engine, so a handler there never shows. Usehome:start,home:contentorhome:end. - A forked page only has the slots you re-render. If you fork a route, keep its
HookSlots. - A handler that throws renders nothing. The error is swallowed (logged in development only).
- Return
nullwhen there is nothing to show. The slot renders whatever you return. - Never register inside a component. It adds a duplicate handler on every mount.
- The context type is loose. Cast it and guard for missing values.
Next
- Fork a built-in route when a slot is not enough.
- Hooks and slots for the full model.