Adding content without editing a page: hook slots
Engine pages leave named slots; register a function for a name and what it returns appears on every page with that slot.
Say you want a delivery note under the "Add to cart" button of every product. The product page belongs to the engine, and copying it just to add one line means maintaining a whole page forever.
Engine pages leave named, empty places for exactly this: hook slots. product:single.form.end is the place right after the add-to-cart form. Your theme registers a small function under that name, and whatever the function returns appears there, on every page that has the slot.
A slot and its handler, live
The dashed box is a HookSlot with a name of the playground's own. The function registered for it, the handler, receives the slot's context (here, badge) and the engine's context (twilight: store, theme, language).
import { HookSlot } from '@salla.sa/twilight-theme-engine/hooks/HookSlot';
export function PromoArea() {
return (
<HookSlot
name="playground:hook-slot"
context={{ badge: 'Free delivery' }}
fallback={<em>{'No handler for this slot'}</em>}
/>
);
}
Where the engine leaves slots
Pick an area to see its slot names and where each one sits.
| Slot name | Where it appears |
|---|---|
head:start · head · head:end | At the top of the page body, despite the names: not inside the head element. |
body:start | Before the layout. |
body:end | After the page, at the end of the body. |
Registering handlers in a theme
Keep every registration in one file and run it once, when the theme starts. Registration adds a handler; it never replaces one, so running the file twice shows everything twice.
// app/hooks/index.tsx
import { hookRegistry, HookName, type HookContext } from '@salla.sa/twilight-theme-engine/hooks';
let registered = false;
export function registerThemeHooks() {
if (registered) return; // registering twice renders everything twice
registered = true;
// Appears after the add-to-cart form, on every product page.
// HookContext types the argument: the slot's context plus `twilight`.
hookRegistry.register(HookName.PRODUCT_FORM_END, ({ twilight }: HookContext) => (
<p className="text-sm">Delivered by {twilight.store.name} in 2 to 4 days.</p>
));
// Priority decides the order when several handlers share a slot: higher first (default 50).
hookRegistry.register(
HookName.CART_SUMMARY_START,
() => <p className="text-sm">Free delivery on orders over 200 SAR</p>,
80
);
}
// app/router.tsx (excerpt): register once, before the router is created
import { registerThemeHooks } from './hooks';
registerThemeHooks();
// Your own components can offer slots too. Use a name of your own.
import { HookSlot } from '@salla.sa/twilight-theme-engine/hooks/HookSlot';
export function LookbookPage({ season }: { season: string }) {
return (
<main>
<h1>Lookbook</h1>
<HookSlot name="my-theme:lookbook.start" context={{ season }} fallback={null} />
</main>
);
}
In engine terms
hookRegistry.register(name, handler, priority = 50)appends and sorts handlers by priority, highest first, then notifies subscribers.clear(name)removes all of a slot's handlers;defineHooks({...})registers many at once (src/hooks/HookRegistry.ts).<HookSlot name context? fallback? ssr? />subscribes to the registry, so a handler registered after the slot appeared shows up at once. It renders each handler with{ ...context, twilight: useTwilight() }.fallbackshows only while no handler is registered: handlers that all returnnullleave the slot empty, fallback included (src/hooks/HookSlot.tsx).- Next to your handlers it renders Salla's
salla-hookelement, where installed apps inject their own markup: only after hydration, unlessssris set. - A handler runs each time its slot renders, on the server as well as in the browser. When the content needs state or effects, return a component of your own from it.
- Import
hookRegistry,HookNameandHookSlotfrom@salla.sa/twilight-theme-engine/hooks(or the/hooks/HookRegistryand/hooks/HookSlotsubpaths). - Reference: HookSlot, hookRegistry, HookName; the recipe Add content to every page with a hook slot.