Skip to content

Theme Architecture

Directory What lives there
sections/ Full-width page components. Each has a {% schema %} and appears in the customizer. 50 files.
blocks/ Reusable components that slot into sections. Own schema, own settings. 72 files.
snippets/ Shared Liquid fragments rendered with {% render %}. No schema — they take parameters. ~99 files.
frontend/ JS and CSS compiled by Vite. Alpine.js components, React cart, and Tailwind CSS.
locales/ Translations. en.default.schema.json holds every customizer label.
templates/ JSON files defining which sections appear on each page type.
config/ Theme-wide settings — settings_schema.json, settings_data.json, markets.json.

The distinction matters, and it’s easy to get wrong.

Sections are the top-level building blocks a merchant adds to a page. They own layout and page-level settings — padding, background, colour.

Blocks are merchant-arrangeable content inside a section. They’re defined once in blocks/ and referenced by type, never redefined per section.

Snippets are developer-level code reuse. If two blocks render the same markup, that markup belongs in a snippet both of them call.

A static block is a theme block rendered individually rather than as part of the merchant-reorderable collection. It’s how a section pins a specific piece of content to a specific spot in its layout — a title above a product grid, say.

{% content_for 'block', type: 'title', id: 'section-title' %}
{% content_for 'block', type: 'buttons', id: 'cta-buttons' %}

Static blocks use the same files from blocks/ — only the rendering differs. You can pass params to override block settings with dynamic values:

{% content_for 'block', type: 'title', id: 'hero-title',
title: product.metafields.nama.hero_title.value %}

Each static block id may only be rendered once per section. When the same content needs to appear in two places (desktop bar and mobile drawer), capture the output first:

{%- capture nav %}{% content_for 'block', type: 'header-nav', id: 'nav' %}{% endcapture -%}
<div class="desktop">{{ nav }}</div>
<div class="mobile">{{ nav }}</div>

Almost every section follows this structure:

  1. Static blocks for pinned header content (title, tag, richtext)
  2. Theme blocks for repeatable content via {% content_for 'blocks' %}
  3. Each block renders itself — the section never reads block settings

For hide-when-empty on PDP and collection pages, check the metafield directly rather than relying on section.blocks.size (which doesn’t work with theme blocks).

Template Used for
product.json (default) Flagship juicers — full PDP with all blocks
product.parts.json Parts and warranty products — simplified layout
product.mid-tier.json Accessories, books, apparel — mid-complexity PDP
Template Used for
collection.json Core collection — auto-fill grid with metaobject editorial cards at configured positions
collection.parts.json Parts collection — 4-column minimal grid
collection.gift-guide.json Gift guide — stacked sections/blocks, no collection loop

Vite with vite-plugin-shopify. Assets are hashed on build and referenced through snippets/vite-tag.liquid.

Terminal window
npm run dev # Vite + Shopify theme dev together
npm run build # Production asset build

The Shopify dev command runs with --theme-editor-sync, which pulls customizer changes down into local templates/*.json files. Expect those files to change under you while the dev server is running; they’re owned by the editor, not by hand-editing.

Alpine.js for interactivity — dropdowns, drawers, toggles, filtering, product forms. Components are registered in frontend/entrypoints/js/alpine/ (14 components including cart, header, collection filters, product form, quick add, and bundle group).

Tailwind v4 with CSS-first config. The theme’s tokens live in frontend/entrypoints/css/theme.css under @theme. No tailwind.config.js — everything is in CSS. See Conventions for the rules on colours and spacing.

React is used only for the cart drawer, mounted to #cart-drawer. Components live in frontend/entrypoints/js/cart/components/ — Cart, CartLineItems, UpsellProduct, PromoCode, etc.

Splide.js for carousels — initialized via frontend/entrypoints/js/splide-init.js.

The theme relies heavily on Shopify metaobjects for structured content that’s reused across pages:

  • bundle_offer — bundle product cards on PDP
  • collection_editorial_set / collection_editorial_card — editorial cards in collection grids
  • faq_group / faq_item — reusable FAQ clusters
  • featured_articles — curated article sets per product
  • slides / social_slide_item — social media slider content
  • text_image_split — text/image split content per product
  • educator — educator profile cards
  • article_topic — blog topic taxonomy (Fresh Press)
  • product_card_images — custom card images per product

Product-level metafields in the nama.* namespace connect products to their metaobjects.