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

app/hooks/index.tsx
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

app/router.tsx
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:

AreaSlots
Page shellbody:start, body:end, header:start, header:end, footer:start, footer:end
Homehome:start, home:content, home:end
Productproduct: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
Listingsproduct:list.start, product:list.items.start, product:list.items.end, product:list.end
Cartcart: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 HookName entry is rendered. HOMEPAGE_SLIDER, HOMEPAGE_BANNERS and HOMEPAGE_FEATURED (homepage:*) have no slot in the engine, so a handler there never shows. Use home:start, home:content or home: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 null when 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

On this page