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"
}| Key | Required | Notes |
|---|---|---|
version | yes | Digits 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. |
name | yes | Display 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, description | recommended | Shown on the listing / install tile. |
author | recommended | Studio name. |
tags | recommended | Search / filter tags. |
preview_image | yes to ship | Path inside the bundle. Without it the marketplace tile shows a placeholder. |
screenshots | recommended | Array of paths for the gallery. |
demo_url | optional | Live 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_tagfilters. - 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 fromshop.*+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
mainsection 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 acart.jsonorcustomers/*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>inlayout/theme.liquidthat interpolatessettings.*.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; }