LiquidResponse

Rails controller and view helpers for rendering Liquid templates — designed for Shopify App Proxy and any Rails app that serves Liquid markup to a storefront.

How it works

Shopify's App Proxy lets your Rails app respond to storefront requests with Liquid templates, which Shopify then renders in the context of the live theme. LiquidResponse handles:

  • Registering the liquid MIME type with Rails
  • Telling Rails to prefer liquid over html for app proxy requests
  • Stripping turbo-stream from the Accept header (Turbo injects it everywhere; the proxy can't use it)
  • View helpers for building valid Liquid {% render %}, {% assign %}, and {% capture %} tags from ERB

Installation

Add to your Gemfile:

gem "liquid_response"

Setup

1. Register the MIME type

# config/initializers/liquid_response.rb
require "liquid_response"

LiquidResponse.register_liquid_mime_type!

2. Include the controller helper

class AppProxyController < ApplicationController
  include LiquidResponse::ControllerHelper

  allow_liquid_rendering_when_html_is_requested
end

allow_liquid_rendering_when_html_is_requested installs a before_action that:

  • Strips turbo-stream from request.formats
  • Prepends Mime[:liquid] so Rails picks the .liquid.erb template

3. Create .liquid.erb views

Rails will look for app/views/posts/show.liquid.erb when the request format resolves to liquid. Write Liquid tags using the view helpers:

<%# app/views/posts/show.liquid.erb %>
<%= render_liquid "my-app--post--card", title: @post.title, published: @post.published? %>

This outputs:

{% render "my-app--post--card", title: "Hello World", published: true %}

View helpers

All helpers are available in views once LiquidResponse::ControllerHelper is included (it auto-includes LiquidResponse::ViewHelper as a helper module).

render_liquid(snippet_name, **assigns)

Emits a Liquid {% render %} tag with the given assigns.

<%= render_liquid "my-app--product--card",
      title: @product.title,
      price: @product.price,
      available: @product.available? %>
{% render "my-app--product--card", title: "Widget", price: 9.99, available: true %}

assign_liquid(variable_name, value)

Emits a {% assign %} tag. Useful for hoisting a value to Liquid scope before passing it to a snippet.

<%= assign_liquid :greeting, "Welcome" %>
{% assign greeting = "Welcome" %}

liquid_variable(name)

Returns a LiquidVariable object — a placeholder that renders as a bare Liquid variable name (no quotes). Pass it as an assign value when the variable already exists in Liquid scope.

{% assign products = collections.all.products %}
<%= render_liquid "my-app--product--list", items: liquid_variable(:products) %>
{% render "my-app--product--list", items: products %}

liquid_capture(name, &block)

Wraps the block output in {% capture %} / {% endcapture %}.

<%= liquid_capture(:sidebar) do %>
  <p>Some sidebar content</p>
<% end %>
{% capture sidebar %}<p>Some sidebar content</p>{% endcapture %}

Passing complex values

Strings, numbers, booleans, nil, dates, times, symbols

Converted to their JSON equivalents automatically.

<%= render_liquid "my-app--order",
      id: 42,
      status: :pending,
      placed_on: Date.today,
      gift: false,
      note: nil %>
{% render "my-app--order", id: 42, status: "pending", placed_on: "2026-05-26", gift: false, note: null %}

Hashes (nested objects)

Each Hash assign is promoted to an intermediate {% assign %} variable so Liquid receives it as an object.

<%= render_liquid "my-app--address--card",
      address: {
        street: @address.line1,
        city:   @address.city,
        zip:    @address.zip
      } %>
{% assign h_address_a1b2c = null | default: street: "Via Roma 1", city: "Roma", zip: "00100" %}
{% render "my-app--address--card", address: h_address_a1b2c %}

Nesting works recursively:

<%= render_liquid "my-app--subscription",
      plan: {
        name:     "Monthly",
        interval: { unit: "week", count: 4 }
      } %>
{% assign h_interval_x = null | default: unit: "week", count: 4 %}
{% assign h_plan_y     = null | default: name: "Monthly", interval: h_interval_x %}
{% render "my-app--subscription", plan: h_plan_y %}

Arrays

Arrays are converted to indexed hashes (Liquid has no native array literal):

<%= render_liquid "my-app--tag--list", tags: ["sale", "new"] %>
{% assign h_tags_z = null | default: 0: "sale", 1: "new" %}
{% render "my-app--tag--list", tags: h_tags_z %}

Custom objects — as_liquid protocol

Any object that implements as_liquid is serialized through it. The method must return a Hash, a scalar, or another as_liquid-capable object.

class Product
  def as_liquid
    { "title" => title, "price" => price.to_f, "available" => available? }
  end
end
<%= render_liquid "my-app--product--card", product: @product %>

LiquidResponse ships with as_liquid on String, Numeric, TrueClass, FalseClass, NilClass, Date, Time, Symbol, Array, Hash, and Data — all the types you use day-to-day.

ActiveModel / Serializable objects

Objects that respond to serializable_hash are converted automatically:

<%= render_liquid "my-app--user--card", user: current_user %>

Mixing types

Everything composes:

<%= render_liquid "my-app--portal",
      customer_name: @customer.name,
      logged_in:     true,
      items:         liquid_variable(:cart_items),
      address: {
        city: @shipping_address.city,
        zip:  @shipping_address.zip
      } %>

liquid_routes (app-level, not in the gem)

The helper liquid_routes — which exposes Rails route helpers as a Liquid-usable hash — is intentionally not part of this gem. It depends on your app's routing conventions. Define it yourself in your base controller and expose it via helper_method:

class AppProxyController < ApplicationController
  include LiquidResponse::ControllerHelper
  allow_liquid_rendering_when_html_is_requested

  helper_method :liquid_routes

  private

  def liquid_routes
    Rails.application.routes.routes
      .filter_map do |route|
        next unless route.name&.start_with?("my_app_proxy_")
        [route.name.sub("my_app_proxy_", "") + "_url", route.path.spec.to_s.sub(/\([^)]*\)/, "")]
      end
      .sort.to_h
  end
end

Then in views:

<%= render_liquid "my-app--nav", routes: liquid_routes %>

Development

bundle install
bundle exec rake test

License

MIT