Skip to Content
Theme structure & bundle format

Theme structure & bundle format

A theme is a directory bundled into a zip and installed into a store. This page covers each part of the bundle in detail.

The manifest - theme.json

{ "version": "1.0.0", "name": "Aurora", "tagline": "A clean editorial storefront", "description": "Minimal, typography-led theme for fashion & lifestyle brands.", "author": "Your Studio", "tags": ["minimal", "fashion", "editorial"], "preview_image": "assets/screenshots/home.png", "screenshots": [ "assets/screenshots/home.png", "assets/screenshots/collection.png", "assets/screenshots/product.png" ], "demo_url": "https://example.com/demo" }
KeyRequiredNotes
versionyesDigits and dots only - 1.0.0, 1.2, 2.0.11. Pre-release and build metadata (1.0.0-beta.1, 1.0.0+build7) are rejected, because the version is a path segment in every asset URL and bundle cache key. Bump it on every meaningful change.
nameyesDisplay name in the marketplace, and the source of your slug - it is kebab-cased into one ("Vesper Noir"vesper-noir). The first author to submit a slug owns it.
tagline, descriptionrecommendedShown on the listing / install tile.
authorrecommendedStudio name.
tagsrecommendedSearch / filter tags.
preview_imageyes to shipPath inside the bundle. Without it the marketplace tile shows a placeholder.
screenshotsrecommendedArray of paths for the gallery.
demo_urloptionalLive demo.

Every theme must ship a preview image and at least one screenshot.

The layout - layout/theme.liquid

The single HTML wrapper for every page. It must:

  • Load global CSS via the asset_url + stylesheet_tag filters.
  • Emit CSS-variable overrides from settings.* in an inline <style> so merchant theme settings actually affect the page.
  • Wire <title>, <meta name="description">, OG tags, and favicon from shop.* + settings.*.
  • Render the header section, {{ content_for_layout }}, and the footer section.
  • Mount the cart script.
<!doctype html> <html lang="en"> <head> <title>{{ shop.seo_title | default: shop.name }}</title> <meta name="description" content="{{ shop.seo_description | default: shop.description }}"> <link rel="stylesheet" href="{{ 'theme.css' | asset_url }}"> <link rel="stylesheet" href="{{ 'common.css' | asset_url }}"> <style> :root { --color-bg: {{ settings.color_bg | default: '#FFFFFF' }}; --color-primary: {{ settings.color_primary | default: '#0F0F0F' }}; } </style> </head> <body> {% section 'header' %} <main>{{ content_for_layout }}</main> {% section 'footer' %} <script src="{{ 'cart.js' | asset_url }}" defer></script> </body> </html>

Templates - templates/*.json

Each template describes the section instances placed on that page type, plus their settings and block contents.

{ "sections": { "hero-1": { "type": "hero", "settings": { "height_vh": 88 }, "blocks": { "slide-1": { "type": "slide", "settings": { "heading": "New season" } } }, "block_order": ["slide-1"] }, "main": { "type": "main-product", "settings": {} } }, "order": ["hero-1", "main"] }

Rules:

  • Section keys reference a Liquid file in sections/. The "type" is the filename without .liquid.
  • order[] is the visual top-to-bottom order.
  • On PDP / collection / search / 404, the main section is the page’s core renderer (main-product, main-collection, etc.) and must be present.

Cart, checkout, login, and /account/* are not theme-rendered. Do not add a cart.json or customers/* template - they will not render.

Sections - sections/*.liquid

Every file in sections/ ends with a {% schema %} block. See Sections & settings for the full schema contract.

Snippets - snippets/*.liquid

Reusable Liquid partials with no schema. Rendered with {% render 'name' %}. Use these for the product card, price block, and other repeated markup so it renders identically across pages.

Assets & asset loading

Reference assets through the asset_url filter, which resolves to the hosted bundle path:

<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}"> <script src="{{ 'cart.js' | asset_url }}" defer></script> <!-- section-scoped CSS, lazily loaded from a section --> <link rel="stylesheet" href="{{ 'hero.css' | asset_url: folder: 'sections' }}"> <!-- an image asset --> <img src="{{ 'logo.svg' | asset_url }}" alt="">

Merchant-uploaded images ({{ block.settings.image }}) come back as full URLs already - do not run them through asset_url.

Assets are served with Cache-Control: immutable. Re-publishing at the same theme.json#version will keep the stale copy cached - bump the version.

CSS architecture

  • assets/theme.css - design tokens (CSS custom properties on :root). Override defaults via the inline <style> in layout/theme.liquid that interpolates settings.*.
  • assets/common.css - shared components (buttons, product card, pagination, header/footer). Anything rendered on more than one page belongs here - lift shared primitives here rather than duplicating per section.
  • assets/sections/<name>.css - section-scoped layout, loaded lazily by the section itself.

Two mandatory resets in common.css:

ul, ol { list-style: none; padding: 0; margin: 0; } select { -webkit-appearance: none; appearance: none; }