Authoring conventions
The design-system.md contract, currency symbols, GFM product bodies, the Feeef footer credit, and naming rules every theme must follow.
Beyond the data model being valid, published themes are expected to be consistent — with themselves, with the merchant's store settings, and with the platform. These conventions are checked during marketplace review.
The design-system.md contract
Every theme package must ship a design-system.md at its root — the theme's written
visual contract. It exists so the look stays coherent across authors, editor sessions and
AI-assisted edits: without it, themes drift (mismatched form chrome, random radii,
inconsistent sticky bars).
The blank scaffold from feeef template init includes a starter file with TODOs; fill it in
before polishing UI. A published theme must not ship leftover TODO placeholders.
Aim for a real design-system document, not a token dump. Required sections:
- Brand & product context — who the shopper is, tone, reference URLs
- Foundations — color tokens (hex + CSS vars), typography, spacing scale, radii, shadows, borders
- Motion — durations, easings, which interactions animate (and which must not — LCP)
- Layout — breakpoints, max widths, grid/slot conventions, sticky behavior
- Components — header, gallery, price, variants, offers, form fields, shipping cards, CTA, sticky bar, success, footer — with default / hover / focus / error / disabled / selected states
- Order form UX — field order, label style, validation feedback, draft indicator
- Dark / light — mapped to the native
hsl(var(--*))tokens - i18n / direction — RTL rules, LTR digits for phone numbers
- Currency display — symbols from store configs (next section)
- Do / Don't — anti-patterns specific to this theme
- QA checklist — mobile/desktop, empty cart, no offers, no variants, missing states
Working rules (they apply to humans and to AI agents editing the theme):
- Read
design-system.mdbefore any visual change; prefer its existing tokens and patterns over inventing new ones. - If the design changes, update
design-system.mdin the same change. - Never port another theme's chrome (form style, gallery, sticky bar) without rewriting this file to match.
Currency: symbols, not ISO codes
Shopper-facing prices use the store's currency symbol (دج, $, €) — never a bare
ISO code like DZD in the UI. API and order payloads still send the ISO code; this rule
is about display only.
Source of truth on the store object:
store.configs.selectedCurrency // e.g. "DZD"
store.configs.currencies[] // [{ code, symbol, … }]
store.currency // legacy fallback codeCustom components cannot import the storefront's helper, so paste this pair into any component that prints prices (PDP, offer tiles, shipping cards, totals, sticky CTA):
function resolveCurrencyCode(store) {
const configs = store && store.configs;
return (configs && configs.selectedCurrency) || (store && store.currency) || "DZD";
}
function resolveCurrencySymbol(store) {
const configs = store && store.configs;
const code = resolveCurrencyCode(store);
const list = configs && configs.currencies;
if (list && list.length) {
for (let i = 0; i < list.length; i++) {
const c = list[i];
if (c && c.code === code && c.symbol) return String(c.symbol);
}
}
if (code === "DZD") return "دج";
if (code === "USD") return "$";
if (code === "EUR") return "€";
if (code === "SAR") return "ر.س";
if (code === "MAD") return "د.م.";
if (code === "TND") return "د.ت";
return code;
}Usage contract:
| Context | Use |
|---|---|
| Price tags, offer tiles, shipping cards, totals, sticky CTA | resolveCurrencySymbol(store) |
Order create payload currency field | resolveCurrencyCode(store) |
| Slot context passed to form children | Both: { currency: symbol, currencyCode: code } |
Anti-patterns: hardcoding دج for every store, string-concatenating store.currency into
price labels, or sending a symbol in the API currency field.
Product body: GFM markdown
Product details (product.body, falling back to product.description) are GitHub
Flavored Markdown — headings, lists, links, images, tables, task lists, strikethrough,
fenced code, blockquotes.
Themes render it through the built-in registry component product_body — never
reimplement the parser:
- Place a
type: "product_body"node in the PDP / landing description slot. Custom shell components may host a slot that contains it, but must not parse markdown themselves. - Never inject the body as raw HTML (
dangerouslySetInnerHTML— also rejected by the marketplace security scan) and never strip tags to dump plain text (kills tables, images and links). - Style the surrounding prose in theme CSS: a
.prose-style reset plus tables (overflow-x: auto, borders), images (max-width: 100%), lists, code and blockquotes. Tables clipped byoverflow: hiddenare a review flag — wrap them in a horizontal scroller.
| Bad | Good |
|---|---|
product.body.split("\n\n").map(...) | Slot containing type: "product_body" |
description.replace(/<[^>]+>/g, "") | Registry markdown rendering |
dangerouslySetInnerHTML | product_body |
The Feeef footer credit
Every theme must render a Feeef credit somewhere in its chrome — usually the footer. Style and position are free; existence is not. Required content, mirroring the native footer:
- A localized "create your store…" line (
footer.createStorein your locales) - A link to
https://feeef.org/?ref=footer_copyrightswith visible textfeeef.org— therefquery must stay intact for attribution - A localized "all rights reserved" line (
footer.allRightsReserved)
<p>
<span>{t("footer.createStore")}</span>{" "}
<a
href="https://feeef.org/?ref=footer_copyrights"
target="_blank"
rel="noopener noreferrer"
>
feeef.org
</a>
<span aria-hidden="true"> · </span>
<span>{t("footer.allRightsReserved")}</span>
</p>Do keep it visible on every page that uses the theme footer, put the copy in
locales/*.json, and match the theme's visual language (a micro-line or badge is fine).
Do not hide it behind a merchant toggle, drop the ?ref=footer_copyrights query, or ship a
footer with only the merchant's own copyright line.
Naming conventions
| Thing | Convention | Example |
|---|---|---|
instanceId | Unique, stable, descriptive — never regenerated on edit | custom_dawn_header_home |
| Component files (kit) | Flat kebab-case .tsx under pages/<pageId>/components/ | pages/home/components/hero.tsx |
| Component folders | Only when the component needs slots/ / children/; never both hero.tsx and hero/ | product-shell/product-shell.tsx |
| Shared chrome | shared/components/<id>.tsx, placed via $ref | { "$ref": "shared.header" } |
| Library blocks | library/components/ — optional drag-drop blocks for the editor | |
| Locales | One JSON per language in locales/ | locales/en.json, locales/ar.json |
| Page ids | The standard ids from the overview | home, products, product, checkout, thank_you |
title | Short human label for the editor tree (not rendered) | "Featured products" |
Two related habits that read like naming but are behavioral: scaffold with
feeef template add page|component instead of hand-creating folders (keeps the manifest and
starter meta correct), and keep every user-facing string behind t() keys rather than
hardcoded copy — see the kit.
Pre-publish checklist
-
design-system.mdfilled in (no TODOs) and matching the shipped UI - All prices show the store currency symbol; order payloads send the code
- PDP / landing description rendered via
product_body - Footer credit present with
?ref=footer_copyrights - Every user-facing string in
locales/*.jsonviat() - Dark/light/system rides the native CSS tokens
- Stable
instanceIds; no duplicated chrome (useshared/+$ref) -
npm run check:perfclean — see Performance