Theme Integration
Commerce is presented by the active theme, but a theme never imports the Commerce plugin. All interaction is core-mediated: render-hook slots for fragments, and the theme-shell renderer for full plugin-owned pages. The bundled shop-starter theme is the reference storefront.
Render-hook slots
A shop theme calls documented slots via renderHook; Commerce fills them when active and they degrade to nothing when it is not.
| Slot | Where the theme calls it | What Commerce injects |
|---|---|---|
commerce.product.add_to_cart |
product card / detail template, passing the product item | price block (with sale strike-through) + add-to-cart form, or an out-of-stock notice |
header.nav.after |
header nav | mini-cart badge with live item count, plus a "my orders" link for logged-in customers |
{{/* product card */}}
<h3><a href="{{contentURL . "product"}}">{{.Title}}</a></h3>
{{renderHook "commerce.product.add_to_cart" . $.Ctx}}
{{/* header */}}
<ul class="nav-cart">{{renderHook "header.nav.after" .}}</ul>
The slot value works whether the theme passes a gin.H map (BaseTheme) or a typed struct β Commerce reflects the ID out of either.
Full pages via the theme shell
/cart, /checkout, /order-tracking, /checkout/complete/:number, and /my-account/* are dynamic, plugin-owned full pages. Commerce renders them with Engine.RenderNamespacedInActiveTheme(c, "commerce", ...), which composes the fragment inside the active theme's layouts/base.tmpl + partials using the theme's FuncMap. The core API accepts any safe namespace; Commerce supplies its own. Template resolution follows a WordPress-style override order:
<theme>/templates/commerce/<fragment>.tmpl β theme override (wins)
plugins/commerce/templates/commerce/<fragment>.tmpl β plugin default
So a theme can restyle any storefront page by dropping a file in templates/commerce/, without touching Go.
Building a shop theme
A shop theme:
- Embeds
BaseTheme(for product archive/detail rendering from the content registry). - Declares the dependency in
theme.toml:[requires] plugins = [ { slug = "commerce", version = ">=0.2.1" } ] - Does not declare its own
productcontent type β Commerce owns it. (A theme that ships its ownproducttype will collide; the last registration wins.) - Calls the render-hook slots above in its product and header templates.
shop-starter is the minimal-yet-complete example. Its only bespoke landing page loads published products and product categories through Core, while price + add-to-cart comes from commerce.product.add_to_cart. It presents Core's provider-neutral sign-in choices in a theme-aligned overlay using loginProviders + loginProviderURL; the normal /login link remains the no-JavaScript fallback. archive-product.tmpl, single-product.tmpl, and the product-taxonomy templates cover catalog browsing; cart, checkout, payment, tracking, and account pages remain plugin-owned and render through the same lightweight theme shell.
Theme settings
A theme exposes editable settings by implementing coreTheme.SettingsProvider:
func (t *MyTheme) SettingsTemplatePath() string {
return filepath.Join(t.ThemeDir, "templates", "admin", "theme_settings.tmpl")
}
The admin renders that template inside the admin chrome with .Settings (all options) available; read a value with {{index .Settings "home_hero_title"}}. The form posts to /admin/themes/<slug>/settings.
Key-prefix rule: ThemeSettingsSave only persists keys that are registered translatable or match a recognized prefix β site_, home_, about_, company_, social_, footer_, nav_, contact_, showcase_, package_. Name your settings accordingly (e.g. home_hero_title, site_name, company_email) or they will be silently dropped. shop-starter exposes identity, announcement, hero copy + illustration, product-section copy, footer contact, and optional social links this way.
Images
Follow the split from the theme-from-UI conventions:
- Theme-owned art (hero illustration and logo mark) β
themes/<slug>/static/images/andstatic/logo.svg, referenced through/static/images/<file>. These are not in the media library.shop-starterintentionally ships one small SVG hero illustration instead of a photo bundle. - CMS content images (product photos, article covers) β put the URL in the seed's
image_url; the seeder downloadshttp(s)://images into/static/uploads/demo/β¦and registers them in the media library. Use stable, verified image URLs (Unsplash directimages.unsplash.com/photo-<id>links, notsource.unsplash.com, which is discontinued).
Demo data and the seed.completed bridge
A theme provides one-click demo data by implementing coreTheme.DemoDataProvider:
func (t *MyTheme) DemoSeedPath() string {
return filepath.Join(t.ThemeDir, "demo", "data", "seed.toml")
}
The seed is pure core content. Legacy [[categories]]/[[tags]] remain available for the built-in taxonomies, while generic [[taxonomies]] declarations and [contents.taxonomies] mappings support extension-registered taxonomy names. It cannot write plugin tables directly. Product category/tag relationships therefore use product_cat/product_tag, while product prices are carried as _commerce_* content meta:
[[taxonomies]]
taxonomy = "product_cat"
name = "Audio"
slug = "audio"
[[contents]]
type = "product"
title = "Aura Headphones"
image_url = "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=800&h=800&fit=crop&q=75"
[contents.taxonomies]
product_cat = ["audio"]
[contents.meta]
_commerce_price = "259.00"
_commerce_sale_price = "199.00"
_commerce_sku = "AUD-AURA-01"
_commerce_manage_stock = "true"
_commerce_stock_qty = "120"
After the import, the seeder fires the generic seed.completed action. Commerce listens and builds product_data + product_lookup from that meta (and runs the same sync on activation, so enable-after-import also prices products). This keeps seed files free of plugin-table knowledge while still producing priced demo products.
Set theme setting values with
[[settings]]using the prefixed keys (site_name,home_hero_image, β¦) so they are visible immediately and editable in Theme Settings. Keep the store currency at the Commerce default unless you also handle the option-cache reload timing β the sync reads the currency option at import time.
Back to Overview.
