Kirill Platonov
Kirill Platonov
All Posts Sep 22, 2021

Detecting Shopify theme version in Rails

When building theme app extensions you’ll definitely need a way to detect the version of the theme (Vintage or Online Store 2.0). Because the integration for every version will be different.

The official Shopify docs provide only instruction for Node.js. So I’d like to share the approach for Rails that I was using in my latest Product Color Swatches app.

To detect the version I use shopify_api and extend Theme object by adding #version method. I found this solution the cleanest and easy to use. Doing the same in GraphQL will be extremely verbose so I stick with REST and shopify_api for this task.

You’ll need to add the following code to config/initializers/shopify_app.rb:

class ShopifyAPI::Theme
  # Specify templates required by the app (where app block will be installed).
  # E.g. %w[product collection index]
  APP_BLOCK_TEMPLATES = %w[product]

  def assets
    ShopifyAPI::Asset.find(:all, params: { theme_id: self.id })
  end

  def version
    @version ||= begin
      theme_assets = assets
      template_json_files = theme_assets.select do |asset|
        APP_BLOCK_TEMPLATES.any? { |t| asset.key ==  "templates/#{t}.json"}
      end
      return :vintage if template_json_files.size != APP_BLOCK_TEMPLATES.size

      template_main_sections = template_json_files.map do |template|
        asset = ShopifyAPI::Asset.find(template.key, params: { theme_id: self.id })
        json = JSON.parse(asset.value)

        main = json["sections"].find { |k, v| k == "main" || v["type"].start_with?("main-") }
        if main
          theme_assets.find { |asset| asset.key == "sections/#{main[1]["type"]}.liquid" }
        elsif template.key == "templates/index.json"
          theme_assets.find { |asset| asset.key == "sections/apps.liquid" }
        else
          nil
        end
      end.compact

      sections_with_app_block = template_main_sections.map do |asset|
        accepts_app_block = false
        file = ShopifyAPI::Asset.find(asset.key, params: { theme_id: self.id} )
        match = file.value.match(/\{\%[-\s]+schema[-\s]+\%\}([\s\S]*?)\{\%[-\s]+endschema[-\s]+\%\}/m)
        schema = match && JSON.parse(match[1])
        if schema && schema["blocks"]
          accepts_app_block = schema["blocks"].any? { |b| b["type"] == "@app" }
        end

        accepts_app_block ? file : nil
      end.compact

      if sections_with_app_block.size > 0
        :os2
      else
        :vintage
      end
    end
  end
end

You can customize which templates should be checked for app blocks support in APP_BLOCK_TEMPLATES constant. It will be different for every app. In my app users can only add app block to the product page so I need to check only product template for blocks support.

After adding this code you can use it like this:

theme = ShopifyAPI::Theme.find(THEME_ID)
theme.version
# => :os2 or :vintage

Very easy. I like that solution and I wish we could add this code to shopify_api gem eventually. In the next articles, I will tell more about my experience building theme extension.

If you like reading about Shopify app development follow me on Twitter.

UPD Nov 20, 2022:

I’ve updated the code to support ShopifyAPI 10+:

# config/initializers/shopify_app.rb

Rails.application.config.after_initialize do
  class ShopifyAPI::Theme
    # Specify templates required by the app (where app block will be installed).
    # E.g. %w[product collection index]
    APP_BLOCK_TEMPLATES = %w[product]

    def assets
      ShopifyAPI::Asset.all(theme_id: self.id)
    end

    def version
      @version ||= begin
        theme_assets = assets
        template_json_files = theme_assets.select do |asset|
          APP_BLOCK_TEMPLATES.any? { |t| asset.key ==  "templates/#{t}.json"}
        end
        return :vintage if template_json_files.size != APP_BLOCK_TEMPLATES.size

        template_main_sections = template_json_files.map do |template|
          asset = ShopifyAPI::Asset.all(theme_id: self.id, asset: {key: template.key}).first
          json = JSON.parse(asset.value)

          main = json["sections"].find { |k, v| k == "main" || v["type"].start_with?("main-") }
          if main
            theme_assets.find { |asset| asset.key == "sections/#{main[1]["type"]}.liquid" }
          elsif template.key == "templates/index.json"
            theme_assets.find { |asset| asset.key == "sections/apps.liquid" }
          else
            nil
          end
        end.compact

        sections_with_app_block = template_main_sections.map do |asset|
          accepts_app_block = false
          file = ShopifyAPI::Asset.all(theme_id: self.id, asset: {key: asset.key}).first
          match = file.value.match(/\{\%[-\s]+schema[-\s]+\%\}([\s\S]*?)\{\%[-\s]+endschema[-\s]+\%\}/m)
          schema = match && JSON.parse(match[1])
          if schema && schema["blocks"]
            accepts_app_block = schema["blocks"].any? { |b| b["type"] == "@app" }
          end

          accepts_app_block ? file : nil
        end.compact

        if sections_with_app_block.size > 0
          :os2
        else
          :vintage
        end
      end
    end
  end
end

Subscribe to get new blog posts via email

Or grab the RSS feed