liquid icon indicating copy to clipboard operation
liquid copied to clipboard

[FEATURE] render with params + LiquidJS vs Go Liquid comparison

Open pierre-b opened this issue 1 month ago • 2 comments

Pre-submission Checklist

  • [x] I have searched the issue list for similar feature requests

Problem Description

Hi, thanks for creating this lib.

In my use-case users build templates on the frontend (preview with LiquidJS) and are served by backend Go Liquid. Sadly it does not support yet render with params, and I'd like to know if someone is already working on it please?

Proposed Solution

It should accept syntax:

{% render "snippet.liquid", product: product, color: "blue" %}

Alternatives Considered

LiquidJS embedded in Go v8

Shopify Liquid Compatibility

https://shopify.dev/docs/storefronts/themes/architecture/snippets

Additional Context

FYI here is a full comparison with LiquidJS compatibility:

Liquid Template Engine Comparison

LiquidJS vs Go Liquid (osteele/liquid)

Date: November 18, 2025
LiquidJS Version: Latest (main branch)
Go Liquid Version: Latest (main branch)


Executive Summary

This document provides a comprehensive feature comparison between two popular Liquid template engine implementations:

Quick Feature Parity Overview

Category LiquidJS Go Liquid Notes
Tags 21 tags 12 tags LiquidJS has 9 more tags
Filters 85+ filters 45+ filters LiquidJS has ~40 more filters
Operators Full support Full support Both support all standard operators
Whitespace Control ✅ Full support ✅ Full support Both use - syntax
Dynamic Partials ✅ Supported ⚠️ Limited LiquidJS has more flexibility
Extensibility ✅ Excellent ✅ Good Both allow custom filters/tags
Performance Good Excellent Go has better raw performance
Memory Safety JavaScript VM Native Go Go has stronger memory safety

Key Differences

  1. Tag Support: LiquidJS has more advanced tags like render, layout, block, echo, liquid, and inline-comment
  2. Filter Richness: LiquidJS offers significantly more filters, especially for array manipulation and expressions
  3. Render Tag: LiquidJS has full render tag with variable isolation; Go Liquid only has include
  4. Expression Filters: LiquidJS supports *_exp filters (where_exp, reject_exp, etc.) for dynamic filtering
  5. Jekyll Compatibility: Both support Jekyll extensions, but with different APIs

1. Tags Comparison

1.1 Control Flow Tags

Tag LiquidJS Go Liquid Syntax Differences Behavior Notes
if ✅ Full support ✅ Full support Identical Same behavior
unless ✅ Full support ✅ Full support Identical Same behavior (inverted if)
elsif ✅ Full support ✅ Full support Identical Used within if/unless blocks
else ✅ Full support ✅ Full support Identical Used within if/unless/case blocks
case ✅ Full support ✅ Full support Identical Switch-like conditional
when ✅ Full support ✅ Full support Identical Used within case blocks

Syntax Example (Identical):

{% if user.age >= 18 %}
  Adult
{% elsif user.age >= 13 %}
  Teen
{% else %}
  Child
{% endif %}

1.2 Iteration Tags

Tag LiquidJS Go Liquid Syntax Differences Behavior Notes
for ✅ Full support ✅ Full support Identical Loop over arrays/objects
break ✅ Full support ✅ Full support Identical Exit loop early
continue ✅ Full support ✅ Full support Identical Skip to next iteration
cycle ✅ Full support ✅ Full support Identical Cycle through values
tablerow ✅ Full support ✅ Full support Identical Generate HTML table rows

For Loop Modifiers:

{% for item in collection limit:5 offset:10 reversed %}
  {{ item }}
{% endfor %}
  • ✅ Both support: limit, offset, reversed
  • ✅ Both provide: forloop.index, forloop.first, forloop.last, etc.

Else Clause:

{% for item in collection %}
  {{ item }}
{% else %}
  No items found
{% endfor %}
  • ✅ LiquidJS: Fully supported
  • ✅ Go Liquid: Fully supported

1.3 Variable Assignment Tags

Tag LiquidJS Go Liquid Syntax Differences Behavior Notes
assign ✅ Full support ✅ Full support Identical Create/update variables
capture ✅ Full support ✅ Full support Identical Capture block content as variable
increment ✅ Supported ❌ Not supported - Auto-incrementing counter
decrement ✅ Supported ❌ Not supported - Auto-decrementing counter

Syntax Examples:

{% assign name = "John" %}
{% capture greeting %}Hello {{ name }}{% endcapture %}

{# LiquidJS only: #}
{% increment my_counter %}  {# outputs: 0, then 1, then 2... #}
{% decrement my_counter %}  {# outputs: -1, then -2, then -3... #}

Notes:

  • Go Liquid: increment and decrement tags are not implemented
  • Jekyll Extensions: Go Liquid supports dot notation in assign (e.g., {% assign obj.prop = value %}) when Jekyll extensions are enabled

1.4 Template Inclusion Tags

Tag LiquidJS Go Liquid Syntax Differences Behavior Notes
include ✅ Full support ✅ Full support Different scoping LiquidJS more flexible
render ✅ Full support ❌ Not supported - Variable isolation
layout ✅ Supported ❌ Not supported - Template inheritance
block ✅ Supported ❌ Not supported - Content blocks for layouts

Include Tag:

LiquidJS:

{% include "header.liquid" %}
{% include "header.liquid" with product %}
{% include "header.liquid" with product as item %}
{% include "header.liquid" for products %}
{% include "header.liquid", color: "blue", size: "large" %}

Go Liquid:

{% include "header.liquid" %}
{# No with/for/as syntax support #}
{# Variables passed via RenderFile context #}

Render Tag (LiquidJS only):

{% render "snippet.liquid", product: product, color: "blue" %}
{# Variables are isolated - parent scope not accessible #}

Layout/Block Tags (LiquidJS only):

{# layout.liquid #}
<html>
  <body>
    {% block content %}Default content{% endblock %}
  </body>
</html>

{# page.liquid #}
{% layout "layout.liquid" %}
{% block content %}Custom content{% endblock %}

Key Differences:

  • include in LiquidJS has parent scope access by default
  • render in LiquidJS provides isolated scope
  • Go Liquid only has include with custom file system interface

1.5 Output and Raw Tags

Tag LiquidJS Go Liquid Syntax Differences Behavior Notes
echo ✅ Supported ❌ Not supported - Alternative to {{ }}
raw ✅ Full support ✅ Full support Identical Disable Liquid parsing
comment ✅ Full support ✅ Full support Identical Multi-line comments
liquid ✅ Supported ❌ Not supported - Tag without delimiters
# (inline comment) ✅ Supported ❌ Not supported - Single-line comments

Syntax Examples:

Echo Tag (LiquidJS only):

{% echo product.title %}
{# Equivalent to: {{ product.title }} #}

Liquid Tag (LiquidJS only):

{% liquid
  assign name = "John"
  if name == "John"
    echo "Hello John"
  endif
%}

Inline Comment (LiquidJS only):

{% # This is an inline comment %}

Raw Tag (Both):

{% raw %}
  This {{ will not }} be parsed
{% endraw %}

Comment Tag (Both):

{% comment %}
  Multi-line comment
  that spans several lines
{% endcomment %}

2. Filters Comparison

2.1 String Filters

Filter LiquidJS Go Liquid Syntax Differences Behavior Notes
append Identical Add string to end
prepend Identical Add string to start
capitalize Identical Capitalize first letter
downcase Identical Convert to lowercase
upcase Identical Convert to uppercase
strip Different LiquidJS supports char param
lstrip Different LiquidJS supports char param
rstrip Different LiquidJS supports char param
strip_html Identical Remove HTML tags
strip_newlines Identical Remove newlines
newline_to_br Identical Convert \n to <br />
remove Identical Remove all occurrences
remove_first Identical Remove first occurrence
remove_last - Remove last occurrence
replace Identical Replace all occurrences
replace_first Identical Replace first occurrence
replace_last - Replace last occurrence
split Identical Split string into array
slice Identical Extract substring
truncate Identical Truncate to length
truncatewords Identical Truncate to word count
normalize_whitespace - Collapse multiple spaces
number_of_words - Count words (CJK support)

Syntax Examples:

Strip with custom characters (LiquidJS):

{{ "...hello..." | strip: "." }}  {# Output: hello #}

Strip (Go Liquid):

{{ "  hello  " | strip }}  {# Output: hello #}
{# No custom character parameter #}

Number of words (LiquidJS only):

{{ "Hello world" | number_of_words }}  {# Output: 2 #}
{{ "你好世界" | number_of_words: "cjk" }}  {# Output: 4 #}

2.2 Array Filters

Filter LiquidJS Go Liquid Syntax Differences Behavior Notes
join Identical Join array elements
first Identical Get first element
last Identical Get last element
concat Identical Concatenate arrays
map Identical Extract property from each item
reverse Identical Reverse array
size Identical Get array/string length
sort Identical Sort array
sort_natural Identical Case-insensitive sort
uniq Identical Remove duplicates
compact Identical Remove nil values
where - Filter by property value
where_exp - Filter by expression
reject - Inverse of where
reject_exp - Inverse of where_exp
find - Find first matching item
find_exp - Find by expression
find_index - Find index of item
find_index_exp - Find index by expression
has - Check if array has item
has_exp - Check by expression
group_by - Group by property
group_by_exp - Group by expression
sum - Sum numeric values
push - Add item to end
pop - Remove item from end
shift - Remove item from start
unshift - Add item to start
sample - Random sample
array_to_sentence_string - Convert to sentence

Syntax Examples:

Where filter (LiquidJS only):

{{ products | where: "featured", true }}
{{ products | where: "price" }}  {# truthy values #}

Expression filters (LiquidJS only):

{{ products | where_exp: "item", "item.price < 100" }}
{{ products | reject_exp: "item", "item.sold_out" }}
{{ products | find_exp: "item", "item.id == 5" }}

Group by (LiquidJS only):

{% assign by_type = products | group_by: "type" %}
{% for group in by_type %}
  <h3>{{ group.name }}</h3>
  {% for product in group.items %}
    {{ product.title }}
  {% endfor %}
{% endfor %}

Array manipulation (LiquidJS only):

{{ array | push: "new_item" }}
{{ array | pop }}
{{ array | shift }}
{{ array | unshift: "first_item" }}
{{ array | sample: 3 }}  {# Random 3 items #}

2.3 Math Filters

Filter LiquidJS Go Liquid Syntax Differences Behavior Notes
abs Identical Absolute value
ceil Identical Round up
floor Identical Round down
round Identical Round to precision
plus Different Go has type-aware arithmetic
minus Different Go has type-aware arithmetic
times Different Go has type-aware arithmetic
divided_by Different Go supports integer division
modulo Identical Remainder operation
at_least - Return value or minimum
at_most - Return value or maximum

Syntax Examples:

Divided by with integer arithmetic (LiquidJS):

{{ 10 | divided_by: 3 }}  {# Output: 3.33... #}
{{ 10 | divided_by: 3, true }}  {# Output: 3 (integer) #}

At least/most (LiquidJS only):

{{ 5 | at_least: 10 }}  {# Output: 10 #}
{{ 15 | at_most: 10 }}  {# Output: 10 #}

Go Liquid Type-Aware Arithmetic:

  • When both operands are integers, returns integer
  • When either operand is float, returns float
  • Division by zero returns error

2.4 Date Filters

Filter LiquidJS Go Liquid Syntax Differences Behavior Notes
date ✅ Full support ✅ Full support Different LiquidJS has more format codes
date_to_xmlschema - ISO 8601 format
date_to_rfc822 - RFC 822 format
date_to_string - Short date format
date_to_long_string - Long date format

Syntax Examples:

Date filter:

{# Both: #}
{{ "now" | date: "%Y-%m-%d" }}

{# LiquidJS only: #}
{{ "now" | date: "%Y-%m-%d", 480 }}  {# timezone offset #}
{{ date | date_to_xmlschema }}  {# 2025-11-18T10:30:00+00:00 #}
{{ date | date_to_rfc822 }}  {# Mon, 18 Nov 2025 10:30:00 +0000 #}

Go Liquid:

  • Uses github.com/osteele/tuesday for strftime
  • Supports standard strftime format codes
  • Takes time.Time as input

LiquidJS:

  • Parses strings like "now", "today", or timestamps
  • Supports timezone offset parameter
  • More specialized date formatting filters

2.5 HTML/URL Filters

Filter LiquidJS Go Liquid Syntax Differences Behavior Notes
escape Identical HTML escape
escape_once Identical Escape unescaped only
xml_escape - Alias for escape
url_encode Identical URL encode
url_decode Identical URL decode
uri_escape - URI encode (preserves [])
cgi_escape - CGI-style encoding
slugify - Convert to URL slug

Syntax Examples:

Slugify (LiquidJS only):

{{ "Hello World!" | slugify }}  {# hello-world #}
{{ "Hello World!" | slugify: "ascii" }}  {# hello-world #}
{{ "Hello World!" | slugify: "pretty" }}  {# hello-world! #}
{{ "Café" | slugify: "latin" }}  {# cafe #}

CGI escape (LiquidJS only):

{{ "hello world" | cgi_escape }}  {# hello+world #}
{{ "hello world" | url_encode }}  {# hello+world (same) #}
{{ "hello world" | uri_escape }}  {# hello%20world #}

2.6 Utility Filters

Filter LiquidJS Go Liquid Syntax Differences Behavior Notes
default Different LiquidJS has allow_false
json / jsonify Identical Convert to JSON
inspect Different LiquidJS handles circular refs
raw - Output without escaping
to_integer - Convert to integer
type - Get Go type string
base64_encode - Base64 encode
base64_decode - Base64 decode

Syntax Examples:

Default with allow_false (LiquidJS):

{{ false | default: "backup" }}  {# Output: backup #}
{{ false | default: "backup", allow_false: true }}  {# Output: false #}

Base64 (LiquidJS only):

{{ "Hello World" | base64_encode }}  {# SGVsbG8gV29ybGQ= #}
{{ "SGVsbG8gV29ybGQ=" | base64_decode }}  {# Hello World #}

Type (Go Liquid only):

{{ value | type }}  {# Output: string, []interface{}, etc. #}

3. Operators & Expressions

3.1 Comparison Operators

Operator LiquidJS Go Liquid Notes
== Equal
!= Not equal
> Greater than
< Less than
>= Greater than or equal
<= Less than or equal
contains String/array contains

Both implementations support all standard comparison operators with identical syntax.


3.2 Logical Operators

Operator LiquidJS Go Liquid Notes
and Logical AND
or Logical OR

Syntax:

{% if user.active and user.age >= 18 %}
{% if user.admin or user.moderator %}

Both implementations support logical operators with identical behavior.


3.3 Truthiness

Both implementations follow Liquid's truthiness rules:

Truthy values:

  • All values except false and nil
  • Empty strings "" are truthy
  • Empty arrays [] are truthy
  • Zero 0 is truthy

Falsy values:

  • false
  • nil / null

4. Special Features

4.1 Whitespace Control

Feature LiquidJS Go Liquid Syntax Notes
Strip left whitespace {%- Remove left whitespace
Strip right whitespace -%} Remove right whitespace
Output strip left {{- Remove left whitespace
Output strip right -}} Remove right whitespace

Syntax Example:

{%- if true -%}
  Whitespace trimmed
{%- endif -%}

Both implementations support identical whitespace control syntax.


4.2 Dynamic Partials

Feature LiquidJS Go Liquid Notes
Variable file names ⚠️ Limited LiquidJS has better support
Dynamic partials option LiquidJS-specific
Expression in filenames LiquidJS can evaluate expressions

LiquidJS:

{% assign template_name = "header" %}
{% include template_name %}  {# Dynamic! #}
{% include "files/{{ type }}.liquid" %}  {# Expression! #}

Go Liquid:

{% include "header.liquid" %}  {# Static filename only #}

4.3 Jekyll Extensions

Feature LiquidJS Go Liquid API
Jekyll compatibility mode Different APIs
Dot notation in assign Enabled via option
Jekyll include behavior LiquidJS only
Jekyll where filter LiquidJS only

LiquidJS:

const engine = new Liquid({
  jekyllInclude: true, // Include vars under 'include' scope
  jekyllWhere: true // where filter behavior
})

Go Liquid:

engine := liquid.NewEngine()
engine.SetJekyllExtensions(true)  // Enable dot notation

4.4 Error Handling

Feature LiquidJS Go Liquid Notes
Strict variables ✅ Configurable ⚠️ Lenient LiquidJS can throw on undefined
Strict filters ✅ Configurable ⚠️ Lenient LiquidJS can throw on undefined
Error context ✅ Rich ✅ Good Both provide line numbers
Partial errors ✅ Configurable ✅ Returns error Different strategies

LiquidJS:

const engine = new Liquid({
  strictVariables: true, // Throw on undefined variables
  strictFilters: true // Throw on undefined filters
})

Go Liquid:

  • Undefined variables return empty string
  • Undefined filters cause parse error
  • Errors returned via Go error type

4.5 Performance & Resource Limits

Feature LiquidJS Go Liquid Notes
Template caching Both cache parsed templates
Memory limits ✅ Configurable ⚠️ OS-level LiquidJS tracks memory
Timeout limits ✅ Configurable ⚠️ Manual LiquidJS has renderLimit
Parse limits ✅ Configurable LiquidJS has parseLimit

LiquidJS:

const engine = new Liquid({
  parseLimit: 102400, // 100KB template size
  renderLimit: 5000, // 5 second timeout
  memoryLimit: 10485760 // 10MB memory
})

Go Liquid:

  • No built-in resource limits
  • Use Go's context for timeouts
  • Memory managed by Go runtime

5. Syntax & Behavior Differences

5.1 Include vs Render

LiquidJS:

  • include: Shares parent scope, variables accessible both ways
  • render: Isolated scope, must pass variables explicitly
  • Recommended: Use render for better encapsulation

Go Liquid:

  • Only include available
  • Scope controlled via context API
  • Variables passed via RenderFile parameters

5.2 Variable Scoping

LiquidJS:

{% assign x = 1 %}
{% include "partial" %}  {# Can access x #}
{% render "partial" %}   {# Cannot access x #}
{% render "partial", x: x %}  {# Must pass explicitly #}

Go Liquid:

{% assign x = 1 %}
{% include "partial" %}  {# Scope depends on implementation #}

5.3 Filter Argument Syntax

Both support two syntaxes for filter arguments:

Comma syntax:

{{ "hello" | append: " world" }}
{{ array | slice: 0, 5 }}

Keyword syntax (named parameters):

{{ false | default: "value", allow_false: true }}

5.4 Array Iteration

LiquidJS:

{% for item in array %}
  {{ item }}
{% endfor %}

{% for item in hash %}
  {{ item[0] }}: {{ item[1] }}
{% endfor %}

Go Liquid:

{% for item in array %}
  {{ item }}
{% endfor %}

{% for pair in hash %}
  {{ pair[0] }}: {{ pair[1] }}
{% endfor %}

Both iterate over maps/objects as key-value pairs.


6. Limitations & Missing Features

6.1 Go Liquid Limitations

Missing Tags:

  • render - Use include with context isolation
  • layout / block - No template inheritance
  • echo - Use {{ }} output tags
  • liquid - Use standard tag delimiters
  • increment / decrement - Manual counter management
  • ❌ Inline comments ({% # %}) - Use {% comment %}

Missing Filters:

  • ❌ Expression filters (where_exp, reject_exp, etc.)
  • ❌ Advanced array filters (group_by, sum, push, pop, etc.)
  • ❌ Date formatting filters (use date with strftime)
  • ❌ URL filters (slugify, cgi_escape, uri_escape)
  • ❌ Base64 filters
  • remove_last, replace_last
  • normalize_whitespace, number_of_words
  • at_least, at_most
  • array_to_sentence_string
  • to_integer

API Limitations:

  • Manual context management for timeouts
  • No built-in memory/parse size limits
  • Less flexible dynamic partials

6.2 LiquidJS Limitations

None identified - LiquidJS appears to be a more feature-complete implementation with:

  • All standard Liquid tags and filters
  • Additional Jekyll-compatible features
  • Expression-based filters
  • Better resource limit controls
  • More flexible partial system

7. Extensibility

7.1 Custom Filters

LiquidJS:

engine.registerFilter('shout', (v) => v.toUpperCase() + '!!!')

// Async filter
engine.registerFilter('fetchData', async (url) => {
  const response = await fetch(url)
  return response.json()
})

Go Liquid:

engine.AddFilter("shout", func(s string) string {
  return strings.ToUpper(s) + "!!!"
})

7.2 Custom Tags

LiquidJS:

engine.registerTag('upper', {
  parse(token) {
    this.str = token.args
  },
  render(ctx) {
    const str = this.liquid.evalValue(this.str, ctx)
    return str.toUpperCase()
  }
})

Go Liquid:

engine.AddTag("upper", func(source string) (func(io.Writer, render.Context) error, error) {
  return func(w io.Writer, ctx render.Context) error {
    // Parse and render logic
  }, nil
})

7.3 Custom File Systems

LiquidJS:

const engine = new Liquid({
  fs: {
    readFileSync: (file) => templates[file] || '',
    existsSync: (file) => templates.hasOwnProperty(file),
    resolve: (root, file) => file
  }
})

Go Liquid:

type TemplateStore interface {
  ReadTemplate(name string) ([]byte, error)
}

engine.SetTemplateStore(myStore)

Appendix: Complete Filter List

LiquidJS Filters (85)

Array (26): abs, append, array_to_sentence_string, at_least, at_most, base64_decode, base64_encode, capitalize, ceil, cgi_escape, compact, concat, date, date_to_long_string, date_to_rfc822, date_to_string, date_to_xmlschema, default, divided_by, downcase, escape, escape_once, find, find_exp, find_index, find_index_exp

Continuation: first, floor, group_by, group_by_exp, has, has_exp, inspect, join, json, jsonify, last, lstrip, map, minus, modulo, newline_to_br, normalize_whitespace, number_of_words, plus, pop, prepend, push, raw, reject, reject_exp, remove, remove_first, remove_last, replace, replace_first, replace_last, reverse, round, rstrip, shift, size, slice, slugify, sort, sort_natural, split, strip, strip_html, strip_newlines, sum, times, to_integer, truncate, truncatewords, uniq, unshift, upcase, uri_escape, url_decode, url_encode, where, where_exp, xml_escape

Go Liquid Filters (45)

All Filters: abs, append, capitalize, ceil, compact, concat, date, default, divided_by, downcase, escape, escape_once, first, floor, inspect, join, json, last, lstrip, map, minus, modulo, newline_to_br, plus, prepend, remove, remove_first, replace, replace_first, reverse, round, rstrip, size, slice, sort, sort_natural, split, strip, strip_html, strip_newlines, times, truncate, truncatewords, type, uniq, upcase, url_decode, url_encode


End of Comparison Document

pierre-b avatar Nov 18 '25 10:11 pierre-b

Couldn't wait actually, I implemented it in https://github.com/osteele/liquid/pull/129

pierre-b avatar Nov 18 '25 11:11 pierre-b

For the record I just released a full feature parity - Liquid Golang implementation - of the official Ruby lib: https://github.com/Notifuse/liquidgo

pierre-b avatar Nov 19 '25 09:11 pierre-b