Add a home block

Create a section merchants can add to the home page and render it from your theme.

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 the home page needs a section the engine does not ship, and you want merchants to place and fill it from the dashboard.

Goal: a brands section with a title and a list of brands.

Merchants arrange sections from the components declared in twilight.json (Settings). The home loader fetches that list and strips the home. prefix from each path. The renderer then looks up home:<path> in a registry and renders your component with the block's values as a data prop. See also Layout and components.

Declare the block

The path is home. plus your key. Each field id becomes a key on data. The brands field below is a picker the merchant fills with their brands.

twilight.json
{
  "components": [
    {
      "key": "6b0f2c2e-0a4e-4c1b-9d57-3d2f8a7e1c10",
      "title": { "en": "Brands", "ar": "الماركات التجارية" },
      "icon": "sicon-award-ribbon",
      "path": "home.brands",
      "fields": [
        { "type": "string", "id": "title", "label": "Title", "multilanguage": true, "format": "text" },
        {
          "type": "items",
          "id": "brands",
          "label": "Brands",
          "format": "dropdown-list",
          "source": "brands",
          "multichoice": true,
          "searchable": true,
          "required": true,
          "selected": [],
          "options": []
        }
      ]
    }
  ]
}

Write the component

It receives { data }.

app/components/home/Brands.tsx
import { memo } from 'react';
import type { Brand } from '@salla.sa/twilight-theme-engine/types';
import { Link } from '@salla.sa/twilight-theme-engine/common';

export const Brands = memo(function Brands({ data }: { data: { brands: Brand[]; title?: string } }) {
  if (!data.brands.length) return null;
  return (
    <div className="container">
      {data.title && <h2>{data.title}</h2>}
      {data.brands.map((b) => (
        <Link key={b.id} to={b.url}>{b.name}</Link>
      ))}
    </div>
  );
});

Register it

Register it next to the engine's defaults. The optional config reserves height and a class on the wrapper section so the page does not jump while the block loads.

app/router.tsx
import {
  registerHomeComponents,
  registerHomeComponentConfig,
  DefaultHomeComponents,
} from '@salla.sa/twilight-theme-engine/routes/home';
import { Brands } from './components/home/Brands';

registerHomeComponents({
  ...DefaultHomeComponents,
  brands: Brands,
});

registerHomeComponentConfig({
  brands: { height: '300px', className: 's-block s-block--brands' },
});

Check it worked

Add the block in the theme editor, pick a few brands, and open the home page: your list renders. With no block placed, nothing changes.

Traps

  • Omit ...DefaultHomeComponents and every built-in block is gone. Nothing else registers the engine's blocks, so only what you pass is available.
  • The key is path minus home.. home.brands registers as brands. A mismatch shows a yellow "Unknown component" box in development and nothing in production.
  • view_style wins. For a block with view_style: "style2" the renderer tries featured-products:style2 before featured-products. To replace a styled block, register the :styleN keys too (the reference theme does).
  • Config replaces, not merges. A theme entry in registerHomeComponentConfig replaces the engine's entry for that path entirely.
  • Register at module scope in app/router.tsx, not inside a component, so the server and the browser agree.
  • Blocks mount when scrolled into view, except the first three, which render immediately. Do not rely on effects running at page load for later blocks.

The "order again" row

One row on the home page is not a block: ReorderProducts, from @salla.sa/twilight-theme-engine/components/home. The engine's HomePage renders it after the blocks and the home:end slot. It shows only to a signed-in customer, and only when the store's features include reorder (the merchant turned Reorder on). It lists the products that customer ordered before, drawn with the products-slider component you registered, titled with the pages.orders.repurchase translation and linking to /products?source=reorder. Replacing products-slider therefore changes this row too.

Inside the row, each add button reads "order again" and adds the product as the customer last ordered it. useReorderButtons(enabled) does the same for your own markup: call it, and put the products inside an element with data-source="reorder".

app/components/product/ReorderGrid.tsx
import type { ReactNode } from 'react';
import { useReorderButtons } from '@salla.sa/twilight-theme-engine/components/home';

export function ReorderGrid({ children }: { children: ReactNode }) {
  useReorderButtons(true);
  return <div data-source="reorder">{children}</div>;
}

The Salla SDK cannot remove a hook, so the button change is registered once and stays until the next full page load.

Next

On this page