Shopify theme architecture that survives a year
A theme does not rot from badly written Liquid. It rots from a handful of structural decisions taken in week two, when nobody is thinking about month eleven.
GuideMay 6, 20268 min read
A Shopify theme rarely breaks. It degrades, and it degrades in one specific way: the theme editor stops being the place where changes happen, and every content change turns into a developer ticket. The pages still render. The store still sells. But the thing that was actually bought — a storefront the merchant's own team can operate — is gone, and getting it back usually costs more than the original build did.
That decay is almost never caused by badly written Liquid. It is caused by a small number of architectural decisions taken in the first two weeks, when the pressure is to get a homepage on screen. Here is what those decisions are, and which way to make them.
The section is the unit of a page; the block is the unit of a row
Online Store 2.0 lets you put sections on every template, and the usual reading of that is permission to make everything a section. It is not. A section is a horizontal band of a page that a merchant might reorder, remove, or place once. A block is a repeated item inside that band whose count the merchant controls.
The test is arity. If a merchant will ever want two of something side by side, it is a block. If they will want exactly one of it, in a particular position in the stack, it is a section. Getting this backwards is the most common structural mistake in theme work, and it is expensive because it stays invisible for months.
A three-column feature strip built as three separate sections looks identical in the editor on day one. On day sixty it is a page where someone removed the middle column and left a two-thirds-width gap, where the three sections have drifted apart in padding, and where changing the heading style is three edits. The same strip built as one feature-strip section with a feature block and max_blocks: 4 is the same page with a quarter of the surface to maintain.
Schema is an interface, not a drawer
Every setting you add to a section schema is a promise that the setting works in combination with every other setting in that section. Ten independent booleans is not ten states, it is one thousand and twenty-four, and you will have tested about six. Settings are cheap to add and permanent to support.
So keep them few and keep them behavioural. layout: grid | carousel is a good setting: it names an intent, it has a bounded set of values, and each value is a code path someone wrote on purpose. column_gap_px is a bad one: it hands a design decision to somebody who is not making design decisions, and it guarantees that six months of small edits leave the site slightly inconsistent everywhere.
Scroll the figure sideways to see all of it
Settings or metafields: who owns the value
This is the decision that separates a theme you can replace from a theme you are married to, and it has a clean test. Export the theme, install it on a different store, and ask whether the value should travel with it. If yes, it belongs in schema settings. If the value describes a product, a collection, a page or a market, it is store data and it belongs in a metafield.
Care instructions typed into a product template's section settings are a value living inside the theme. They apply to whichever product happens to use that template, they cannot be exported, they cannot be edited in bulk, they cannot be translated through Shopify's own translation surface, and they disappear the day the theme is replaced. The same text as product.metafields.custom.care_instructions belongs to the product. It survives the theme, it is edited in the admin alongside everything else about that product, a bulk operation can fill it, and an app or an export can read it.
The working rule: anything that varies per product, per collection or per market is data. Anything describing how the theme presents data is a setting. The moment you find yourself writing a setting whose label begins with the name of a specific product, you have crossed the line and should stop.
Metaobjects for the shapes that repeat
When the same structured thing appears against many products — a size guide, an ingredient, a certification, a care symbol — it is not a metafield per product, it is a metaobject with a reference pointing at it. That gives you one place to edit the definition of "organic cotton" and every product referencing it updates. Modelling that on day one costs an hour. Retrofitting it once two hundred products have the text pasted into a rich-text metafield costs a week, and it is exactly the kind of work our Shopify development engagements spend time on before anyone opens a template.
Scroll the figure sideways to see all of it
Snippets, and the copy-paste that eats a theme
Price rendering. Product cards. Badge logic. Variant availability text. Those are the four places where duplication accumulates, and duplication is not a tidiness problem — it is a correctness problem on a delay. The cost of a copy-paste is never the duplicate. It is the day the tax display rule changes, or unit pricing has to appear, or a sale badge needs a new condition, and the change has to land in seven places, and it lands in six.
Use render rather than include, and use it deliberately. The scoping is the whole point: a snippet rendered with declared parameters is a function, and a function can be checked by reading it. A snippet that reaches for section.settings on its own, or assumes a product object is in scope, is not a snippet at all — it is a fragment of one section that will break the first time it is rendered from another.
A good snippet has a short parameter list, no knowledge of where it was called from, and a comment at the top saying what it expects. Three lines of documentation at the head of price.liquid is the cheapest maintenance anyone will ever buy.
Where the JavaScript lives
Theme JavaScript has one structural constraint people forget until it bites: sections re-render. The theme editor swaps section markup in and out live, and the Section Rendering API replaces markup on the storefront during cart updates and filtering. Any listener bound once on DOMContentLoaded to an element inside a section is pointing at a node that no longer exists.
The answer is to attach behaviour to markup rather than to page load. A custom element whose connectedCallback does the wiring is re-initialised by the browser automatically every time that markup reappears, with no bookkeeping on your side. One behaviour, one custom element, one file, sitting next to the section that uses it. It is the structure Dawn uses, and it is the right one for reasons that have nothing to do with fashion.
The other decision is whether to put a build step in front of the theme at all. A theme is served from Shopify's CDN and needs no bundler; adding one buys you TypeScript and module resolution, and costs you a compiled artefact in the repository, a step somebody has to remember, and a debugging experience one layer removed from what ships. Both answers are defensible. What is not defensible is a bundler nobody documented, which is how a theme ends up with a dist directory no one dares regenerate.
The decisions that make a theme rot
Concretely, in order of damage done:
- Page-specific sections. A section named
homepage-herois a section that will be duplicated aslanding-herothe first time marketing needs one. Name sections after what they are, not where they were first used. - Product data in section settings. Covered above, and the one that turns a theme migration into a data recovery project.
- Duplicated templates. Two near-identical product templates is two places to fix everything. Alternate templates with a suffix are for genuinely different page structures, not for a different heading.
- Global JavaScript bound at load. It works on first paint and silently stops after any section re-render, which means it is broken precisely where the merchant tests least.
- Pixel values in schema. Every one is a future inconsistency with a merchant's name on it.
- Never deleting. Sections nobody uses, settings nothing reads, snippets nothing renders. Dead code in a theme is worse than dead code in an app, because the theme editor keeps offering it to merchants.
- Editing the live theme. One untracked change is enough to make the repository a lie, and after that nobody trusts a deploy again.
What maintainable actually looks like a year in
A maintainable theme is not one with clever Liquid. It is one where a new page can be composed in the editor without a developer, where product data lives on products, where changing the price display is one file, and where a developer who has never seen the theme can find the code behind any band of any page in under a minute by reading its section name.
That is a low bar to describe and a rare thing in practice, because each of those properties is the result of a decision somebody refused to defer. If you want the same discipline applied to speed instead of structure, the argument repeats in performance work: the expensive part is never the fix, it is the architecture that made the fix necessary. Our theme development work starts at that layer for the same reason.