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:
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:
<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:
| Term | Meaning | Example |
|---|---|---|
| Section type | The class (your index.vue + manifest.json). One per folder. | Banner, Heading, ProductGrid |
| Section instance | One 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:
[
{ "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'stype.values— the prop your component receives asconfig.
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 to1for "only-one" sections like Header/Footer/ProductDetail;nullmeans 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:
<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
configlooks 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: "تخفيضات" }). TheuseTcomposable 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. —useStylesturns 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.
Header and Footer are special
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.vuedirectly (or via the layouts), not byTemplateSections. TemplateSectionsexplicitly filters Header/Footer instances out, in case a stale theme document accidentally has them intemplates.home:tsconst 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:
<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.