Skip to content
On this page

Anatomy of a section ​

A section is one folder under components/sections/ with exactly two files:

components/sections/MyBanner/
  ├── index.vue       ← the Vue component (what renders)
  └── manifest.json   ← editable fields, defaults, styleKeys (what the builder shows)

That's it. The folder name must match manifest.type exactly (MyBanner here). The auto-registration in utils/sectionRegistry.ts uses this convention to wire the two files together.

How the system finds your section ​

When the storefront boots, utils/sectionRegistry.ts runs:

ts
import.meta.glob('../components/sections/*/manifest.json', { eager: true });
import.meta.glob('../components/sections/*/index.vue', { eager: true });

Vite resolves both globs at build time. By the time any page renders, the registry already has every section indexed. Adding a new folder = adding a new section. No registration call, no central array.

When a page renders, components/TemplateSections.vue walks themeStore.theme.config.templates.<pageKey> (an array of section instances stored in the database) and asks the registry for each section's component:

vue
<component
  :is="resolveSection(section.type) ?? 'div'"
  v-for="section in sections"
  :key="section.id"
  :config="section.values"
  :data-section-id="section.id"
  :data-section-type="section.type"
/>

So your component receives a single prop: config. Everything the merchant typed in the builder lives there.

A section instance vs a section type ​

These two terms come up constantly. Don't confuse them:

TermMeaningExample
Section typeThe class (your index.vue + manifest.json). One per folder.Banner, Heading, ProductGrid
Section instanceOne placement of a section on a page, with its own filled-in values. Stored in MongoDB.A Banner on the home page with title="Summer sale", and a different Banner on /shop with title="Outlet"

A page (e.g. templates.home) is just a JSON array of section instances:

json
[
  { "id": "sec_abc123", "type": "Banner", "values": { "title": "Summer sale", "layout": "image-bg" } },
  { "id": "sec_def456", "type": "Heading", "values": { "text": "New arrivals" } },
  { "id": "sec_ghi789", "type": "ProductGrid", "values": { "limit": 8 } }
]

Each instance carries:

  • id — unique, generated by the builder.
  • type — must match a registered manifest's type.
  • values — the prop your component receives as config.

File 1: manifest.json ​

The manifest declares everything the builder needs to know about your section:

  • type — must match the folder name.
  • title, description, icon, preview — how it appears in the section library.
  • category — groups the section in the library (text, media, cta, blog, ...).
  • pages — which page templates this section is allowed on (["home"], ["any"], ["product", "page"], ...).
  • maxPerTemplate — set to 1 for "only-one" sections like Header/Footer/ProductDetail; null means unlimited.
  • schema — the editable fields shown in the right-hand inspector. Each entry is one field.
  • defaults — the values a fresh instance starts with.
  • aiHints — keywords the AI generator uses to decide when to pick this section.
  • styleKeys (optional) — named element scopes the merchant can recolor independently.

The detailed reference is in Writing the manifest. For now, scan the Heading/manifest.json file — every field above is illustrated there.

File 2: index.vue ​

The Vue component. Its only required interface is:

vue
<script setup lang="ts">
import { useStyles, type ElementStylePayload } from '~/composables/useStyles';

interface MyBannerConfig {
  title?: string;
  // ...other fields matching your manifest schema...
  styles?: Record<string, ElementStylePayload>;
}

const props = defineProps<{ config: MyBannerConfig }>();
</script>

<template>
  <section>
    {{ props.config.title }}
  </section>
</template>

A few conventions you'll see in every section:

  • Typed config interface. Always declare what config looks like. The builder writes anything; you defend against missing fields with optional chaining and defaults.
  • useT() for translatable text. Field values like the title can be either a string or a per-locale map ({ en: "Sale", ar: "تخفيضات" }). The useT composable resolves to the active language. See Writing the component.
  • useStyles() for editable colors / sizes. Merchants can change a section's background, the title color, etc. — useStyles turns the merchant's edits into the right inline styles.
  • useEditable() for inline text editing. Lets the merchant click directly on a heading inside the iframe and type a new value. Optional but very nice.

Two section types — Header and Footer — don't live in config.templates.<page>. They live in config.global.header and config.global.footer. There's only one Header and one Footer per theme, rendered on every page.

Practically, this means:

  • The folder pattern is identical (components/sections/Header/, components/sections/Footer/).
  • The manifest is identical.
  • The Vue component is identical.
  • But you'll find them rendered by app.vue directly (or via the layouts), not by TemplateSections.
  • TemplateSections explicitly filters Header/Footer instances out, in case a stale theme document accidentally has them in templates.home:
    ts
    const HIDDEN_FROM_TEMPLATES = new Set(['Header', 'Footer']);

If you're building a Header / Footer variant, follow the section pattern as usual. The merchant picks which Header to use via the builder's chrome inspector, not by dragging it onto a page.

For any other section work (your Banner, Hero, ProductGrid, etc.), ignore Header/Footer and just use the standard templates.<page> flow.

What about CSS? ​

Tailwind, everywhere. The theme imports Tailwind once globally; your section uses utility classes inline. Tokens (the merchant's brand colors, fonts, radii) are exposed as CSS variables:

html
<h2 class="text-ink"><!-- merchant's chosen text color --></h2>
<button class="bg-accent text-surface rounded-token-button">
  <!-- accent background, surface text, button radius -->
</button>

The token list (--token-primary, --token-accent, --token-text, --token-fontHeading, etc.) is defined per theme in config.tokens and re-emitted as CSS variables by plugins/theme-vars.client.ts. As a section author, just use the Tailwind classes — text-ink, bg-accent, font-heading — and the merchant's values flow through.

What's next ​

Open Writing the component for the Vue side, or Writing the manifest for the schema side. They're independent — pick whichever you'll work on first.

Released under the MIT License.