Skip to content

Restyle the storefront

There are two ways to make a Beluga store look like yours, and they stack. The theme editor covers palette, corner radius, logo and typeface with no code. Forking the storefront components covers everything else. Do the first before the second, because a replacement component should still read the theme tokens or it stops matching the rest of the store the moment a merchant opens the editor.

Settings → Look. Primary colour, accent, page background, light or dark scheme, a corner radius from 0 to 4 px, a logo, and a font.

The font needs two fields. A font stack alone only renders on a machine that already has the face installed, so Font stylesheet URL holds the stylesheet that defines it: for Google Fonts, the href out of the <link> they give you. Saving it widens the store’s Content-Security-Policy by exactly the origins that stylesheet needs, which is why fonts.gstatic.com gets allowed even though it appears nowhere in the URL you pasted. The preview cannot show a typeface you have not saved yet, for the same reason.

Settings → Landing page sets the hero: heading, a line of text, a button label and target, and a background image. Leave any empty and the storefront falls back to the store name and a Shop everything button.

Everything above lands on :root as --beluga-* custom properties: --beluga-ink, --beluga-muted, --beluga-line, --beluga-surface, --beluga-page, --beluga-primary, --beluga-on-primary, --beluga-accent, --beluga-on-accent, --beluga-radius. src/index.css carries the light values as a first-paint fallback; src/lib/theme.ts is the source of truth for both schemes. Read colours and radius from these in anything you write.

src/pages/LandingPage.tsx is presentation. It reads store.hero and two helpers, getFeaturedProducts and getVisibleCollections from shared/catalog.ts, and nothing else depends on its internals. Rewrite it. A custom landing page is a worked example that keeps the hero fallbacks and adds a section of its own.

src/components/product/ProductCard.tsx takes a name, a preformatted price string, an image and a sold-out flag. It takes no Product and calls nothing from shared/, on purpose, so the theme editor can render a real-looking card with no catalogue behind it. ProductList.tsx is the thin layer above it that turns a Product[] into those props via formatPriceRange and isSoldOut.

Replace the card freely. If you replace the list too, keep it deriving price and sold-out state from the catalogue rather than caching them; that is the cart rule applied one layer up. A custom product card.

src/components/layout/Banner.tsx and Footer.tsx build their links from store.pages and the visible collections. Read the footer once before rewriting it: it deliberately lists every published page whether or not it is in the header menu. That is the only way a returns policy a merchant publishes but forgets to add to the menu is still one link from every page of the shop. Keep that behaviour.

emails/*.hbs are Handlebars: a body.hbs and subject.hbs per template, a shared layout.hbs, and an items.hbs partial. The locals are pre-formatted display strings, so a template does no arithmetic and cannot get money wrong by rounding it twice. Restyle them as HTML email allows.

CartPage.tsx, ConfirmPage.tsx and ProductDetails.tsx look like the files above and are not. Each is the specific place an invariant is enforced on the client: the cart holds identifiers and re-derives every price on render, the confirmation page polls for status rather than assuming payment, and the quantity control re-clamps against stock when the variant changes. The styling around those rules is yours; the rules are not. The seams has the full map.