Storefront themes and configuration
A shop’s storefront is a theme plus a configuration (ADR-013). The Storefront Settings page and the internal AI agent (MCP, phase E) both edit the configuration through one service, App\Services\Storefront\StorefrontConfig. Themes receive it read-only through the Storefront API (GET /shop).
Themes and manifests
Section titled “Themes and manifests”Each theme lives in storefront/themes/<id>/ with a theme.json manifest next to its Astro templates (runtime and themes). ThemeRegistry loads and validates every manifest. A malformed manifest fails loudly instead of loosening validation. A manifest declares:
- the theme’s id, name, semantic version and description (in English and Ukrainian);
- the templates it provides (home, catalogue, bouquet, flower, builder, cart, checkout, order, not-found);
- its global settings and the section types a shop can arrange, each with a maximum count and its own settings;
- the default sections a new shop starts with.
Settings use a small typed DSL (App\Support\Storefront\SettingSchema), not free JSON Schema. Each type has bounds:
| Type | Value |
|---|---|
text, textarea |
Plain text up to max characters; < and > are refused |
color |
#rrggbb; optionally a contrast floor against the theme’s background (WCAG ratio) |
select |
One of the declared options |
range |
An integer between min and max, in steps |
toggle |
true or false |
image |
An upload of this shop (workspaces/<id>/storefront/…) or bouquet:<id>, meaning that bouquet’s photo |
url |
https, optionally limited to allowed hosts |
bouquet_list, category_list |
Up to max distinct ids of this shop’s visible bouquets or its categories |
occasion_list |
Up to max occasion keys |
The same declarations drive the validation rules, the settings form, and (in phase E) the description the AI agent reads. A theme therefore cannot widen what a shop’s configuration may contain.
Two themes ship: Bloom, light and photo-led with nine section types, and Noir, dark and editorial with four section types and different settings. Noir exists to prove that switching themes and per-theme settings work.
The configuration document
Section titled “The configuration document”{ "schema": 1, "theme": "bloom", "profile": { "tagline": "…", "phone": "…", "hours": { "mon": { "open": "09:00", "close": "18:00" } }, "links": { "instagram": "https://instagram.com/…" } }, "themes": { "bloom": { "settings": { "accent": "#7a2e2a" }, "sections": [ { "id": "hero-1", "type": "hero", "enabled": true, "settings": {} } ] } }}- The profile (
StorefrontProfile) is independent of the theme. Every field is optional, and a theme shows a block only when its data is present, so nothing is invented. Text is plain (no markup). Each social link must point at its own service over https; Viber is a phone number. - Per-theme settings and sections. Switching theme keeps every theme’s own settings, so switching back restores them.
- Validation is all or nothing. A change is merged into the draft and the whole document is validated. Any error refuses the change and saves nothing, and the errors name document paths (
themes.bloom.sections.2.settings.limit). Ids must belong to the shop and be visible, and uploads must sit in the shop’s own folder.
Draft, publish, revert, preview
Section titled “Draft, publish, revert, preview”Rows of storefront_versions:
- Draft. One per shop, edited in place.
- Published. Each publication copies the draft into the next numbered version and archives the previous one. Published and archived versions are never edited. Partial unique indexes allow one draft and one published version per shop.
- Revert. Copies an earlier version back into the draft, so restoring it takes one more publish.
- Audit. Every change records its source (
settings,mcp,seedorrevert) and the member or operator who made it. - Concurrency. Writers lock the shop’s row, so the settings page and the agent cannot interleave.
- Default. A shop that never configured anything is served the Bloom defaults with an empty profile.
A preview token (PreviewToken) lets a storefront render the draft:
- It is HMAC-signed with the app key, scoped to one shop, and expires after an hour.
- It is issued only to members with
settings.edit(the “Preview draft” button) and, later, to the internal agent. GET /shop?preview=<token>then returns the draft withpreview: trueandCache-Control: no-store. Without a valid token, everyone gets the published version.
Settings → Storefront
Section titled “Settings → Storefront”The page is available to owners and admins (settings.edit). It has four tabs:
- Shop details: the profile, opening hours, links and logo.
- Theme: the theme picker and its settings, generated from the manifest.
- Home page: sections can be switched on or off, reordered, added up to each type’s maximum, removed, and edited with fields generated from the manifest.
- History: published versions, with “Restore into draft”.
“Save draft” sends only the current tab’s part as a patch. “Publish” takes an optional note. Images go up through POST /control/settings/storefront/images: JPEG, PNG or WebP, up to 5 MB and 6000 px, stored in workspaces/<id>/storefront/ under a random name.
Images fetched by URL
Section titled “Images fetched by URL”App\Support\SafeImageDownloader is the only way the server fetches an image by URL (AI results today, agent tools later):
- https only, and no credentials in the URL;
- every address the host resolves to must be public, and the connection is pinned to the checked address, so DNS cannot be re-pointed in between;
- no redirects;
- the body must decode as a JPEG, PNG or WebP image of at most 5 MB.
It replaced a download in FlowerController that fetched any URL it was given.