Skip to content
Twilight React Playground
ثيم رائدaren

Add a home page block

Beginner10 min

Write a block component, register it next to the default blocks, and declare its fields in twilight.json.

Goal: give merchants a new block they can place on their home page and fill in from the Salla dashboard.

Mechanism: two halves that meet on one name. In twilight.json you declare the block: its title, its icon and the fields the merchant fills in, under a path such as home.trust-badges. In app/router.tsx you register the component that draws it under trust-badges. When the home page loads, the store sends the blocks the merchant placed, in order, each with its field values; the engine finds your component by the name and hands it those values as one data prop.

1. Try it

The block from step 2, with field values you type. This is exactly what it receives once a merchant fills in the fields.

Why shop with us

  • Fast delivery
  • Secure payment
  • Easy returns

And how the engine finds a block by name, on a path of the playground's own: untick registered to see what an unregistered block looks like.

A block list entry with path "playground:notice", drawn by HomeComponentRenderer before and after its component is registered.Try this: untick registered: the renderer finds nothing under home:playground:notice and shows its fallback (empty in production).
Storefront canvas · ar · RTL

registry.has('home:playground:notice'): false

Controls
registered
What a theme writes
import { createRouter } from '@salla.sa/twilight-theme-engine/tanstack';
import {
  DefaultHomeComponents,
  registerHomeComponents,
  type HomeComponentsProps,
} from '@salla.sa/twilight-theme-engine/components/home';
import { routeTree } from './routeTree.gen';

// A block the merchant adds with path "notice" gets its settings in `data`.
function Notice({ data }: HomeComponentsProps) {
  return <p className="container">{String(data.text ?? 'Free delivery this week')}</p>;
}

// app/router.tsx, at module scope: runs on the server and in the browser.
registerHomeComponents({
  ...DefaultHomeComponents,
  notice: Notice,
});

export function getRouter() {
  return createRouter(routeTree);
}

2. Write the block

app/components/home/TrustBadges.tsx
import { memo } from 'react';
import type { HomeComponentsProps } from '@salla.sa/twilight-theme-engine/routes/home';

/** One row of the `items` collection declared in twilight.json. */
interface Badge {
  icon?: string;
  text?: string;
}

/**
 * A home page block. Each field the merchant fills in for `home.trust-badges`
 * arrives in `data` under the field's `id`, already in the page's language.
 */
export const TrustBadges = memo(function TrustBadges({ data }: HomeComponentsProps) {
  const title = typeof data.title === 'string' ? data.title : '';
  const items: Badge[] = Array.isArray(data.items) ? data.items : [];

  // Placed on the page but not filled in yet: draw nothing rather than an empty box.
  if (items.length === 0) return null;

  return (
    <section className="container py-6">
      {title && <h2 className="mb-4 text-lg font-bold">{title}</h2>}
      <ul className="grid grid-cols-2 gap-4 md:grid-cols-4">
        {items.map((item, index) => (
          <li
            // Rows have no id, and a merchant may repeat a text: the position keeps keys unique.
            key={`${index}:${item.text ?? ''}`}
            className="flex items-center gap-3 rounded-md bg-gray-100 p-3"
          >
            {item.icon && <i className={`${item.icon} text-2xl text-primary`} aria-hidden="true" />}
            <span className="text-sm">{item.text}</span>
          </li>
        ))}
      </ul>
    </section>
  );
});

Each field arrives under its id. A collection field arrives as an array, with its sub-field ids stripped of the prefix (items.text becomes text), and a multilanguage field arrives already in the page's language:

What data looks like
// One entry of the block list the store sends for the home page
{
  "path": "home.trust-badges",          // the engine's loader turns this into "trust-badges"
  "title": "Why shop with us",          // field "title", in the page's language
  "items": [                            // field "items": the "items." prefix is gone
    { "icon": "sicon-shipping-fast", "text": "Fast delivery" },
    { "icon": "sicon-lock", "text": "Secure payment" }
  ]
}

3. Register it next to the default blocks

app/router.tsx (excerpt)
import { createRouter } from '@salla.sa/twilight-theme-engine/tanstack';
import {
  DefaultHomeComponents,
  registerHomeComponentConfig,
  registerHomeComponents,
} from '@salla.sa/twilight-theme-engine/routes/home';
import { routeTree } from './routeTree.gen';
import { TrustBadges } from './components/home/TrustBadges';

registerHomeComponents({
  ...DefaultHomeComponents, // keep the built-in blocks
  'trust-badges': TrustBadges, // path "home.trust-badges" in twilight.json, without "home."
});

// Optional: the space held for the block until it scrolls into view.
registerHomeComponentConfig({
  'trust-badges': { height: '120px' },
});

export function getRouter() {
  return createRouter(routeTree);
}

registerHomeComponents and DefaultHomeComponents are exported from /routes/home and from /components/home alike; the reference theme imports them from /routes/home.

4. Declare it in twilight.json

twilight.json (the components entry)
{
  "components": [
    {
      "key": "5c1f6f0e-8a41-4a7e-9d0b-6f3e2a9c1b27",
      "title": { "ar": "شارات الثقة", "en": "Trust badges" },
      "icon": "sicon-award-ribbon",
      "path": "home.trust-badges",
      "fields": [
        {
          "id": "title",
          "type": "string",
          "format": "text",
          "label": "العنوان",
          "multilanguage": true,
          "required": false,
          "value": null
        },
        {
          "id": "items",
          "type": "collection",
          "format": "collection",
          "label": "الشارات",
          "item_label": "شارة",
          "required": true,
          "minLength": 1,
          "maxLength": 4,
          "fields": [
            { "id": "items.icon", "type": "string", "format": "icon", "label": "الأيقونة", "value": "sicon-shipping-fast" },
            { "id": "items.text", "type": "string", "format": "text", "label": "النص", "multilanguage": true }
          ]
        }
      ]
    }
  ]
}
In engine terms

homeLoader (src/routes/home/loader.ts) reads home.queries.components() and replaces home. in each path. HomeComponentRenderer (src/components/home/HomePageRenderer.tsx) resolves home:<path>:<view_style> first, then home:<path>, and renders <Component data={{ ...data, position, priority }} /> inside ComponentErrorBoundary. The first three blocks render at once; later ones wait in RenderWhenVisible with the height and placeholder from registerHomeComponentConfig, or 400px and a products-slider skeleton when none is registered. registerHomeComponents(components, prefix = 'home:') registers prefix + key for every non-null value and removes nothing.

Traps