Themes
Themes control the visual presentation of the storefront. Each theme is a folder under app/themes/<slug>/ with package.json, a client-safe index.js, a components/ directory, and optional i18n/ translations.
The registered theme id is always the full package name from package.json name — for example @bermooda/theme-default. The filesystem path and i18n path use bermooda.slug — for example app/themes/default/. The folder name must equal that slug.
Engine responsibilities
Section titled “Engine responsibilities”The theme engine does three jobs:
- Runtime validation —
defineTheme()checks the component map in a client-safe entry module. - Discovery and registration — bootstrap merges
package.jsonidentity with theindex.jsruntime export and stores the result in an in-memory registry keyed by full package id. - Resolution —
resolveActiveThemereads theactiveThemesetting from the database (TTL-cached for 5 minutes) and returns the matching manifest. The setting stores the full package id, not the slug.
Theme registry code lives in app/core/themes/index.server.js. That module is server-only — never import it in client bundles.
Discovery
Section titled “Discovery”Both the server registry and the client-safe component registry discover themes with Vite import.meta.glob('#/themes/*/index.js') plus sibling package.json files. Both globs stay in place: the client module needs its own eager graph for browser and SSR component resolution; the server module needs its own for bootstrap registration and engine checks.
Shared helpers live in app/core/themes/discover-shared.js:
buildMergedThemeManifest(pkg, runtime)— wrapsmergeExtensionPackageindexThemeManifest(registry, manifest)— indexes by packageidandslug
Failure modes
Section titled “Failure modes”Malformed packages (bad package.json identity, merge errors, invalid manifests) are skipped rather than crashing startup, with different logging on each surface:
| Surface | Behavior |
|---|---|
Server (discoverThemes) |
Log via #/utils/logger.server and skip (Skipping malformed theme) |
Client (storefront-components) |
Silent skip (no logger in the browser) |
Incompatible bermooda.engine ranges are soft-skipped on the server with a log (Skipping incompatible theme). Folder/slug mismatches (assertSlugMatchesFolder) are also treated as malformed and skipped. A missing package.json for a theme folder, or a duplicate slug, still throws on the server.
Id vs. slug
Section titled “Id vs. slug”Keep these two identifiers distinct:
| Value | Source | Used for |
|---|---|---|
| id | package.json name |
Registry key, activeTheme setting, theme settings prefix |
| slug | bermooda.slug |
Folder name under app/themes/, i18n path, display URLs |
bermooda.slug must match ^[a-z0-9]+(?:-[a-z0-9]+)*$. Bundled themes live at app/themes/<slug>/.
Next steps
Section titled “Next steps”- Package contract —
package.jsonfields,defineTheme, imports, and folder layout - Components — engine-required, route-required, and optional components
- Slots — named injection points for plugin blocks
- Create a theme — fork the default theme and activate it
- Theme API —
defineTheme,resolveActiveTheme, slot helpers, and constraints