Theme structure

A theme is a single folder with eight subfolders. Nothing else under theme/ is read by the renderer.

Text
theme/
  layouts/    *.liquid, flat, no schema - page wrappers (<html>, <head>, nav)
  sections/   *.liquid, flat, one per section type - markup + settings schema
  blocks/     *.liquid, flat, one per block type - markup + settings schema
  snippets/   *.liquid, flat, no schema - reusable partials
  assets/     CSS, JS, icons, favicons - served as-is at /assets/<path>
  root/       optional - files that must sit at an exact path at the site root
  templates/  *.json, flat, one per prebuilt page - see Layouts & templates
  config/     site_settings.json (optional) - the settings editors can change

No subfolders inside sections/, blocks/, layouts/, or snippets/. Every component is exactly one file, named directly — the filename (without its extension) is that component’s identifier.

Assets

Any file under theme/assets/ is served exactly as-is at /assets/<same relative path>, subfolders preserved:

Text
theme/assets/style.css          -> /assets/style.css
theme/assets/icons/arrow.svg    -> /assets/icons/arrow.svg

Reference them with plain paths, no special Liquid filter:

Liquid
<link rel="stylesheet" href="/assets/style.css">
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png">

Favicons and other icons belong here, linked from the layout’s <head>. Browsers use whatever the <link> names, so nothing needs to sit at /favicon.ico.

Assets are for design, not content

Photographs, and any image an editor might ever want to replace, belong in the media library, not theme/assets/. An image served from the theme renders perfectly but is invisible to the admin, so an editor can never swap it. npm run check reports any content image or video served from theme/assets/ or theme/root/. See The content model.

Root-level files

theme/root/ is optional. Create it only for a file that genuinely must sit at an exact path at the site’s root: anything under it is mirrored verbatim there, not under /assets/ — theme/root/robots.txt → /robots.txt, theme/root/.well-known/security.txt → /.well-known/security.txt, or a verification file a service asks for by name.

Note

Google’s <meta name="google-site-verification"> method needs no file at all — add the tag to your layout’s <head>. And GET /sitemap.xml is a built-in dynamic route, generated fresh on every request from whatever is currently published, so it’s always accurate. It takes priority over a same-named file under theme/root/.

Site settings

theme/config/site_settings.json defines settings used across the whole site — social links, a contact email, a font choice — that editors change in the admin and every template reads as {{ settings.<name> }}. See Site settings.

The one naming rule that matters everywhere

A section or block’s filename (without .liquid) must match ^[a-z0-9][a-z0-9-]*$ — lowercase, digits, and hyphens only. Examples: hero.liquid, media-text.liquid, testimonial-grid.liquid. This exact string is what content JSON uses in its own "type" field to reference the component, and it must match exactly, including case.

Hard constraints

  • No dynamically registered Liquid tags or filters, ever. Only standard LiquidJS built-ins plus the CMS-provided context objects described in the following pages.
  • Every template render is bounded to roughly 50ms. Keep templates straightforward — simple loops and conditionals, no heavy computation in Liquid.
  • One file, one type, no subfolders inside sections/, blocks/, layouts/, or snippets/.

Next