Theming
Restyling with custom properties, overriding templates, and using a theme in a deployment.
A theme is two directories: STATIC_DIR for assets and TEMPLATE_DIR for
templates. A file in either replaces the bundled one of the same name, and
anything you do not override keeps coming from the binary. Nothing is forked and
nothing is rebuilt.
make up and make run point both at theme/ with THEME_RELOAD=true, so
writing a theme is editing a file and refreshing the page.
Restyle with CSS
Most stores never need a template. Every colour, size and spacing value in the
default theme is a custom property in one :root block at the top of
styles.css, so rebranding is editing a dozen values.
Copy the default and edit its :root:
cp internal/handler/static/styles.css theme/static/styles.css:root {
--paper: #fffdf8;
--paper-sunk: #f4efe4;
--ink: #241f18;
--ink-soft: #5c5348;
--ink-faint: #8d8375;
--rule: #e2d9c8;
--accent: #7a2e1f; /* buttons, links, prices */
--accent-ink: #fffdf8;
--font: "Iowan Old Style", Georgia, serif;
--radius: 0;
--page: 1000px;
}Your file replaces the bundled one entirely — there is no cascade between them — so start from a full copy rather than a file of just the variables.
To change the logo, drop a logo.svg into STATIC_DIR.
Web fonts
The default theme uses the system font stack: no download and no third-party request. To use a web font, either:
- Self-host it. Put the
.woff2inSTATIC_DIRand reference it from your stylesheet with a/static/...URL. Nothing else changes. - Use a hosted service, such as Adobe Fonts. Set
FONT_CSS_URLto the kit's stylesheet andFONT_ORIGINSto the origins it loads from. Set both or neither. This puts a third-party request on every page, the checkout included.
Either way, set --font in your stylesheet to apply it.
Override templates
Templates override by path: a file at pages/product.gohtml in
TEMPLATE_DIR replaces the default pages/product.gohtml. Start from a copy:
mkdir -p theme/templates/pages
cp internal/handler/templates/pages/products.gohtml theme/templates/pages/| Directory | Holds |
|---|---|
layouts/ |
The public and admin layouts |
partials/ |
Pieces every page can use, such as the product card grid |
pages/ |
The storefront pages |
admin/ |
Everything behind the admin login |
mail/ |
The order emails, which no layout wraps |
Each page file defines content, and replacing a file replaces only the
definitions it names — so a file can override one fragment and leave the rest of
the page alone.
Four things to know before shipping a theme:
- Class names are the contract between templates and stylesheet. Change the markup and keep the names, or change both together.
- Every form needs
{{template "csrf" .CSRFToken}}, or it is refused. - No inline styles or scripts. The CSP allows neither, so CSS goes in a
stylesheet and JavaScript in a
.jsfile, both inSTATIC_DIR. - A missing field or template is an error on that page, not at startup, because Go checks them when a template runs. Render every page you have touched.
Keeping a copied file current
A file you copy stops receiving gostore's changes to it, and a new feature that lives in that file will be missing from your store until you bring it across. When you upgrade, diff your overrides against the defaults. The ones that have needed it so far:
layouts/public.gohtml— the account menu. A signed-in administrator gets a profile icon in the storefront header, fetched into a placeholder the layout must keep inside its<nav>:<span class="account-slot" hx-get="/admin/account/menu" hx-trigger="load" hx-swap="outerHTML"></span>
Its styles are the "Your account in the header" block of the default
styles.css, which a copied stylesheet needs too.pages/checkout.gohtml— the submission key that makes a repeated submit return the same order. Keep it inside the form, beside the CSRF field:<input type="hidden" name="checkout_key" value="{{.Form.Key}}">.
Using a theme in a deployment
The ready-made deployments always mount a theme/
directory next to their compose.yaml into the server at /theme, read-only.
Put your theme there — git clone it, or copy it — and set, in .env:
TEMPLATE_DIR=/theme/templates
STATIC_DIR=/theme/staticThese are paths inside the container, so they always start /theme/. Spell
them exactly: a misspelt name is not an error, it is simply never read, and the
store runs with its default templates. Set only the ones your theme has — a
stylesheet-only theme sets just STATIC_DIR — since a directory that is set but
missing refuses the boot. Then docker compose up -d, which recreates the
server with the new settings.
Reloading
THEME_RELOAD=true re-reads both directories on every request. It is for
writing a theme and nothing else — leave it off in a deployment. Without it, the
theme is read once at startup: a template that does not parse refuses the boot,
and shipping a change is replacing the files and restarting
(docker compose restart server).