Skip to content
Ecom

Writing Shopify Theme Code That Survives Theme Updates

The anti-patterns that break on every theme update and the structural patterns that do not. Section override hygiene, snippet versioning, theme settings migration.

By WitsCode10 min read

The first time a Shopify merchant clicks Update Theme and watches eighteen months of custom work quietly vanish, they learn a lesson most agencies only teach after the fact. A Shopify theme is not a static artefact you ship once and edit forever. It is a living dependency with a vendor upstream, a merchant who keeps editing it through the admin, and a JSON settings file that changes every time someone moves a hero banner. Code that survives theme updates is code written with all three of those forces acknowledged from line one.

This article is the one we wish every junior developer read before they touched their first Dawn fork. It covers the anti-patterns that guarantee pain on the next Shopify upgrade, the structural patterns that sidestep that pain, and the operational discipline around GitHub integration, settings_data.json, and theme-check that turns a theme repo into something you can deploy against for years. WitsCode has worked on more than two hundred and fifty Shopify storefronts across performance, security, and conversion mandates, and the single biggest correlation with low ongoing maintenance cost is whether the original developer followed the rules below.

Why Shopify theme updates break custom code

Shopify themes are distributed as complete file bundles. When a merchant installs Dawn 15 over Dawn 14, or when a developer runs shopify theme pull against a freshly duplicated theme, the file system is replaced. Merchant-facing customisations made in the admin editor survive because they live in JSON template files and settings_data.json, both treated as merchant property. Everything else, every liquid edit, every assets/theme.js tweak, every schema modification, is code, and code is replaced wholesale.

There are only two sustainable ways to customise a Shopify theme. Treat the upstream theme as a base you never edit and put all your work in namespaced additions, or fork hard and accept that every upstream release becomes a three-day manual merge. Most juniors drift into a third option by accident, which is editing base files directly with no plan for upstream, and that option produces the exact failure mode the title of this article describes. Writing shopify theme update safe code starts by refusing it.

The anti-patterns that break on every update

The first anti-pattern is editing files that ship with the upstream theme. Open Dawn's card-product.liquid, add a badge renderer inside, and you have created an invisible dependency on that exact file structure. Next time Dawn refactors its card system, which happens on most major releases, your badge either disappears during merge or silently stops rendering because surrounding markup changed. The fix is never in that file. It is a new snippet rendered from a parent section you control.

The second anti-pattern is using generic names for custom snippets and sections. A developer who creates hero.liquid or product-form.liquid has gambled on Shopify and Dawn never shipping a file by that name. That gamble has lost twice in the last three years, most recently when Dawn's product form consolidation overwrote agency work on hundreds of storefronts overnight. Generic names also make code review painful because reviewers cannot tell at a glance which files are vendor and which are custom.

The third anti-pattern is hardcoding text, image URLs, and product handles into liquid. Anything a merchant might want to change in six months belongs in a section setting or metafield. Hardcoded content does not technically break on updates, but it pressures developers back into the codebase for trivial edits, which is where accidental base-file changes creep in.

The fourth anti-pattern is modifying config/settings_schema.json without understanding merge semantics. Adding a setting is safe. Renaming an ID or removing a configured setting produces orphaned entries in settings_data.json that silently drop. Every schema change needs a migration plan, not a rename.

The fifth anti-pattern is coupling custom JavaScript to IDs and classes belonging to the base theme. If your sticky add-to-cart listens on button.product-form__submit, you are one Dawn refactor away from a dead feature. JS that survives updates listens on data attributes you control and dispatches custom events on elements you own.

The sixth and subtlest anti-pattern is using the deprecated include tag instead of render. Include leaks parent scope, so a snippet that worked in Dawn 12 can silently break in Dawn 15 when a parent variable renames. Render forces explicit parameters, which is both a safety feature and documentation for future maintainers.

The namespace rule that prevents half of all collisions

The single highest leverage pattern in this whole article is namespacing every custom snippet, section, asset, and setting with a short vendor prefix. At WitsCode we use wc- as the prefix, but the specific token matters less than the discipline. A custom hero section is wc-hero.liquid, not hero.liquid. A custom product card snippet is wc-product-card.liquid. A custom JS bundle is assets/wc-sticky-atc.js. A new theme setting is wc_enable_quickview, not enable_quickview.

This rule exists for one reason. Shopify and the upstream theme authors own the unprefixed namespace. They can and do ship new files and settings into it on any release. If you take names from that namespace, your files are liable to be shadowed, overwritten, or collided with. If you stay in your own prefix, you have a guarantee that nothing Shopify ships will ever touch your files, because Shopify will never ship a file starting with wc-.

The rule also makes every workflow easier. Theme-check custom rules can assert the prefix, code review can reject unprefixed additions on sight, and upgrade ports become a simple copy of every wc-* file from the old theme into the new one followed by a targeted QA pass. Agencies that adopt this convention consistently report Dawn upgrade time dropping from days to under an hour.

Override by addition, not by modification

The corollary to namespacing is that Shopify theme override patterns should always add new files rather than mutate existing ones. If you need a custom product page, create templates/product.custom.json with its own section list, including your wc-product-form section and wc-product-gallery section. Merchants assign the custom template per product from the admin, and your base product.liquid stays untouched. If you need a different header behaviour on campaign pages, create sections/wc-campaign-header.liquid and a template that uses it, rather than editing sections/header.liquid and adding conditional logic.

This pattern extends to snippets. If Dawn's price.liquid is almost what you need but you want to render a strikethrough for bundle parents, create wc-price.liquid that calls through to the base snippet for standard cases and renders your custom markup for the edge case. Your parent section renders wc-price instead of price. When Dawn ships a new price.liquid, your wrapper still works because it was never coupled to internals.

The same logic governs CSS and JavaScript. Do not edit assets/base.css or assets/theme.js. Create assets/wc-overrides.css and assets/wc-enhancements.js, enqueue them from your layout after the base assets, and let cascade and event ordering do the work. When Dawn rewrites theme.js as ES modules, your enhancements file is unaffected because it never imported from it.

Settings schema migration without losing merchant data

Config/settings_schema.json is where most developers get hurt because the docs do not emphasise how brittle ID changes are. When a merchant saves customisations, Shopify writes every setting value keyed by its schema ID into settings_data.json. Rename a setting from enable_sticky_header to wc_enable_sticky_header and the merchant's existing value still lives under the old key. The setting reverts to default in the editor and the merchant has to re-enable it, assuming they notice.

The correct pattern is additive. Add new settings under a namespaced group with IDs like wc_sticky_atc_enabled. Never rename or remove settings in a live theme. If a setting becomes obsolete, mark it deprecated in the label and let it age out on the next major theme refresh with an explicit migration plan.

For defaults, put them in the schema default field, never in settings_data.json directly. That data file is owned by the merchant. Editing it in code is equivalent to reaching into their admin and changing settings without asking.

The settings_data.json merge conflict strategy

Even when you never edit settings_data.json directly, it will cause merge conflicts, because the merchant keeps editing the live theme while you work in a feature branch. This is the single most common source of confusion for teams new to GitHub integration. Your feature branch has one version of settings_data.json frozen at the moment you pulled. The live branch has a newer version because the merchant moved a section yesterday. When you open a PR, git sees a conflict in a file neither of you meaningfully edited.

The strategy that works is treating the live branch as authoritative for settings_data.json at all times. Before merging any feature branch to the branch connected to the live theme, you pull the latest settings_data.json from live and overwrite your branch's copy. You never accept your own branch's version of that file except in the rare case where you deliberately added a new section with new block defaults, and even then you merge the live file and re-add your new block manually.

Operationally the workflow has a small ritual at merge time. Run shopify theme pull --live to fetch the current settings_data.json, commit it to your branch, then merge. GitHub's conflict resolution almost always wants you to take the incoming live copy. Teams that adopt this ritual stop losing merchant customisations and stop shipping stale configurations that snap the homepage back to last month's layout.

GitHub integration with branch per feature discipline

Shopify's GitHub integration is deceptively simple. You connect a theme to a branch, and every push to that branch syncs to the theme. The danger is that teams wire main to the live theme and then push freely to main, which means any developer pushing broken liquid takes the storefront down in the time it takes Shopify's sync to complete.

The discipline that avoids this is a branch per environment plus a branch per feature. Main is connected to the live published theme and is protected. Staging is connected to an unpublished preview theme on the store. Every feature branch spawns a disposable unpublished theme via shopify theme push --unpublished, which gives developers and QA a live URL preview without affecting shoppers.

Merging follows a strict order. Feature branches merge to staging first. Staging is QA'd against real products, real metafields, and real cart behaviour. Only after staging signs off does staging merge to main, and only via a PR that requires theme-check to pass. Main is never committed to directly. This pattern means the only way code reaches the live storefront is through review and automated linting, and the live theme always matches what QA signed off on.

Theme-check as a CI gate, not a local nicety

Shopify ships theme-check, the official theme linter, as part of the CLI. Most developers run it locally and treat the output as advisory. The move that changes outcomes is wiring theme-check into CI and failing the build on any error, so that no code reaches any branch without passing.

A minimal GitHub Actions workflow runs on every pull request, installs the Shopify CLI, and executes shopify theme check against the repo. The default rule set catches the obvious mistakes, such as deprecated filters, parser-blocking JavaScript, missing templates, unused assigns, and asset sizes that breach performance budgets. This alone prevents the most common regressions.

The larger win comes from custom rules via .theme-check.yml in the repo root. Raise AssetSizeCSS severity to error at your performance budget so any dev adding a 300KB stylesheet fails the build. Enforce the namespace rule with a custom check that asserts every non-base snippet and section starts with your prefix. Forbid the include tag. Require that every section schema has a preset so it is placeable via the admin.

When theme-check is a hard CI gate with project-specific rules, the anti-patterns above stop being things developers have to remember. They become errors in the pull request that block merge, and new contributors learn the rules on their first PR instead of their first production incident.

What this looks like on a real upgrade

The payoff for all of this discipline arrives the day Shopify publishes a new Dawn release and the merchant asks whether they can adopt it. On a disciplined theme the workflow is mechanical. A developer creates a branch from the current base, runs shopify theme pull to a fresh Dawn checkout, copies every wc- prefixed file from the current theme into the new base, merges the .theme-check.yml and any namespaced additions to settings_schema.json, runs the CI pipeline, pushes to an unpublished preview theme, and hands it to QA. End to end this is typically a few hours of work regardless of how heavily customised the storefront is, because the customisations live in files that do not collide with upstream.

On an undisciplined theme the same upgrade is a multi-day port, because every custom change is entangled with files Dawn has just rewritten. The team has to diff Dawn 14 against Dawn 15, find every spot where custom code was grafted into a vendor file, and reapply it while accounting for renamed variables and restructured markup. This is how stores end up running three-year-old versions of Dawn with accumulating security and performance debt.

Where WitsCode picks this up

Most storefronts that come to WitsCode for performance or conversion work arrive with themes that were not built this way. The first engagement often becomes a quiet refactor: extracting custom code out of vendor files into namespaced snippets, tightening settings_schema.json into an additive pattern, wiring GitHub integration with the correct branch topology, and putting theme-check into CI. Merchants who sign a theme maintenance retainer afterwards benefit from painless Dawn upgrades on every release instead of dreading them.

If you are starting a fresh Shopify build, the cheapest time to adopt every pattern here is now. If you inherited a theme that fails most of these rules, the refactor is still worth doing before the next major upgrade because the hours invested are recovered on the first port. Either way the goal is the same. Write the code so that when the update button gets clicked, the answer is boring. That boring is the product.

Reach out to the WitsCode team about a theme maintenance audit → and we will show you, on your own repo, which files will survive the next Shopify update and which ones will not.

Get weekly field notes.

Practical writing on shipping products, straight to your inbox. No spam.

Need help with this?

Shopify Development

We design and build web apps, MVPs, and SaaS products. Talk to us about what you are working on.

Talk to us

Want to discuss ecom for your business?

Start a project and we'll talk through where you are, what's working, and the highest-leverage moves for the next 90 days.