The following are a set of points that
make Shopify themes easier to work with.
Like most other software projects, themes should make use of
issue trackers - via GitHub for example - where developers and
merchants can submit problems, look for existing problems
they are currently facing and stay up to date about new issues
that have been found or resolved.
Themes should provide documentation in an easy to access manner,
that describes the structure, features, components, mechanics, .. that
are provided by the theme.
In combination with the previous point about issue trackers,
providing the documentation in form of markdown files in a
GitHub repository would be more than fine.
Providing the docuemntation in the same repository as the issue
tracker for the theme would also allow people to submit questions,
issues and changes to the documentation itself.
While frameworks can provide interesting ways of building
themes and make the vendor facing development easier,
downstream development work becames much more difficult.
Not only does anyone trying to properly change an existing
theme have to understand the theme's structure and any
existing changes made by apps, merchants & agencies but
also learn and comprehend the use of whatever frameworks
have been integrated.
Especially as many frameworks require the use of specific setups
such as configuration files that aren't provided with the theme,
source files that are kept private and plugins to IDEs to properly
work with them, it makes continued development hard to impossible.
For example Alpine.js not only clutters the usually
compressed html + liquid code with more content
but also introduces implicit local & global state that
without tooling is practically invisible in development.
These problems concern not only structural framworks
but also styling solutions like Tailwind, PostCSS, ..
In general, as themes are already bound to be messes of
various technologies, it's best not to contribute to that
from the vendor side as well and stay with the 'vanilla'
web technologies that everyone knows and uses.
Most sections tend to have a boat load of schema which in turn
clutters up the theme files that ideally should only contain code.
70 lines of liquid / .. + 6000 lines of schema

The way around this - that I have been enforcing in any code that
we write - is to simply separate the actual code from the schema
into a separate snippet with the same / suffixed name.
sections/Slideshow.liquid
{%- render 'Slideshow-Section' -%}
{%- schema -%}
{
"name" : "Slideshow" ,
"settings" : [{
"label" : "Heading"
"type" : "text" ,
"id" : "Heading"
}]
}
{%- endschema -%}
snippets/Slideshow-Section.liquid
snippets/Slideshow.liquid
{%- liquid
assign options = section.settings
assign heading = options.Heading
# ..
-%}
<h2> {{- heading -}} </h2>
<!-- .. -->While some like to inline content as it seems to make it
easier for them to write code - reading, comprehending
and changing such code is a pain.
The simple way around it is to just not inline things.
It's as simple as proprocessing the content you want
to insert in before hand in a pure liquid block.
In combination with other techniques described in this document,
more and more can be done purly in liquid, html, css, js with
defined interfaces instead of a giant mix of languages.
For the benefit of any human having to read & change theme code I
would recommend sensible amounts of code per file / columns per line.
Traditional print media didn't limit the amounts of lines per columns
for the fun of it, the avarage human cannot properly comprehend more
than 65 letters per line which only worsens the context switching.
Stretching that definition, the common 80 - 100 columns would be
a great goal for liquid code considering the examples found below.
The same thing goes for lines per file, some people have seemingly
made it their life's mission to increase the amount of liquid code.
Don't stuff everthing into layout files as they get
vandalized by apps and integrators constantly.
Instead do the opposite, move the 'static' into snippets
that are referenced in various parts of the layout.
This not only ensures that apps don't accidentally fuck
with the theme code but gives it more structure & helps
with multi-stores where layout files are unique to each
store due to settings being added by apps & merchants.
This example doesn't use inserts / etc. to showcase
the raw change recommended by this point.
<!doctype html>
<html
lang = en
{% render 'Theme-Attributes' -%}
>
<head>
{%- render 'Theme-Before-Header' -%}
{{ content_for_header }}
{%- render 'Theme-After-Header' -%}
</head>
<body>
{%- render 'Theme-Before-Content' -%}
<main>
{{ content_for_layout }}
</main>
{%- render 'Theme-After-Content' -%}
</body>
</html>Similar to other things that can / should be rendered via
snippets, microdata is better kept in dedicated snippets.
While microdata related to specific subjects like product
that doesn't mean it has to be colocated in the same file
as product rendering code.
After there shouldn't really be anything the microdata relies
on from sections / blocks, so all state that it renders into it's
JSON LD is based on globally available data.
A much cleaner way that doesn't clutter up the section
code is to have a central entrypoint referenced by the
layout / the previously discussed Theme- snippets that
renders the microdata for whatever context is applicable.
snippets/Microdata.liquid
{%- liquid
render 'Microdata-Organization'
case request.page_type
when 'product'
render 'Microdata-Product'
endcase
-%}As the HTTP/3 protocol has had sufficient support for a while now
- including from Shopify - it's high time to stop the giant bundles
and embrace component based asset loading.
The TLDR of HTTP/3 is that there is effectively no limit to the
amount of connections that can be held to load resources,
making the use of multiple CDN domains, giants bundles
and various other old loading techniques redundant.
Instead the proper approach now is finally the simplest,
shipping JS / CSS on the level it was written at - components.
Since most themes have been using custom web components for
a long time now, the transition isn't that difficult either.
In combination with importmaps, modules & a good bundler
like bun, the resulting 'micro' bundles - when transpiled -
don't look much different to their original TypeScript code.
assets/Component-X.js
import { init } from 'Library'
class ComponentX
extends HTMLElement {
connenctedCallback (){
init()
}
}
customElements.define(
'component-x' ,
ComponentX
)assets/Library-X.js
export { init }
function init (){}assets/Component-X.css
component-x {}importmap
<script type = importmap >
{
"imports" : {
"Library" : "{{- 'Library.js' | asset_url -}}"
}
}
</script>snippets/Component-X.liquid
<script
type = module
src = '{{- 'Component-X.js' | asset_url }}'
/>
{{ 'Component-X.css' | asset_url | stylesheet_tag }}
<component-x>
<!-- ... -->
</component-x>This way dependencies like the library are treating the
same as when for example referencing an NPM module.
( Dependeing on your setup you have to mark it as external )
( In it's literally just --external <Import Name> )
The resulting setup generates more files than one large
bundle, yes, however now the code is actually workable.
A giant bundle slows down opening / navigating / editing,
prevents from properly searching / tracing back things,
makes the Shopify CLI slower and currently can even break it.
( Depending on the file size, they don't upload
and you have to restart the CLI process )
Using HTTP/3 and only loading the bits that are needed obviously
also improves the performance of the page upon other things.
Let me know if you are interested in the described build setup,
it's quite minimal with only a small script calling Bun & a tsconfig.
( No need for a giant pile of webpack )
Don't just use data attributes and CSS variables half heartedly,
use them as they were intended, to map any kind of state.
If you use some class with a variation like
Slider--Wide then it should be an attribute.
Using attributes consistently also describes the default for the
component, while an absent class infers a default style, a data
attribute that is always set also describes the component's state.
<div
data-size = '{{- size -}}'
class = 'Slider'
> .Slider[ data-size = Slim ]{}
.Slider[ data-size = Wide ]{}Data attributes are also easier and more consistent when working
with custom web components, not only in accessing them but also
in the fact that they describe their own presence.
Similarly using locally scoped CSS variables either
defined inline on an element or separate in a tag
make away with static CSS being put into liquid files.
<style>
#{{- sectionId -}}{
grid-template-columns : minmax( {{- minimum -}}px , auto ) ;
}
</style>.Component {
grid-template-columns : minmax( var( ---Minimum ) , auto ) ;
}<style>
#{{- sectionId -}}{
---Minimum : {{- minimum -}}px ;
}
</style>or
<div
style = '---Minimum : {{- Minimum -}}px' # Even better if captured in advance
class = 'Component'
>This reduced the CSS to only it's interface - the variables -
instead of moving specific styling usage into the structural code.
Once the doc tag has been properly supported via the
VSC Shopify Liquid extension, it should be beneficial
to document every snippet with it.
Even if it turns out to be too flawed / .. documenting
a snippets inputs in one way or another is important.
Less so what exactyle the snippet does - as this should
come from the name but rather properly describe the
input parameters - like enums.
{%- doc -%}
@param {string} [size] - Size of the component.
{%- enddoc -%}or
{%- liquid
#
# Parameters
# * block : String [ Small , Large ]
#
-%}



