Skip to documentation content

Template Best Practices

Template Best Practices

Guidance on readability, variable scope, and keeping templates maintainable.

A few habits make Liquid templates easier to read, debug, and maintain.

Prefer var over assign: use {% var %} to store variables in the current scope so they disappear when the block or include ends. Reserve {% assign %} for cases where you intentionally need to set or update a variable on the root scope. That way you avoid accidentally overwriting variables used elsewhere and make the flow of data clearer. See Variable Scopes for the full picture on var, set, and assign.

Keep logic simple: move complex conditions or repeated logic into well-named variables (with {% var %}) so the template reads in plain language. Avoid deep nesting of {% if %} and {% for %} when possible; consider an include for a large or complex block.

Document the template contract: open a template or partial with a comment describing what it does, then list its Inputs — the bindings it expects to be available, such as entity or a value passed in at the include boundary. Name the binding itself, not dotted paths into it. Add Custom Fields when this template owns any, and Outputs when it uses set or assign to create or update a binding on a parent or root scope. Omit sections that do not apply. A reader can then tell what a partial needs and what it changes without reading the whole body.

Use comments for non-obvious sections: the Liquid comment tag also documents why a block exists or what a variable represents. That helps other developers (and your future self) understand the template quickly.

Trim whitespace deliberately: add a trimming hyphen to a delimiter only when the source character next to it is whitespace you do not want rendered, such as a newline or indentation. When a tag already sits flush against markup the hyphen does nothing, so prefer the bare delimiter and keep the template readable.

Stay within scope: when writing partials or includes, pass in what they need and use the child scope so the partial does not depend on or modify parent variables unless that is intentional. See Variable Scopes and Using Includes and Snippets for how scope works with includes.
Example How to use the var, set, and assign methodsUse var (current scope), set (nearest existing scope), and assign (root scope) to store values; filters can modify the value.

Var sets the value on the current scope

Liquid
{{ test1 }}
{%- var test1 = 'out1' -%}
,{{ test1 }}
{%- if true -%}
	,{{ test1 }}
	{%- var test1 = 'out2' -%}
	,{{ test1 }}
{%- endif -%}
,{{ test1 }}
Output
,out1,out1,out2,out1

The first time the variable is output it has not been set yet so it does not output anything. The second time it has been set to out1. The third time it has not been set on the current scope but it is still set on the parent scope so it outputs out1 again. The next time it has been set to out2 on the current scope so that is output instead. When the current scope ends (with the endif method) the variable is still set on the parent scope so it outputs out1 again.

Set changes the value on the closest scope

Liquid
{%- var test2 = 'out1' -%}
{{ test2 }}
{%- if true -%}
	{%- var test2 = 'out2' -%}
	,{{ test2 }}
	{%- set test2 = 'out3' -%}
	,{{ test2 }}
	{%- if true -%}
		{%- set test2 = 'out4' -%}
		,{{ test2 }}
	{%- endif -%}
	,{{ test2 }}
{%- endif -%}
,{{ test2 }}
Output
out1,out2,out3,out4,out4,out1

The first time the variable is output it has been set to out1 on the root scope. The second time it has been set to out2 on the current scope using the var method so it is still unchanged on the root scope. The third time it has been updated to out3 on the nearest scope that it is defined for, which also happens to be the current scope. The fourth time it has been updated to out4 on the parent scope, which is the nearest scope that it is defined for. When the current scope ends and returns to the parent scope, the updated value is still present. When that scope ends and it returns to the root scope, the variable returns to the value that was set on the root scope.

Assign sets the value on the root scope and removes the variable from all other scope

Liquid
{{ test3 }}
{%- if true -%}
	{%- var test3 = 'out1' -%}
	,{{ test3 }}
	{%- if true -%}
		{%- assign test3 = 'out2' -%}
		,{{ test3 }}
	{%- endif -%}
	,{{ test3 }}
{%- endif -%}
,{{ test3 }}
Output
,out1,out2,out2,out2

The first time the variable is output it has not been set yet so it does not output anything. The second time it has been set to out1 on the current scope (not the root scope). The third time it has been set to out2 on the root scope by the assign method, which also clears it from all other scopes. Then the fourth and fifth times it outputs out2 from the root scope.

Store a value in a variable using a filter

Liquid
{%- var title = entity.name | default: 'Untitled' -%}
<p>{{ title }}</p>

Stores the result of the expression in a variable for reuse.

Create an empty variable, then update it as needed using the set method

Liquid
{%- var tag = '' -%}
{%- if entity.tags is_list and entity.tags.is_valid -%}
	{%- set tag=entity.tags | first -%}
{%- endif -%}
{%- datastore_items datastore:"products" tag:tag -%}

Conditionally stores the first tag in the tags list in the tag variable. Then uses the tag variable to filter the datastore items. Using var and set in this way prevents the tag variable from being overwritten in the parent scope.

Duplicate a variable, then update it as needed to preserve the original value

Liquid
{%- var tag = tag -%}
{%- if entity.tags is_list and entity.tags.is_valid -%}
	{%- set tag=entity.tags | first -%}
{%- endif -%}
{%- datastore_items datastore:"products" tag:tag -%}

By creating a new variable on the current scope from an existing variable on the parent scope, we can update the variable using the set method without affecting the parent scope. Note that even if the variable is not set on the parent scope the current scope will still define the variable with a null value.

Use assign to force the variable to be accessible from the root scope

Liquid
{%- if entity.tags is_list and entity.tags.is_valid -%}
	{%- assign num_tags = entity.tags | size -%}
{%- endif -%}
There are {{num_tags | default:0}} tags on this entity

The assign method creates a root-scope variable, so it is accessible from the root scope and all child scopes.