Last updated

Schema Components: Complete Developer Guide

This comprehensive guide explains how to build dynamic, customizable theme sections using Fluid's schema system. Learn through the from production themes, with complete code snippets showing both schema definitions and Liquid implementations.

What You'll Learn

  • Resource selectors - How to let users select products, collections, categories, and posts
  • Global loops - How to display all items from a resource type with pagination
  • Blocks - How to create repeatable, reorderable content units
  • Settings - How to configure section-level options
  • Real examples - Production-ready patterns from actual implementations
  • Template integration - How templates and sections work together

How Templates and Sections Work Together

Understanding the three-layer architecture is crucial for building Fluid themes. This section explains how layouts, templates, and sections work together.

The Three-Layer Architecture

1. Layout (theme.liquid)
   └── Wraps entire page with <html>, <head>, <body>
   └── Includes global sections (navbar, footer)
   └── Renders template via {{ content_for_layout }}
   
2. Template (e.g., home_page/default/index.liquid, product/default/index.liquid)
   └── Defines which sections to include
   └── Has its own schema defining section order and default settings
   
3. Sections (e.g., hero_section, main_product, related_products)
   └── Each Section has:
       ├── Schema (defines settings/blocks available)
       ├── Liquid Template (renders content)
       ├── Styles (CSS)
       └── Settings Values (stored per template instance)

How It Works: Real Example

Layer 1: Layout (theme.liquid)

The layout wraps every page with HTML structure and global sections:

File: app/themes/templates/base/layouts/theme.liquid

<!DOCTYPE html>
<html lang="{{localization.language.iso_code}}">
  <head>
    {{ content_for_header }}
    {`%`- comment -`%`} CSS, fonts, global settings {`%`- endcomment -`%`}
  </head>
  <body>
    {`%`- comment -`%`} Global section - appears on every page {`%`- endcomment -`%`}
    {`%` section 'navbar' `%`}
    
    {`%`- comment -`%`} Template content renders here {`%`- endcomment -`%`}
    {{ content_for_layout }}
    
    {`%`- comment -`%`} Global section - appears on every page {`%`- endcomment -`%`}
    {`%` section 'footer' `%`}
    
    {`%`- comment -`%`} Global scripts {`%`- endcomment -`%`}
    <script src="{{ 'global.js' | asset_url }}"></script>
  </body>
</html>

Layer 2: Template Defines Sections

File: app/themes/templates/base/home_page/default/index.liquid


  {`%`- comment -`%`} Each template calls specific sections {`%`- endcomment -`%`}
  {`%` section 'hero_section', id: 'hero_section' `%`}
  {`%` section 'intro_section', id: 'intro_section' `%`}
  {`%` section 'multiple_slider', id: 'multiple_slider' `%`}
  {`%` section 'banner1', id: 'banner1' `%`}
  {`%` section 'testimonial', id: 'testimonial' `%`}
  {`%` section 'feature', id: 'feature' `%`}
  {`%` section 'blog', id: 'blog' `%`}
  {`%` section 'brand', id: 'brand' `%`}


{`%`- comment -`%`} Template schema defines which sections are available {`%`- endcomment -`%`}
{`%` schema `%`}
{
  "name": "Home Page",
  "sections": {
    "hero_section": {
      "type": "hero_section"
    },
    "intro_section": {
      "type": "intro_section"
    },
    "multiple_slider": {
      "type": "multiple_slider"
    }
    // ... more sections
  }
}
{`%` endschema `%`}

Another Template: app/themes/templates/base/product/default/index.liquid

{`%`- comment -`%`} Product template uses different sections {`%`- endcomment -`%`}
{`%` section 'main_product', id: 'main_product' `%`}
{`%` section 'testimonial_slider', id: 'testimonial_slider' `%`}
{`%` section 'related_products', id: 'related_products' `%`}
{`%` section 'cta_banner5', id: 'cta_banner5' `%`}

{`%` schema `%`}
{
  "sections": {
    "main_product": {
      "type": "main_product",
      "settings": {
        "show_breadcrumb": true
      }
    },
    "related_products": {
      "type": "related_products",
      "settings": {
        "heading": "Related Products",
        "heading_size": "h1"
      }
    }
  }
}
{`%` endschema `%`}

Layer 3: Section Has Schema

File: app/themes/templates/base/sections/hero_section/index.liquid

{`%`- comment -`%`} Section renders its content {`%`- endcomment -`%`}
<section class="hero">
  <h1>{{ section.settings.heading }}</h1>
  <p>{{ section.settings.subtitle }}</p>
</section>

{`%`- comment -`%`} Section schema defines what settings are available {`%`- endcomment -`%`}
{`%` schema `%`}
{
  "name": "Hero Section",
  "tag": "section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Hero Heading",
      "default": "Welcome"
    },
    {
      "type": "textarea",
      "id": "subtitle",
      "label": "Subtitle",
      "default": "Your journey starts here"
    }
  ]
}
{`%` endschema `%`}

Key Concepts

1. Layout = Global Wrapper

  • One layout per theme (usually theme.liquid)
  • Contains <html>, <head>, <body> tags
  • Includes global sections (navbar, footer)
  • Uses {{ content_for_layout }} to inject template content

2. Template = Page Structure

  • Each page type has a template (home_page, product, collection, etc.)
  • Defines which sections appear on that page type
  • Has its own schema listing available sections
  • Can set default settings for sections

3. Section Schema = Settings Definition

  • Defines what settings/blocks are available
  • Does NOT store actual values
  • Reusable across multiple templates

4. Template Data = Actual Values

  • Each template instance stores section settings
  • Same section, different values per template

Data Flow Example

When a user visits the home page:

1. Fluid loads: theme.liquid
   ├── Renders <head> with global CSS
   ├── Renders {`%` section 'navbar' `%`} (global)2. Fluid injects: {{ content_for_layout }}
   └── Loads: home_page/default/index.liquid
       ├── Reads template schema
       ├── Renders {`%` section 'hero_section' `%`}
       │   └── Loads sections/hero_section/index.liquid
       │       ├── Reads section schema
       │       ├── Gets settings from home_page data
       │       └── Renders with section.settings.heading
       │
       ├── Renders {`%` section 'intro_section' `%`}
       └── Renders other sections...3. Back to theme.liquid
   ├── Renders {`%` section 'footer' `%`} (global)
   └── Closes </body></html>

Settings Storage Structure

{
  "home_page": {
    "sections": {
      "hero_section": {
        "type": "hero_section",
        "settings": {
          "heading": "Transform Your Life Today",
          "subtitle": "Join thousands of satisfied customers"
        }
      },
      "intro_section": {
        "type": "intro_section",
        "settings": {
          "text": "Welcome to our store..."
        }
      }
    }
  },
  "product": {
    "sections": {
      "main_product": {
        "type": "main_product",
        "settings": {
          "show_breadcrumb": true
        }
      },
      "related_products": {
        "type": "related_products",
        "settings": {
          "heading": "Related Products"
        }
      }
    }
  }
}

Important: Each template (home_page, product, etc.) stores independent section settings.

Template Schema vs Section Schema

Understanding the difference between these two schemas is critical:

Template Schema

Located in the template file (e.g., home_page/default/index.liquid):

{`%` schema `%`}
{
  "name": "Home Page",
  "sections": {
    "hero_section": {
      "type": "hero_section"  // References section by type
    },
    "intro_section": {
      "type": "intro_section",
      "settings": {
        "heading": "Default Heading"  // Default values for THIS template
      }
    }
  }
}
{`%` endschema `%`}

Purpose:

  • Lists which sections can appear on this template
  • Sets default values for section settings (optional)
  • Defines section order
  • Each template has ONE schema

Section Schema

Located in the section file (e.g., sections/hero_section/index.liquid):

{`%` schema `%`}
{
  "name": "Hero Section",
  "tag": "section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Hero Heading",
      "default": "Welcome"  // Default when section is first added
    }
  ]
}
{`%` endschema `%`}

Purpose:

  • Defines what settings/blocks are available
  • Provides UI labels and controls
  • Sets default values when section is first added
  • Reusable across multiple templates

How Schema Inheritance Works

When a template includes a section, here's what happens:

1. Template calls: {`%` section 'hero_section', id: 'hero_section' `%`}2. Fluid looks up: sections/hero_section/index.liquid
   │
3. Reads section schema to know available settings
   │
4. Looks for stored data in template data:
   └── IF found: Use customized values
   └── IF NOT found: Use template schema defaults OR section schema defaults
   │
5. Renders section with section.settings object

Example Flow

Section defines structure:

{
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "default": "Welcome"
    }
  ]
}

Template sets instance defaults:

{
  "sections": {
    "hero_section": {
      "type": "hero_section",
      "settings": {
        "heading": "Transform Your Life"  // Template-specific default
      }
    }
  }
}

User customizes in editor:

{
  "home_page": {
    "sections": {
      "hero_section": {
        "settings": {
          "heading": "Welcome to Our Store"  // User's custom value
        }
      }
    }
  }
}

Fallback chain:

  1. User's custom value ✅ "Welcome to Our Store"
  2. Template schema default (if no custom value)
  3. Section schema default (if no template default)

Accessing Section Data in Liquid

When a section renders, it automatically receives data through the section object:

{`%`- comment -`%`} Inside any section file {`%`- endcomment -`%`}

{`%`- comment -`%`} Section metadata {`%`- endcomment -`%`}
{{ section.id }}           {`%`- comment -`%`} Unique ID: "hero_section" {`%`- endcomment -`%`}
{{ section.type }}         {`%`- comment -`%`} Section type: "hero_section" {`%`- endcomment -`%`}

{`%`- comment -`%`} Section settings {`%`- endcomment -`%`}
{{ section.settings }}     {`%`- comment -`%`} Object with all setting values {`%`- endcomment -`%`}
{{ section.settings.heading }}
{{ section.settings.background_color }}

{`%`- comment -`%`} Section blocks {`%`- endcomment -`%`}
{{ section.blocks }}       {`%`- comment -`%`} Array of block objects {`%`- endcomment -`%`}
{{ section.blocks.size }}  {`%`- comment -`%`} Number of blocks {`%`- endcomment -`%`}

{`%`- comment -`%`} Loop through blocks {`%`- endcomment -`%`}
{`%` for block in section.blocks `%`}
  {{ block.id }}              {`%`- comment -`%`} Block ID {`%`- endcomment -`%`}
  {{ block.type }}            {`%`- comment -`%`} Block type {`%`- endcomment -`%`}
  {{ block.settings }}        {`%`- comment -`%`} Block settings {`%`- endcomment -`%`}
  {{ block.fluid_attributes }} {`%`- comment -`%`} Required for editor {`%`- endcomment -`%`}
{`%` endfor `%`}

Real-World Example: Same Section, Different Templates

Let's see how the same related_products section has different settings in different templates:

The Section (Reusable)

File: sections/related_products/index.liquid

<section class="related-products">
  <h2>{{ section.settings.heading }}</h2>
  
  {`%` for block in section.blocks `%`}
    {`%` assign product = block.settings.product `%`}
    <div class="product-card">
      <h3>{{ product.title }}</h3>
      <p>{{ product.price | money }}</p>
    </div>
  {`%` endfor `%`}
</section>

{`%` schema `%`}
{
  "name": "Related Products",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Section Heading",
      "default": "You May Also Like"
    }
  ],
  "blocks": [
    {
      "type": "product_card",
      "name": "Product",
      "settings": [
        {
          "type": "product",
          "id": "product",
          "label": "Select Product"
        }
      ]
    }
  ]
}
{`%` endschema `%`}

Used in Product Template

File: product/default/index.liquid

{`%` section 'related_products', id: 'related_products' `%`}

{`%` schema `%`}
{
  "sections": {
    "related_products": {
      "type": "related_products",
      "settings": {
        "heading": "Complete Your Collection"
      }
    }
  }
}
{`%` endschema `%`}

Stored data for Product A:

{
  "product_A": {
    "sections": {
      "related_products": {
        "settings": {
          "heading": "Perfect Pairings"
        },
        "blocks": [
          {
            "type": "product_card",
            "settings": { "product": 123 }
          }
        ]
      }
    }
  }
}

Used in Home Page Template

File: home_page/default/index.liquid

{`%` section 'related_products', id: 'featured_products' `%`}

{`%` schema `%`}
{
  "sections": {
    "featured_products": {
      "type": "related_products",
      "settings": {
        "heading": "Staff Favorites"
      }
    }
  }
}
{`%` endschema `%`}

Stored data for Home Page:

{
  "home_page": {
    "sections": {
      "featured_products": {
        "settings": {
          "heading": "This Month's Bestsellers"
        },
        "blocks": [
          {
            "type": "product_card",
            "settings": { "product": 456 }
          }
        ]
      }
    }
  }
}

Result: Same section schema, completely different data per template!

Blocks: Template-Specific Arrays

Blocks work the same way - they're stored per template instance:

Section Schema Defines Block Structure

{
  "name": "Features",
  "blocks": [
    {
      "type": "feature",
      "name": "Feature Item",
      "settings": [
        {
          "type": "text",
          "id": "title",
          "label": "Title"
        }
      ]
    }
  ]
}

Home Page Has Different Blocks

{
  "home_page": {
    "sections": {
      "features_section": {
        "type": "features",
        "blocks": [
          {
            "id": "block_1",
            "type": "feature",
            "settings": { "title": "Fast Shipping" }
          },
          {
            "id": "block_2",
            "type": "feature",
            "settings": { "title": "Easy Returns" }
          }
        ]
      }
    }
  }
}

About Page Has Different Blocks

{
  "about_page": {
    "sections": {
      "features_section": {
        "type": "features",
        "blocks": [
          {
            "id": "block_3",
            "type": "feature",
            "settings": { "title": "Founded in 2020" }
          },
          {
            "id": "block_4",
            "type": "feature",
            "settings": { "title": "Family Owned" }
          }
        ]
      }
    }
  }
}

Accessing in Liquid (Same Code, Different Results)

{`%` for block in section.blocks `%`}
  <div {{ block.fluid_attributes }}>
    <h3>{{ block.settings.title }}</h3>
  </div>
{`%` endfor `%`}

On home_page: Shows "Fast Shipping", "Easy Returns"
On about_page: Shows "Founded in 2020", "Family Owned"

Global Sections vs Template Sections

Global Sections (in theme.liquid)

These appear on EVERY page:

{`%`- comment -`%`} In layouts/theme.liquid {`%`- endcomment -`%`}
{`%` section 'navbar' `%`}
{{ content_for_layout }}
{`%` section 'footer' `%`}
  • Rendered outside of templates
  • Same content across all pages
  • Settings stored at theme level (not per template)
  • User customizes once, affects all pages

Template Sections (in templates)

These are template-specific:

{`%`- comment -`%`} In home_page/default/index.liquid {`%`- endcomment -`%`}
{`%` section 'hero_section', id: 'hero_section' `%`}
{`%` section 'features', id: 'features' `%`}
  • Rendered inside {{ content_for_layout }}
  • Different content per template
  • Settings stored per template
  • User customizes per template

Real-World Example: Product Template

Let's see how a product template uses sections:

File: app/themes/templates/YourTheme/product/default/index.liquid

<!DOCTYPE html>
<html>
<body>
  
  {`%`- comment -`%`} Each section inherits its schema {`%`- endcomment -`%`}
  {`%` section 'product-header' `%`}
  {`%` section 'product-details' `%`}
  {`%` section 'also-bought' `%`}
  {`%` section 'reviews' `%`}
  
</body>
</html>

Section: sections/also-bought/index.liquid

<section class="also-bought">
  <h2>{{ section.settings.heading }}</h2>
  
  {`%` for block in section.blocks `%`}
    {`%` assign product = block.settings.product `%`}
    <div class="product-card">
      <h3>{{ product.title }}</h3>
      <p>{{ product.price | money }}</p>
    </div>
  {`%` endfor `%`}
</section>

{`%` schema `%`}
{
  "name": "Also Bought",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Section Heading",
      "default": "Customers Also Bought"
    }
  ],
  "blocks": [
    {
      "type": "product_card",
      "name": "Product",
      "settings": [
        {
          "type": "product",
          "id": "product",
          "label": "Select Product"
        }
      ]
    }
  ]
}
{`%` endschema `%`}

Stored Data for Product A:

{
  "product_template_A": {
    "sections": {
      "also_bought_123": {
        "type": "also-bought",
        "settings": {
          "heading": "People Who Bought This Also Liked"
        },
        "blocks": [
          {
            "type": "product_card",
            "settings": {
              "product": 456
            }
          }
        ]
      }
    }
  }
}

Stored Data for Product B:

{
  "product_template_B": {
    "sections": {
      "also_bought_789": {
        "type": "also-bought",
        "settings": {
          "heading": "You May Also Like"
        },
        "blocks": [
          {
            "type": "product_card",
            "settings": {
              "product": 789
            }
          }
        ]
      }
    }
  }
}

Section Variables in Liquid

When a section renders, several variables are automatically available:

{`%`- comment -`%`} section object {`%`- endcomment -`%`}
{{ section.id }}           {`%`- comment -`%`} Unique ID: "hero_section_abc123" {`%`- endcomment -`%`}
{{ section.settings }}     {`%`- comment -`%`} Object with all setting values {`%`- endcomment -`%`}
{{ section.blocks }}       {`%`- comment -`%`} Array of block objects {`%`- endcomment -`%`}
{{ section.blocks.size }}  {`%`- comment -`%`} Number of blocks {`%`- endcomment -`%`}

{`%`- comment -`%`} Individual settings {`%`- endcomment -`%`}
{{ section.settings.heading }}
{{ section.settings.background_color }}

{`%`- comment -`%`} Loop through blocks {`%`- endcomment -`%`}
{`%` for block in section.blocks `%`}
  {{ block.id }}              {`%`- comment -`%`} Block ID {`%`- endcomment -`%`}
  {{ block.type }}            {`%`- comment -`%`} Block type {`%`- endcomment -`%`}
  {{ block.settings }}        {`%`- comment -`%`} Block settings {`%`- endcomment -`%`}
  {{ block.fluid_attributes }} {`%`- comment -`%`} Required for editor {`%`- endcomment -`%`}
{`%` endfor `%`}

Dynamic Sections vs Static Sections

Dynamic Sections (can be added/removed/reordered):

{`%`- comment -`%`} In template {`%`- endcomment -`%`}
{`%` section 'hero' `%`}
{`%` section 'features' `%`}
{`%`- comment -`%`} User can add/remove these in editor {`%`- endcomment -`%`}

Static Sections (always present):

{`%`- comment -`%`} In template {`%`- endcomment -`%`}
{`%` render 'header' `%`}
{`%`- comment -`%`} Always rendered, cannot be removed {`%`- endcomment -`%`}

Schema Updates and Backward Compatibility

When you update a section's schema, existing template data continues to work:

Adding a New Setting

Before:

{
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading"
    }
  ]
}

After:

{
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading"
    },
    {
      "type": "text",
      "id": "subheading",
      "label": "Subheading",
      "default": "New field"  // ← Existing instances use this
    }
  ]
}

Result:

  • Existing templates: Get the default value
  • New templates: Also get the default value
  • No breaking changes ✅

Removing a Setting

Before:

{
  "settings": [
    {
      "type": "text",
      "id": "old_field",
      "label": "Old Field"
    },
    {
      "type": "text",
      "id": "heading",
      "label": "Heading"
    }
  ]
}

After:

{
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading"
    }
  ]
}

Result:

  • Old data in templates is preserved but ignored
  • Section continues to work normally
  • No breaking changes ✅

Changing a Setting ID (Breaking Change!)

Before:

{ "id": "title" }

After:

{ "id": "heading" }

Result:

  • Creates a NEW setting with default value
  • Old title data is preserved but NOT accessible
  • Existing templates lose their customizations ❌
  • This is a breaking change!

Better approach:

  1. Add new setting with new ID
  2. Keep old setting temporarily
  3. Migration script to copy old → new
  4. Remove old setting after migration

Best Practices: Layout-Template-Section Integration

DO:

Architecture:

  • Keep global sections (navbar, footer) in theme.liquid
  • Use templates to define page-specific section arrangements
  • Make sections reusable across multiple templates
  • Use descriptive IDs when calling sections: {%section 'hero', id: 'home_hero'%}

Schema Design:

  • Provide sensible defaults in section schemas
  • Set template-specific defaults in template schemas
  • Document which templates use which sections
  • Use consistent naming conventions

Data Handling:

  • Use | default filter for optional settings: {{ section.settings.heading | default: "Default" }}
  • Check for blank values before rendering: {%if section.settings.text != blank%}
  • Always include {{ block.fluid_attributes }} on block elements
  • Handle empty blocks gracefully: {%if section.blocks.size > 0%}

Performance:

  • Limit number of sections per template (10-15 max recommended)
  • Use lazy loading for images in sections
  • Minimize database lookups in section loops

DON'T:

Architecture:

  • Don't put page-specific content in theme.liquid
  • Don't hardcode values that should be settings
  • Don't duplicate section code - make sections reusable
  • Don't forget that same section = different data per template

Schema Changes:

  • Don't change setting IDs without migration plan
  • Don't remove settings without considering backward compatibility
  • Don't assume settings always have values
  • Don't forget to provide defaults for new settings

Data Access:

  • Don't use global variables when section settings exist
  • Don't access template data directly - use section.settings
  • Don't forget to validate resource selectors (product/collection might not exist)
  • Don't skip fluid_attributes on blocks (breaks editor)

Debugging: Template and Section Data

Check the rendering flow:

{`%`- comment -`%`} In theme.liquid {`%`- endcomment -`%`}
<p>Layout: theme.liquid loaded</p>
{{ content_for_layout }}

{`%`- comment -`%`} In home_page/default/index.liquid {`%`- endcomment -`%`}
<p>Template: home_page loaded</p>
{`%` section 'hero_section', id: 'hero_section' `%`}

{`%`- comment -`%`} In sections/hero_section/index.liquid {`%`- endcomment -`%`}
<p>Section: hero_section loaded</p>

Output section data:

{`%`- comment -`%`} Debug: Output all section settings {`%`- endcomment -`%`}
<pre>
  Section ID: {{ section.id }}
  Section Type: {{ section.type }}
  Settings: {{ section.settings | json }}
  Blocks Count: {{ section.blocks.size }}
  Blocks: {{ section.blocks | json }}
</pre>

Check if setting has value:

{`%` if section.settings.heading != blank `%`}
  <h1>{{ section.settings.heading }}</h1>
{`%` else `%`}
  <p>DEBUG: No heading set (using default or blank)</p>
{`%` endif `%`}

Validate resource selectors:

{`%` assign product = block.settings.product `%`}

{`%` if product `%`}
  <p>Product found: {{ product.title }}</p>
{`%` else `%`}
  <p>DEBUG: No product selected in this block</p>
{`%` endif `%`}

Count and inspect blocks:

<p>DEBUG: This section has {{ section.blocks.size }} blocks</p>

{`%` if section.blocks.size > 0 `%`}
  {`%` for block in section.blocks `%`}
    <p>Block {{ forloop.index }}: Type = {{ block.type }}, ID = {{ block.id }}</p>
  {`%` endfor `%`}
{`%` else `%`}
  <p>DEBUG: No blocks added yet - add some in the theme editor!</p>
{`%` endif `%`}

Check template context:

{`%`- comment -`%`} Available in templates, not in sections {`%`- endcomment -`%`}
<p>Template: {{ template.name }}</p>
<p>Template suffix: {{ template.suffix }}</p>

Resource Selectors: The Complete Guide

Resource selectors are one of the most powerful features in Fluid's schema system. They allow users to pick specific items from your store (products, collections, categories, posts) and display them in sections. This section covers everything you need to know with real production examples.

When to Use Resource Selectors vs Global Loops

Use Resource Selectors when:

  • You want merchants to hand-pick specific items to feature
  • Order matters (e.g., "Staff Picks", "Best Sellers")
  • You need manual curation (e.g., seasonal promotions)
  • You want block-level control (each item can have unique overrides)

Use Global Loops when:

  • You want to show all items automatically (e.g., blog listing pages)
  • Content should update automatically when new items are added
  • You need pagination for large datasets
  • You're building main template pages (e.g., blog.liquid, shop.liquid)

Collection Resource Selector

The collection type lets users select a single collection.

This example is from a production theme showing how to build a dynamic collection showcase with auto-scroll carousel.

Complete Schema Definition

{
  "name": "Shop by Collections",
  "tag": "section",
  "class": "shop-by-collections-section",
  "settings": [
    {
      "type": "header",
      "content": "Section Header"
    },
    {
      "type": "text",
      "id": "heading",
      "label": "Heading Text",
      "default": "Ready to Find Your Perfect Routine?"
    },
    {
      "type": "text",
      "id": "shop_page_url",
      "label": "Shop Page URL",
      "default": "/shop",
      "info": "URL of the shop page where collection filters will be applied"
    },
    {
      "type": "range",
      "id": "card_height",
      "label": "Card Height (px)",
      "min": 300,
      "max": 600,
      "step": 20,
      "default": 400
    }
  ],
  "blocks": [
    {
      "type": "collection_item",
      "name": "Collection Item",
      "settings": [
        {
          "type": "collection",
          "id": "collection",
          "label": "Collection",
          "info": "Select a collection to display. The collection's title, image, and products will be used automatically."
        },
        {
          "type": "text",
          "id": "collection_title",
          "label": "Collection Title (Manual Override)",
          "info": "Only used if no collection is selected above."
        },
        {
          "type": "image_picker",
          "id": "background_image",
          "label": "Background Image (Override)",
          "info": "Override the collection's default image"
        },
        {
          "type": "url",
          "id": "collection_url",
          "label": "Collection URL (Manual Override)"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Shop by Collections",
      "blocks": [
        {
          "type": "collection_item"
        }
      ]
    }
  ]
}

Complete Liquid Implementation

<section class="shop-by-collections {{ section.settings.background_color }}">
  <div class="container">
    
    <!-- Section Header -->
    <div class="text-center mb-2xl">
      {`%` if section.settings.heading != blank `%`}
        <h2 class="text-3xl lg:text-5xl font-bold">
          {{ section.settings.heading }}
        </h2>
      {`%` endif `%`}
    </div>
    
    <!-- Collection Carousel -->
    {`%` if section.blocks.size > 0 `%`}
      <div class="collection-carousel">
        
        {`%`- comment -`%`} Iterate through blocks {`%`- endcomment -`%`}
        {`%` for block in section.blocks `%`}
          {`%` if block.type == 'collection_item' `%`}
            
            {`%`- comment -`%`} Step 1: Initialize variables {`%`- endcomment -`%`}
            {`%`- assign current_collection = blank -`%`}
            {`%`- assign collection_url = block.settings.collection_url | default: '#' -`%`}
            {`%`- assign collection_title = 'Collection' -`%`}
            {`%`- assign background_image = block.settings.background_image -`%`}
            
            {`%`- comment -`%`} Step 2: If collection is selected, find it from global collections {`%`- endcomment -`%`}
            {`%`- if block.settings.collection != blank -`%`}
              {`%`- assign collection_id = block.settings.collection -`%`}
              
              {`%`- comment -`%`} Find collection object from global collections array {`%`- endcomment -`%`}
              {`%`- for c in collections -`%`}
                {`%`- if c.id == collection_id -`%`}
                  {`%`- assign current_collection = c -`%`}
                  {`%`- break -`%`}
                {`%`- endif -`%`}
              {`%`- endfor -`%`}
              
              {`%`- comment -`%`} Step 3: Use collection data if found {`%`- endcomment -`%`}
              {`%`- if current_collection != blank -`%`}
                {`%`- comment -`%`} Build filtered shop URL {`%`- endcomment -`%`}
                {`%`- assign shop_url = section.settings.shop_page_url | default: '/shop' -`%`}
                {`%`- assign collection_url = shop_url | append: '?filterrific[by_collection][]=' | append: current_collection.id -`%`}
                
                {`%`- comment -`%`} Use collection title {`%`- endcomment -`%`}
                {`%`- assign collection_title = current_collection.title | default: 'Collection' -`%`}
                
                {`%`- comment -`%`} Use collection image if no manual override {`%`- endcomment -`%`}
                {`%`- if background_image == blank -`%`}
                  {`%`- if current_collection.image != blank -`%`}
                    {`%`- assign background_image = current_collection.image -`%`}
                  {`%`- elsif current_collection.products.first != blank -`%`}
                    {`%`- assign background_image = current_collection.products.first.images.first -`%`}
                  {`%`- endif -`%`}
                {`%`- endif -`%`}
              {`%`- endif -`%`}
            {`%`- endif -`%`}
            
            {`%`- comment -`%`} Step 4: Fallback to manual overrides if no collection {`%`- endcomment -`%`}
            {`%`- if current_collection == blank and block.settings.collection_title != blank -`%`}
              {`%`- assign collection_title = block.settings.collection_title -`%`}
            {`%`- endif -`%`}
            
            {`%`- comment -`%`} Step 5: Render the card {`%`- endcomment -`%`}
            <div class="collection-card-wrapper">
              <a href="{{ collection_url }}" 
                 class="collection-card"
                 style="height: {{ section.settings.card_height | default: 400 }}px;"
                 {{ block.fluid_attributes }}
                 {`%` if current_collection != blank `%`}data-collection-id="{{ current_collection.id }}"{`%` endif `%`}>
              
                <!-- Background Image -->
                <div class="collection-bg">
                  {`%` if background_image `%`}
                    <img src="{{ background_image | image_url: width: 800 }}" 
                         alt="{{ collection_title }}"
                         class="w-full h-full object-cover"
                         loading="lazy">
                  {`%` else `%`}
                    <img src="{{ 'placeholder-image.png' | asset_url }}" 
                         alt="Placeholder"
                         class="w-full h-full object-cover">
                  {`%` endif `%`}
                </div>
                
                <!-- Content -->
                <div class="collection-content">
                  <h3 class="collection-title">{{ collection_title }}</h3>
                </div>
                
              </a>
            </div>
          {`%` endif `%`}
        {`%` endfor `%`}
        
      </div>
    {`%` endif `%`}
    
  </div>
</section>

Key Techniques Explained

1. Finding Collection from ID

When a user selects a collection in the editor, Fluid stores the collection ID. You need to find the full collection object from the global collections array:

{`%`- assign collection_id = block.settings.collection -`%`}

{`%`- for c in collections -`%`}
  {`%`- if c.id == collection_id -`%`}
    {`%`- assign current_collection = c -`%`}
    {`%`- break -`%`}
  {`%`- endif -`%`}
{`%`- endfor -`%`}

2. Building Filtered Shop URLs

To link to a shop page filtered by collection, use the filterrific parameter:

{`%`- assign shop_url = '/shop' -`%`}
{`%`- assign collection_url = shop_url | append: '?filterrific[by_collection][]=' | append: current_collection.id -`%`}

This creates URLs like: /shop?filterrific[by_collection][]=123

3. Fallback Chain for Images

Provide multiple fallback options for images:

{`%`- if background_image == blank -`%`}
  {`%`- comment -`%`} Try collection image first {`%`- endcomment -`%`}
  {`%`- if current_collection.image != blank -`%`}
    {`%`- assign background_image = current_collection.image -`%`}
  {`%`- comment -`%`} Fall back to first product image {`%`- endcomment -`%`}
  {`%`- elsif current_collection.products.first != blank -`%`}
    {`%`- assign background_image = current_collection.products.first.images.first -`%`}
  {`%`- endif -`%`}
{`%`- endif -`%`}

4. Manual Overrides

Allow merchants to override collection data for marketing purposes:

{`%`- comment -`%`} Use manual title if no collection or as override {`%`- endcomment -`%`}
{`%`- if current_collection == blank and block.settings.collection_title != blank -`%`}
  {`%`- assign collection_title = block.settings.collection_title -`%`}
{`%`- endif -`%`}

Best Practices: Collection Selector

DO:

  • Always loop through collections to find the full object
  • Provide manual override fields for edge cases
  • Use {{ block.fluid_attributes }} for editor highlighting
  • Include fallback images (collection image → first product → placeholder)
  • Build filter URLs for shop integration
  • Add data-collection-id attributes for JavaScript targeting

DON'T:

  • Assume the collection object is directly available
  • Forget to handle blank collections
  • Hardcode shop URLs (use settings)
  • Skip the {%break%} in collection lookup loops
  • Forget to escape/validate user input in URLs

Product Resource Selector (Single)

The product type lets users select a single product. Perfect for blocks where each item represents one product with customization options.

This shows a production pattern for product recommendations with manual overrides, star ratings, and add-to-cart functionality.

Block Schema Definition

{
  "type": "product_card",
  "name": "Product Card",
  "limit": 6,
  "settings": [
    {
      "type": "header",
      "content": "Product Selection"
    },
    {
      "type": "product",
      "id": "product",
      "label": "Product"
    },
    {
      "type": "header",
      "content": "Manual Override (Optional)"
    },
    {
      "type": "text",
      "id": "title",
      "label": "Title Override",
      "info": "Leave blank to use product title"
    },
    {
      "type": "textarea",
      "id": "description",
      "label": "Description Override",
      "info": "Leave blank to use product description"
    },
    {
      "type": "image_picker",
      "id": "product_image",
      "label": "Image Override"
    },
    {
      "type": "url",
      "id": "product_url",
      "label": "URL Override"
    },
    {
      "type": "header",
      "content": "Rating & Reviews"
    },
    {
      "type": "range",
      "id": "star_rating",
      "label": "Star Rating",
      "min": 1.0,
      "max": 5.0,
      "step": 0.1,
      "default": 4.8
    },
    {
      "type": "number",
      "id": "review_count",
      "label": "Review Count",
      "default": 126
    }
  ]
}

Liquid Implementation

<div class="products-grid">
  {`%` for block in section.blocks `%`}
    {`%` if block.type == 'product_card' `%`}
      {`%`- comment -`%`} Get product and variant {`%`- endcomment -`%`}
      {`%` assign product = block.settings.product `%`}
      {`%` assign variant = product.selected_or_first_available_variant `%`}
      {`%` assign variant_id = variant.id | default: product.variants.first.id `%`}
      
      <div class="product-card" {{ block.fluid_attributes }}>
        
        <!-- Product Image -->
        <div class="product-image-wrapper">
          <a href="{{ product.url | default: block.settings.product_url | default: '#' }}">
            {`%` if product.image_url != blank `%`}
              <img 
                src="{{ product.image_url | image_url: width: 600 }}" 
                alt="{{ product.title | default: block.settings.title }}"
                loading="lazy">
            {`%` elsif block.settings.product_image != blank `%`}
              <img 
                src="{{ block.settings.product_image | image_url: width: 600 }}" 
                alt="{{ block.settings.title }}"
                loading="lazy">
            {`%` else `%`}
              <div class="placeholder-image">
                <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor">
                  <rect x="3" y="3" width="18" height="18" rx="2"/>
                  <circle cx="8.5" cy="8.5" r="1.5"/>
                  <polyline points="21,15 16,10 5,21"/>
                </svg>
              </div>
            {`%` endif `%`}
          </a>
        </div>

        <!-- Product Info -->
        <div class="product-info">
          
          <!-- Title with fallback -->
          <h3 class="product-title">
            <a href="{{ product.url | default: block.settings.product_url | default: '#' }}">
              {{ product.title | default: block.settings.title | default: 'Product Name' }}
            </a>
          </h3>

          <!-- Description with fallback -->
          {`%` if section.settings.show_description `%`}
            <p class="product-description">
              {`%` if block.settings.description != blank `%`}
                {{ block.settings.description }}
              {`%` elsif product.short_description != blank `%`}
                {{ product.short_description | truncate: 60 }}
              {`%` elsif product.description != blank `%`}
                {{ product.description | strip_html | truncate: 60 }}
              {`%` endif `%`}
            </p>
          {`%` endif `%`}

          <!-- Star Rating -->
          {`%` if section.settings.show_rating `%`}
            {`%` assign star_rating = block.settings.star_rating | default: 5.0 `%`}
            {`%` assign review_count = block.settings.review_count | default: 0 `%`}
            
            <div class="product-rating">
              <div class="star-container">
                {`%`- comment -`%`} Calculate star display {`%`- endcomment -`%`}
                {`%` assign full_stars = star_rating | floor `%`}
                {`%` assign decimal_part = star_rating | modulo: 1 `%`}
                {`%` assign has_half = false `%`}
                
                {`%` if decimal_part >= 0.25 and decimal_part < 0.75 `%`}
                  {`%` assign has_half = true `%`}
                {`%` elsif decimal_part >= 0.75 `%`}
                  {`%` assign full_stars = full_stars | plus: 1 `%`}
                {`%` endif `%`}
                
                {`%` assign empty_stars = 5 | minus: full_stars `%`}
                {`%` if has_half `%`}
                  {`%` assign empty_stars = empty_stars | minus: 1 `%`}
                {`%` endif `%`}
                
                {`%`- comment -`%`} Render stars {`%`- endcomment -`%`}
                {`%`- for i in (1..full_stars) -`%`}
                  <span class="star star-full">★</span>
                {`%`- endfor -`%`}
                
                {`%`- if has_half -`%`}
                  <span class="star star-half">
                    <span class="star-bg">★</span>
                    <span class="star-fill">★</span>
                  </span>
                {`%`- endif -`%`}
                
                {`%`- if empty_stars > 0 -`%`}
                  {`%`- for i in (1..empty_stars) -`%`}
                    <span class="star star-empty">★</span>
                  {`%`- endfor -`%`}
                {`%`- endif -`%`}
              </div>
              
              <span class="rating-text">
                ({{ star_rating }} stars) • {{ review_count }} reviews
              </span>
            </div>
          {`%` endif `%`}

          <!-- Add to Cart Button -->
          {`%` if section.settings.show_add_to_cart `%`}
            <button 
              type="button"
              class="add-to-cart-btn"
              data-fluid-add-to-cart="{{ variant_id }}"
              data-fluid-quantity="1"
              {`%` if variant_id == blank `%`}disabled{`%` endif `%`}>
              {{ section.settings.button_text | default: 'ADD TO CART' }}
            </button>
          {`%` endif `%`}
          
        </div>
      </div>
    {`%` endif `%`}
  {`%` endfor `%`}
</div>

Key Techniques Explained

1. Product Variant Handling

Always get the correct variant ID for add-to-cart functionality:

{`%` assign product = block.settings.product `%`}
{`%` assign variant = product.selected_or_first_available_variant `%`}
{`%` assign variant_id = variant.id | default: product.variants.first.id `%`}

2. Manual Override Pattern

Use the default filter to prioritize block settings over product data:

{{ product.title | default: block.settings.title | default: 'Product Name' }}

This creates a fallback chain: block override → product data → hardcoded default

3. Star Rating Calculation

Calculate full, half, and empty stars with Liquid math:

{`%` assign full_stars = star_rating | floor `%`}
{`%` assign decimal_part = star_rating | modulo: 1 `%`}
{`%` assign has_half = false `%`}

{`%` if decimal_part >= 0.25 and decimal_part < 0.75 `%`}
  {`%` assign has_half = true `%`}
{`%` elsif decimal_part >= 0.75 `%`}
  {`%` assign full_stars = full_stars | plus: 1 `%`}
{`%` endif `%`}

4. Direct Add-to-Cart Integration

Use Fluid's cart attributes for direct add-to-cart:

<button 
  type="button"
  data-fluid-add-to-cart="{{ variant_id }}"
  data-fluid-quantity="1"
  {`%` if variant_id == blank `%`}disabled{`%` endif `%`}>
  ADD TO CART
</button>

Best Practices: Product Selector

DO:

  • Always get the variant ID, not just the product
  • Provide manual override fields for marketing flexibility
  • Use multi-level fallbacks for images and descriptions
  • Calculate star ratings server-side with Liquid
  • Disable buttons when variant is unavailable
  • Truncate descriptions to prevent layout breaks

DON'T:

  • Assume products always have variants
  • Forget to handle missing images
  • Hardcode button text (use settings)
  • Skip accessibility attributes on images
  • Use client-side JavaScript for simple star calculations
  • Display raw HTML in descriptions (use | strip_html)

Multiple Resource Selectors: Lists

Multiple resource selectors (product_list, collection_list, category_list, posts_list) allow users to select several items at once. These are perfect for "Featured Products" carousels, "Shop by Collection" sections, or curated content grids.

Key Difference: Single vs Multiple

Selector TypeUse CaseAccess Pattern
Single (product, collection, category, posts)Select ONE item, often in blocksNeed to find from global array
Multiple (product_list, collection_list, etc.)Select MULTIPLE items at onceDirect iteration

Product List Selector

The product_list type lets users select multiple products in one setting. Perfect for "Featured Products" or "Best Sellers" sections.

Schema Definition

{
  "name": "Featured Products Carousel",
  "tag": "section",
  "settings": [
    {
      "type": "header",
      "content": "Products Selection"
    },
    {
      "type": "text",
      "id": "heading",
      "label": "Section Heading",
      "default": "Featured Products"
    },
    {
      "type": "product_list",
      "id": "featured_products",
      "label": "Select Products",
      "limit": 12,
      "info": "Choose up to 12 products to feature. Drag to reorder."
    },
    {
      "type": "range",
      "id": "products_per_row",
      "label": "Products Per Row",
      "min": 2,
      "max": 4,
      "step": 1,
      "default": 3
    },
    {
      "type": "checkbox",
      "id": "show_add_to_cart",
      "label": "Show Add to Cart Button",
      "default": true
    }
  ],
  "presets": [
    {
      "name": "Featured Products",
      "settings": {
        "heading": "Staff Picks"
      }
    }
  ]
}

Liquid Implementation

<section class="featured-products {{ section.settings.background_color }}">
  <div class="container">
    
    <!-- Section Heading -->
    {`%` if section.settings.heading != blank `%`}
      <h2 class="section-heading">{{ section.settings.heading }}</h2>
    {`%` endif `%`}
    
    {`%`- comment -`%`} Check if products were selected {`%`- endcomment -`%`}
    {`%` if section.settings.featured_products.size > 0 `%`}
      
      <div class="products-grid" style="--columns: {{ section.settings.products_per_row }};">
        
        {`%`- comment -`%`} Direct iteration - no need to find from global array {`%`- endcomment -`%`}
        {`%` for product in section.settings.featured_products `%`}
          
          <div class="product-card">
            
            <!-- Product Image -->
            <a href="{{ product.url }}" class="product-image-link">
              {`%` if product.image_url != blank `%`}
                <img 
                  src="{{ product.image_url | image_url: width: 600 }}" 
                  alt="{{ product.title }}"
                  loading="lazy">
              {`%` else `%`}
                <div class="placeholder-image">No image</div>
              {`%` endif `%`}
            </a>
            
            <!-- Product Info -->
            <div class="product-info">
              <h3 class="product-title">
                <a href="{{ product.url }}">{{ product.title }}</a>
              </h3>
              
              <p class="product-price">{{ product.price | money }}</p>
              
              <!-- Add to Cart -->
              {`%` if section.settings.show_add_to_cart `%`}
                {`%` assign variant = product.selected_or_first_available_variant `%`}
                {`%` assign variant_id = variant.id | default: product.variants.first.id `%`}
                
                <button 
                  type="button"
                  class="btn-add-to-cart"
                  data-fluid-add-to-cart="{{ variant_id }}"
                  data-fluid-quantity="1"
                  {`%` if variant_id == blank `%`}disabled{`%` endif `%`}>
                  Add to Cart
                </button>
              {`%` endif `%`}
            </div>
            
          </div>
          
        {`%` endfor `%`}
      </div>
      
    {`%` else `%`}
      
      <!-- Empty State -->
      <div class="empty-state">
        <p>No products selected. Please select products in the theme editor.</p>
      </div>
      
    {`%` endif `%`}
    
  </div>
</section>

Key Points: product_list

  1. Direct Access - Products are already full objects, no need to find them
  2. Check Size - Use section.settings.featured_products.size to check if any selected
  3. Maintain Order - Products appear in the order the merchant arranged them
  4. Set Limits - Use "limit" to prevent performance issues (recommended: 8-12)

Collection List Selector

The collection_list (or collections_list) type lets users select multiple collections.

Schema Definition

{
  "name": "Shop by Collections",
  "tag": "section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Shop by Collection"
    },
    {
      "type": "collections_list",
      "id": "featured_collections",
      "label": "Select Collections",
      "limit": 6,
      "info": "Choose up to 6 collections to display"
    },
    {
      "type": "text",
      "id": "shop_page_url",
      "label": "Shop Page URL",
      "default": "/shop"
    }
  ]
}

Liquid Implementation

<section class="shop-by-collections">
  <div class="container">
    
    <h2>{{ section.settings.heading }}</h2>
    
    {`%` if section.settings.featured_collections.size > 0 `%`}
      
      <div class="collections-grid">
        
        {`%`- comment -`%`} Direct iteration over selected collections {`%`- endcomment -`%`}
        {`%` for collection in section.settings.featured_collections `%`}
          
          <div class="collection-card">
            
            {`%`- comment -`%`} Build filtered shop URL {`%`- endcomment -`%`}
            {`%` assign shop_url = section.settings.shop_page_url | default: '/shop' `%`}
            {`%` assign collection_url = shop_url | append: '?filterrific[by_collection][]=' | append: collection.id `%`}
            
            <a href="{{ collection_url }}" class="collection-link">
              
              <!-- Collection Image -->
              {`%` if collection.image_url != blank `%`}
                <img 
                  src="{{ collection.image_url | image_url: width: 800 }}" 
                  alt="{{ collection.title }}"
                  loading="lazy">
              {`%` elsif collection.products.first.image_url != blank `%`}
                {`%`- comment -`%`} Fallback to first product image {`%`- endcomment -`%`}
                <img 
                  src="{{ collection.products.first.image_url | image_url: width: 800 }}" 
                  alt="{{ collection.title }}"
                  loading="lazy">
              {`%` else `%`}
                <div class="placeholder-image">{{ collection.title }}</div>
              {`%` endif `%`}
              
              <!-- Collection Info -->
              <div class="collection-info">
                <h3 class="collection-title">{{ collection.title }}</h3>
                {`%` if collection.products_count `%`}
                  <p class="products-count">{{ collection.products_count }} products</p>
                {`%` endif `%`}
              </div>
              
            </a>
          </div>
          
        {`%` endfor `%`}
      </div>
      
    {`%` else `%`}
      <p class="empty-state">No collections selected.</p>
    {`%` endif `%`}
    
  </div>
</section>

Key Points: collection_list

  1. Direct Access - Collections are full objects with all fields
  2. Build Filter URLs - Use ?filterrific[by_collection][]= for shop page links
  3. Image Fallback - Collection image → first product image → placeholder
  4. Products Count - Access collection.products_count for count display

Category List Selector

The category_list (or categories_list) type lets users select multiple categories.

Schema Definition

{
  "name": "Shop by Categories",
  "tag": "section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Browse Categories"
    },
    {
      "type": "categories_list",
      "id": "featured_categories",
      "label": "Select Categories",
      "limit": 8,
      "info": "Choose up to 8 categories to feature"
    },
    {
      "type": "select",
      "id": "layout",
      "label": "Layout Style",
      "options": [
        { "value": "grid", "label": "Grid" },
        { "value": "carousel", "label": "Carousel" }
      ],
      "default": "grid"
    }
  ]
}

Liquid Implementation

<section class="shop-by-categories">
  <div class="container">
    
    <h2>{{ section.settings.heading }}</h2>
    
    {`%` if section.settings.featured_categories.size > 0 `%`}
      
      <div class="categories-{{ section.settings.layout }}">
        
        {`%`- comment -`%`} Direct iteration over selected categories {`%`- endcomment -`%`}
        {`%` for category in section.settings.featured_categories `%`}
          
          <div class="category-card">
            
            {`%`- comment -`%`} Build category shop URL {`%`- endcomment -`%`}
            {`%` assign category_url = '/shop?filterrific[with_category_id][]=' | append: category.id `%`}
            
            <a href="{{ category_url }}" class="category-link">
              
              <!-- Category Image -->
              {`%` if category.image_url != blank `%`}
                <div class="category-image">
                  <img 
                    src="{{ category.image_url | image_url: width: 600 }}" 
                    alt="{{ category.title }}"
                    loading="lazy">
                </div>
              {`%` endif `%`}
              
              <!-- Category Info -->
              <div class="category-info">
                <h3 class="category-title">{{ category.title }}</h3>
                
                {`%` if category.description != blank `%`}
                  <p class="category-description">
                    {{ category.description | strip_html | truncate: 80 }}
                  </p>
                {`%` endif `%`}
              </div>
              
            </a>
          </div>
          
        {`%` endfor `%`}
      </div>
      
    {`%` else `%`}
      <p class="empty-state">No categories selected.</p>
    {`%` endif `%`}
    
  </div>
</section>

Key Points: category_list

  1. Direct Access - Categories are full objects
  2. Filter URLs - Use ?filterrific[with_category_id][]= for shop links
  3. Description - Categories can have descriptions (strip HTML and truncate)
  4. Products Access - Can access category.products if needed

Posts List Selector

The posts_list type lets users select multiple blog posts. Perfect for "Related Posts" or "Featured Articles" sections.

Schema Definition

{
  "name": "Featured Blog Posts",
  "tag": "section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Section Heading",
      "default": "Featured Articles"
    },
    {
      "type": "posts_list",
      "id": "featured_posts",
      "label": "Select Posts",
      "limit": 6,
      "info": "Choose up to 6 blog posts to feature"
    },
    {
      "type": "checkbox",
      "id": "show_excerpt",
      "label": "Show Post Excerpt",
      "default": true
    },
    {
      "type": "checkbox",
      "id": "show_date",
      "label": "Show Post Date",
      "default": true
    },
    {
      "type": "select",
      "id": "columns",
      "label": "Columns",
      "options": [
        { "value": "2", "label": "2 Columns" },
        { "value": "3", "label": "3 Columns" }
      ],
      "default": "3"
    }
  ]
}

Liquid Implementation

<section class="featured-posts">
  <div class="container">
    
    <h2>{{ section.settings.heading }}</h2>
    
    {`%` if section.settings.featured_posts.size > 0 `%`}
      
      <div class="posts-grid" style="--columns: {{ section.settings.columns }};">
        
        {`%`- comment -`%`} Direct iteration over selected posts {`%`- endcomment -`%`}
        {`%` for post in section.settings.featured_posts `%`}
          
          <article class="post-card">
            
            <a href="{{ post.preview_url }}" class="post-link">
              
              <!-- Post Image -->
              {`%` assign image_url = '' `%`}
              {`%` if post.image_url `%`}
                {`%` assign image_url = post.image_url `%`}
              {`%` elsif post.image `%`}
                {`%` assign image_url = post.image | image_url `%`}
              {`%` elsif post.images.size > 0 `%`}
                {`%` assign image_url = post.images[0].src `%`}
              {`%` endif `%`}
              
              {`%` if image_url != '' `%`}
                <div class="post-image">
                  <img 
                    src="{{ image_url | image_url: width: 600 }}" 
                    alt="{{ post.title }}"
                    loading="lazy">
                </div>
              {`%` endif `%`}
              
              <!-- Post Content -->
              <div class="post-content">
                
                <!-- Category Badge -->
                {`%` if post.category `%`}
                  <span class="post-category">{{ post.category.title }}</span>
                {`%` endif `%`}
                
                <!-- Post Title -->
                <h3 class="post-title">{{ post.title }}</h3>
                
                <!-- Post Meta -->
                {`%` if section.settings.show_date `%`}
                  {`%` assign display_date = post.post_date | default: post.created_at `%`}
                  <time datetime="{{ display_date | date: '%Y-%m-%d' }}" class="post-date">
                    {{ display_date | date: '%B %d, %Y' }}
                  </time>
                {`%` endif `%`}
                
                <!-- Post Excerpt -->
                {`%` if section.settings.show_excerpt `%`}
                  <p class="post-excerpt">
                    {`%` if post.summary != blank `%`}
                      {{ post.summary | strip_html | truncate: 120 }}
                    {`%` elsif post.description != blank `%`}
                      {{ post.description | strip_html | truncate: 120 }}
                    {`%` endif `%`}
                  </p>
                {`%` endif `%`}
                
                <!-- Read More Link -->
                <span class="read-more">Read More →</span>
                
              </div>
            </a>
            
          </article>
          
        {`%` endfor `%`}
      </div>
      
    {`%` else `%`}
      <p class="empty-state">No posts selected.</p>
    {`%` endif `%`}
    
  </div>
</section>

Key Points: posts_list

  1. Direct Access - Posts are full objects with all fields
  2. Image Fallback - Check post.image_url, post.image, then post.images array
  3. Date Handling - Use post.post_date with fallback to post.created_at
  4. HTML Content - Use | strip_html for excerpts, | unescape for full content
  5. Category Access - Posts have post.category object

Comparison Table: Single vs Multiple Selectors

FeatureSingle SelectorMultiple Selector (List)
Schema Typeproduct, collection, category, postproduct_list, collection_list, category_list, posts_list
SelectionOne itemMultiple items (with limit)
Typical UseWithin blocksDirect in section settings
Access PatternNeed to find from global arrayDirect iteration
Order ControlN/A (single item)User can drag to reorder
Best ForCustom blocks with overridesSimple featured grids/carousels

When to Use Which?

Use Single Selector (in blocks) when:

  • Need per-item customization (override title, image, description)
  • Want different block types mixed together
  • Need flexibility in layout/styling per item
  • Example: "Also Bought" with custom messaging

Use Multiple Selector (list) when:

  • Just need to display selected items
  • All items have same styling/layout
  • Simpler configuration needed
  • Example: "Featured Products Carousel"

Combined Example: Products with Fallback

Sometimes you want to use a product list, but allow fallback to all products if nothing selected:

<section class="products-section">
  <div class="container">
    
    {`%`- comment -`%`} Use selected products if available, otherwise show all products {`%`- endcomment -`%`}
    {`%` if section.settings.featured_products.size > 0 `%`}
      {`%` assign products_to_show = section.settings.featured_products `%`}
    {`%` else `%`}
      {`%` assign products_to_show = products `%`}
    {`%` endif `%`}
    
    <div class="products-grid">
      {`%` for product in products_to_show limit: 8 `%`}
        <div class="product-card">
          <a href="{{ product.url }}">
            <img src="{{ product.image_url | image_url: width: 400 }}" alt="{{ product.title }}">
            <h3>{{ product.title }}</h3>
            <p>{{ product.price | money }}</p>
          </a>
        </div>
      {`%` endfor `%`}
    </div>
    
  </div>
</section>

Best Practices: Multiple Resource Selectors

DO:

  • Always check .size > 0 before looping
  • Set reasonable limits (8-12 for products, 4-8 for collections)
  • Provide empty state messaging
  • Use loading="lazy" on images
  • Build proper filter URLs for shop links
  • Handle missing images with fallbacks

DON'T:

  • Allow unlimited selections (causes performance issues)
  • Forget to check if list is empty
  • Assume all items have images
  • Hardcode shop URLs (use settings)
  • Skip the limit parameter in schema
  • Forget to optimize image sizes with | image_url: width:

Global Loops with Pagination

Global loops are used on main template pages (like post_page, shop_page) to display all items of a type automatically. Unlike resource selectors where merchants hand-pick items, global loops show everything and update automatically when new content is added.

When to Use Global Loops

Perfect for:

  • Blog listing pages (blog_page.liquid)
  • Shop/collection pages (collection, shop_page)
  • Large datasets that need pagination
  • Content that should auto-update

Not suitable for:

  • Curated "featured" sections
  • Hand-picked recommendations
  • Content that needs specific ordering

Blog Post Lists: The Global Loop Pattern

Example shows how to build a paginated blog listing page that automatically displays all posts.

Complete Implementation

Schema Definition

{
  "name": "Blog List",
  "tag": "section",
  "enabled_on": {
    "templates": ["blog"]
  },
  "settings": [
    {
      "type": "header",
      "content": "Section Title"
    },
    {
      "type": "text",
      "id": "title",
      "label": "Title",
      "default": "Blog Posts"
    },
    {
      "type": "select",
      "id": "title_font_size",
      "label": "Font Size (Mobile)",
      "options": [
        { "value": "text-2xl", "label": "2XL" },
        { "value": "text-3xl", "label": "3XL" },
        { "value": "text-4xl", "label": "4XL" }
      ],
      "default": "text-2xl"
    },
    {
      "type": "header",
      "content": "Post Card Settings"
    },
    {
      "type": "checkbox",
      "id": "show_post_description",
      "label": "Show Post Description",
      "default": true
    },
    {
      "type": "select",
      "id": "post_card_background",
      "label": "Post Card Background Color",
      "options": [
        { "value": "", "label": "None (Transparent)" },
        { "value": "bg-white", "label": "White" },
        { "value": "bg-neutral-light", "label": "Neutral Light" }
      ],
      "default": "bg-neutral-light"
    },
    {
      "type": "text",
      "id": "empty_state_text",
      "label": "Empty state text",
      "default": "No posts found"
    }
  ]
}

Liquid Implementation

<section class="blog-posts {{ section.settings.background_color }} {{ section.settings.section_padding_y_mobile }} {{ section.settings.section_padding_y_desktop }}">
  <div class="container">
    
    <!-- Section Title -->
    {`%` if section.settings.title != blank `%`}
      <div class="title-section mb-lg">
        <h3 class="{{ section.settings.title_font_family }} {{ section.settings.title_font_size }} {{ section.settings.title_font_size_desktop }} {{ section.settings.title_font_weight }} {{ section.settings.title_color }}">
          {{ section.settings.title }}
        </h3>
      </div>
    {`%` endif `%`}
    
    {`%`- comment -`%`} 
      CRITICAL: Wrap the entire section in {`%` paginate `%`}
      This provides the posts collection and pagination controls
    {`%`- endcomment -`%`}
    {`%`- paginate posts by 10 -`%`}
    
      <div class="post-list-section">
        
        {`%` if posts.size > 0 `%`}
          
          <!-- Posts Grid -->
          <div class="post-list grid grid-cols-1 gap-lg mt-xl">
            
            {`%`- comment -`%`} Loop through posts provided by paginate {`%`- endcomment -`%`}
            {`%` for post in posts `%`}
              
              <a href="{{ post.preview_url }}" class="blog-card {{ section.settings.post_card_background }} {{ section.settings.card_border_radius }}">
                
                <!-- Post Image -->
                <div class="post-image-link">
                  <div class="post-image {{ section.settings.image_border_radius }}">
                    
                    {`%`- comment -`%`} Image fallback chain {`%`- endcomment -`%`}
                    {`%` assign image_url = '' `%`}
                    {`%` if post.image_url `%`}
                      {`%` assign image_url = post.image_url `%`}
                    {`%` elsif post.image `%`}
                      {`%` assign image_url = post.image | image_url `%`}
                    {`%` elsif post.images.size > 0 `%`}
                      {`%` assign image_url = post.images[0].src `%`}
                    {`%` endif `%`}
                    
                    {`%` if image_url != '' `%`}
                      <img 
                        src="{{ image_url }}"
                        alt="{{ post.title }}"
                        class="image-cover"
                        loading="lazy" />
                    {`%` else `%`}
                      <div class="h-full w-full flex items-center justify-center bg-gray-100">
                        <span class="text-gray-400">No image available</span>
                      </div>
                    {`%` endif `%`}
                    
                  </div>
                </div>
                
                <!-- Post Content -->
                <div class="desc px-md py-md">
                  
                  <!-- Post Title -->
                  <div class="post-title {{ section.settings.post_title_font_family }} {{ section.settings.post_title_font_size }} {{ section.settings.post_title_font_size_desktop }} {{ section.settings.post_title_font_weight }} {{ section.settings.post_title_color }} line-clamp-2">
                    {{ post.title }}
                  </div>
                  
                  <!-- Post Description -->
                  {`%` if section.settings.show_post_description `%`}
                    <div class="post-desc {{ section.settings.post_desc_font_size }} {{ section.settings.post_desc_font_size_desktop }} {{ section.settings.post_desc_font_weight }} {{ section.settings.post_desc_color }} line-clamp-2 mt-sm">
                      {`%` if post.summary `%`}
                        {{ post.summary | unescape }}
                      {`%` elsif post.description `%`}
                        {{ post.description | unescape }}
                      {`%` endif `%`}
                    </div>
                  {`%` endif `%`}
                  
                </div>
              </a>
              
            {`%` endfor `%`}
          </div>
          
        {`%` else `%`}
          
          <!-- Empty State -->
          <div class="text-center py-2xl">
            <p class="text-gray-500">{{ section.settings.empty_state_text | default: 'No posts found' }}</p>
          </div>
          
        {`%` endif `%`}
      </div>
      
      {`%`- comment -`%`} Pagination Controls {`%`- endcomment -`%`}
      {`%`- if paginate.pages > 1 -`%`}
        <div class="mt-12">
          {`%` render 'pagination', paginate: paginate, anchor: '' `%`}
        </div>
      {`%`- endif -`%`}
      
    {`%` endpaginate `%`}
    
  </div>
</section>

Key Concepts Explained

1. The Paginate Tag

The {%paginate%} tag is required for global loops. It:

  • Provides the posts collection
  • Handles page numbers automatically
  • Creates the paginate object for controls
{`%`- paginate posts by 10 -`%`}
  {`%`- comment -`%`} posts collection is now available {`%`- endcomment -`%`}
  {`%` for post in posts `%`}
    ...
  {`%` endfor `%`}
  
  {`%`- comment -`%`} paginate object for controls {`%`- endcomment -`%`}
  {`%` if paginate.pages > 1 `%`}
    {`%` render 'pagination', paginate: paginate `%`}
  {`%` endif `%`}
{`%` endpaginate `%`}

Important: Everything that needs access to posts or paginate must be inside the {%paginate%} tags.

2. Post Object Fields

When looping through posts, you have access to these fields:

FieldTypeDescriptionExample
post.titleStringPost title"5 Tips for Better Sleep"
post.preview_urlStringLink to post detail page"/blog/5-tips-for-better-sleep"
post.image_urlStringFeatured image URLDirect image URL
post.imageObjectFeatured image objectUse with `
post.imagesArrayAll post imagespost.images[0].src
post.summaryStringShort description/excerptMay contain HTML
post.descriptionStringFull post contentMay contain HTML
post.post_dateDatePublication dateUse with `
post.post_authorStringAuthor name"John Doe"
post.categoryObjectPost categorypost.category.title
post.collectionsArrayAssociated collectionsFor tags/categories
post.created_atDateCreation timestampUse with `
post.updated_atDateLast update timestampUse with `

3. Image Fallback Pattern

Always provide multiple fallback options for images:

{`%` assign image_url = '' `%`}

{`%`- comment -`%`} Try image_url first (direct URL) {`%`- endcomment -`%`}
{`%` if post.image_url `%`}
  {`%` assign image_url = post.image_url `%`}
  
{`%`- comment -`%`} Try image object (needs filter) {`%`- endcomment -`%`}
{`%` elsif post.image `%`}
  {`%` assign image_url = post.image | image_url `%`}
  
{`%`- comment -`%`} Try images array {`%`- endcomment -`%`}
{`%` elsif post.images.size > 0 `%`}
  {`%` assign image_url = post.images[0].src `%`}
{`%` endif `%`}

{`%`- comment -`%`} Render image or placeholder {`%`- endcomment -`%`}
{`%` if image_url != '' `%`}
  <img src="{{ image_url }}" alt="{{ post.title }}" loading="lazy">
{`%` else `%`}
  <div class="placeholder">No image available</div>
{`%` endif `%`}

4. HTML Content Handling

Post summaries and descriptions often contain HTML. Use the unescape filter:

{`%` if post.summary `%`}
  {{ post.summary | unescape }}
{`%` elsif post.description `%`}
  {{ post.description | unescape }}
{`%` endif `%`}

For plain text previews, strip HTML and truncate:

{{ post.description | strip_html | truncate: 150 }}

5. Pagination Snippet

Create a reusable pagination.liquid snippet:

{`%`- comment -% components/pagination.liquid {`%`- endcomment -`%`}
{`%` if paginate.pages > 1 `%`}
  <nav class="pagination" role="navigation">
    
    {`%`- if paginate.previous -`%`}
      <a href="{{ paginate.previous.url }}" class="pagination-prev">
        ← Previous
      </a>
    {`%`- else -`%`}
      <span class="pagination-prev disabled">← Previous</span>
    {`%`- endif -`%`}
    
    <span class="pagination-info">
      Page {{ paginate.current_page }} of {{ paginate.pages }}
    </span>
    
    {`%`- if paginate.next -`%`}
      <a href="{{ paginate.next.url }}" class="pagination-next">
        Next →
      </a>
    {`%`- else -`%`}
      <span class="pagination-next disabled">Next →</span>
    {`%`- endif -`%`}
    
  </nav>
{`%` endif `%`}

Use it in your template:

{`%` if paginate.pages > 1 `%`}
  {`%` render 'pagination', paginate: paginate, anchor: '' `%`}
{`%` endif `%`}

Best Practices: Global Loops

DO:

  • Always wrap content in {%paginate%}
  • Use appropriate page sizes (10-12 for blogs, 24-48 for products)
  • Provide empty state messaging
  • Include pagination controls when needed
  • Use loading="lazy" on images
  • Strip HTML from descriptions when needed
  • Check for posts.size > 0 before rendering
  • Add empty state text as a setting

DON'T:

  • Forget to close {%endpaginate%}
  • Access posts outside paginate tags
  • Use huge page sizes (causes performance issues)
  • Forget fallbacks for missing images
  • Display raw HTML without | unescape
  • Hardcode empty state messages
  • Skip the pagination controls
  • Use {%paginate%} for manually curated content

Post Details Page: Single Resource Pattern

For post detail pages, you work with a single post object rather than a collection. This is similar to product detail pages.

This shows how to display a full blog post with all its metadata.

Key Liquid Implementation

<section class="post-details">
  <div class="container">
    
    {`%` if post `%`}
      <article class="post-wrapper">
        
        <!-- Post Header -->
        <div class="post-header mb-xl">
          
          <!-- Category Badge -->
          {`%` if section.settings.show_category and post.category `%`}
            <div class="post-category mb-md">
              <a href="{{ post.category.preview_url | default: '#' }}" class="category-badge {{ section.settings.category_badge_background }} {{ section.settings.category_badge_border_radius }}">
                {{ post.category.title }}
              </a>
            </div>
          {`%` endif `%`}
          
          <!-- Post Title -->
          {`%` if section.settings.show_title `%`}
            <h1 class="post-title {{ section.settings.title_font_family }} {{ section.settings.title_font_size }} {{ section.settings.title_font_weight }} {{ section.settings.title_color }} mb-md">
              {{ post.title }}
            </h1>
          {`%` endif `%`}
          
          <!-- Post Meta (Author, Date) -->
          {`%` if section.settings.show_meta `%`}
            <div class="post-meta flex flex-wrap items-center gap-md {{ section.settings.meta_font_size }} {{ section.settings.meta_color }}">
              
              {`%` if section.settings.show_author and post.post_author `%`}
                <div class="post-author flex items-center gap-sm">
                  {`%` if section.settings.show_author_icon `%`}
                    <svg class="author-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
                    </svg>
                  {`%` endif `%`}
                  <span class="author-name">{{ post.post_author }}</span>
                </div>
              {`%` endif `%`}
              
              {`%`- comment -`%`} Date with fallback {`%`- endcomment -`%`}
              {`%` assign display_date = post.post_date `%`}
              {`%` if display_date == blank or display_date == null `%`}
                {`%` assign display_date = post.created_at `%`}
              {`%` endif `%`}
              
              {`%` if section.settings.show_date and display_date `%`}
                <div class="post-date flex items-center gap-sm">
                  {`%` if section.settings.show_date_icon `%`}
                    <svg class="date-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
                    </svg>
                  {`%` endif `%`}
                  <time datetime="{{ display_date | date: '%Y-%m-%d' }}" class="date-text">
                    {{ display_date | date: section.settings.date_format | default: '%B %d, %Y' }}
                  </time>
                </div>
              {`%` endif `%`}
              
            </div>
          {`%` endif `%`}
        </div>
        
        <!-- Featured Image -->
        {`%` if section.settings.show_featured_image `%`}
          {`%` assign image_url = '' `%`}
          {`%` if post.image_url `%`}
            {`%` assign image_url = post.image_url `%`}
          {`%` elsif post.image `%`}
            {`%` assign image_url = post.image | image_url `%`}
          {`%` elsif post.images.size > 0 `%`}
            {`%` assign image_url = post.images[0].src `%`}
          {`%` endif `%`}
          
          {`%` if image_url != '' `%`}
            <div class="post-featured-image-wrapper mb-xl">
              <div class="post-featured-image {{ section.settings.featured_image_border_radius }} overflow-hidden">
                <img 
                  src="{{ image_url }}"
                  alt="{{ post.title }}"
                  loading="lazy"
                  class="featured-image" />
              </div>
            </div>
          {`%` endif `%`}
        {`%` endif `%`}
        
        <!-- Post Summary -->
        {`%` if section.settings.show_summary and post.summary `%`}
          <div class="post-summary mb-xl">
            <div class="summary-card {{ section.settings.summary_background }} {{ section.settings.summary_padding }} {{ section.settings.summary_border_radius }}">
              <div class="summary-content {{ section.settings.summary_font_size }} {{ section.settings.summary_font_weight }} {{ section.settings.summary_color }}">
                {{ post.summary | unescape }}
              </div>
            </div>
          </div>
        {`%` endif `%`}
        
        <!-- Post Content -->
        {`%` if section.settings.show_description and post.description `%`}
          <div class="post-content mb-xl">
            <div class="content-wrapper {{ section.settings.description_font_size }} {{ section.settings.description_font_weight }} {{ section.settings.description_color }} trix-content">
              {{ post.description | unescape }}
            </div>
          </div>
        {`%` endif `%`}
        
        <!-- Post Collections (Tags) -->
        {`%` if section.settings.show_collections and post.collections.size > 0 `%`}
          <div class="post-collections mb-xl">
            {`%` if section.settings.collections_title `%`}
              <h3 class="collections-title {{ section.settings.collections_title_font_size }} {{ section.settings.collections_title_font_weight }} {{ section.settings.collections_title_color }} mb-md">
                {{ section.settings.collections_title }}
              </h3>
            {`%` endif `%`}
            <div class="collections-list flex flex-wrap gap-sm">
              {`%` for collection in post.collections `%`}
                <a href="{{ collection.preview_url | default: '#' }}" class="collection-badge {{ section.settings.collection_badge_background }} {{ section.settings.collection_badge_border_radius }} {{ section.settings.collection_font_size }} {{ section.settings.collection_font_weight }} {{ section.settings.collection_color }} {{ section.settings.collection_padding }}">
                  {{ collection.title }}
                </a>
              {`%` endfor `%`}
            </div>
          </div>
        {`%` endif `%`}
        
      </article>
      
    {`%` else `%`}
      
      <!-- Post Not Found -->
      <div class="text-center py-2xl">
        <p class="text-gray-500">{{ section.settings.empty_state_text | default: 'Post not found' }}</p>
      </div>
      
    {`%` endif `%`}
    
  </div>
</section>

Key Techniques for Post Details

1. Date Handling with Fallback

Posts may have post_date or created_at. Always provide a fallback:

{`%` assign display_date = post.post_date `%`}
{`%` if display_date == blank or display_date == null `%`}
  {`%` assign display_date = post.created_at `%`}
{`%` endif `%`}

{`%` if display_date `%`}
  <time datetime="{{ display_date | date: '%Y-%m-%d' }}">
    {{ display_date | date: '%B %d, %Y' }}
  </time>
{`%` endif `%`}

2. Rich Content Rendering

Post content is stored as HTML. Use the trix-content class for proper styling and unescape:

<div class="trix-content">
  {{ post.description | unescape }}
</div>

The trix-content class should style all HTML elements (headings, lists, links, images, etc.) that might appear in the rich text editor output.

3. Collections as Tags

Post collections work like tags or categories:

{`%` if post.collections.size > 0 `%`}
  <div class="tags">
    {`%` for collection in post.collections `%`}
      <a href="{{ collection.preview_url }}" class="tag">
        {{ collection.title }}
      </a>
    {`%` endfor `%`}
  </div>
{`%` endif `%`}

Best Practices: Post Details

DO:

  • Check if post exists before rendering
  • Provide date fallbacks (post_date → created_at)
  • Use semantic HTML (<article>, <time>, etc.)
  • Style rich content with trix-content class
  • Use | unescape for HTML content
  • Provide empty state messaging
  • Use proper datetime attributes on <time> tags

DON'T:

  • Assume post always exists
  • Display raw dates without formatting
  • Forget to | unescape HTML content
  • Skip checks for empty collections
  • Hardcode labels (use settings)
  • Forget alt text on images
  • Skip the empty state handler

Complete Resource Reference

This section documents all available resource types and their fields.

Product Fields Reference

When working with product objects from any source (selectors, loops, or blocks):

FieldTypeDescriptionUsage Example
product.idNumberUnique product identifier{{ product.id }}
product.titleStringProduct name{{ product.title }}
product.urlStringLink to product detail page<a href="{{ product.url }}">
product.priceNumberCurrent price in cents`{{ product.price
product.compare_at_priceNumberOriginal price (for sales)`{{ product.compare_at_price
product.image_urlStringPrimary product image URL (direct)<img src="{{ product.image_url }}">
product.featured_imageObjectPrimary product image object`{{ product.featured_image
product.imagesArrayAll product images{{ product.images[0].src }}
product.descriptionStringFull HTML description`{{ product.description
product.short_descriptionStringBrief description{{ product.short_description }}
product.variantsArrayProduct variants{%for v in product.variants%}
product.variants.firstObjectFirst variant{{ product.variants.first.id }}
product.selected_or_first_available_variantObjectBest variant to show{%assign v = product.selected_or_first_available_variant%}
product.availableBooleanIn stock status{%if product.available%}
product.tagsArrayProduct tags{%for tag in product.tags%}
product.vendorStringProduct vendor/brand{{ product.vendor }}
product.typeStringProduct type{{ product.type }}

Critical: Always use {{ product.price | money }} - never display raw price numbers.

Collection Fields Reference

FieldTypeDescriptionUsage Example
collection.idNumberUnique collection identifier{{ collection.id }}
collection.titleStringCollection name{{ collection.title }}
collection.handleStringURL-friendly identifier{{ collection.handle }}
collection.urlStringLink to collection page<a href="{{ collection.url }}">
collection.imageObjectFeatured image object`{{ collection.image
collection.image_urlStringDirect image URL<img src="{{ collection.image_url }}">
collection.descriptionStringCollection description{{ collection.description }}
collection.productsArrayProducts in collection{%for p in collection.products%}
collection.products.firstObjectFirst product in collection{{ collection.products.first.image_url }}
collection.products_countNumberTotal products{{ collection.products_count }} items

Category Fields Reference

FieldTypeDescriptionUsage Example
category.idNumberUnique category identifier{{ category.id }}
category.titleStringCategory name{{ category.title }}
category.handleStringURL-friendly identifier{{ category.handle }}
category.imageObjectFeatured image`{{ category.image
category.image_urlStringDirect image URL<img src="{{ category.image_url }}">
category.descriptionStringCategory description{{ category.description }}
category.productsArrayProducts in category{%for p in category.products%}

Category Shop Links: Use /shop?filterrific[with_category_id][]={{ category.id }}

Post Fields Reference (Covered in detail above)

See "Blog Post Lists: The Global Loop Pattern" section for complete post field documentation.


Blocks: Reorderable Dynamic Content

Blocks allow merchants to add, remove, reorder, and customize multiple instances of content. Perfect for testimonials, features, FAQs, slides, etc.

Basic Block Pattern

Schema

{
  "name": "Features Grid",
  "blocks": [
    {
      "type": "feature",
      "name": "Feature Item",
      "settings": [
        {
          "type": "text",
          "id": "title",
          "label": "Feature Title",
          "default": "Fast Shipping"
        },
        {
          "type": "textarea",
          "id": "description",
          "label": "Description"
        },
        {
          "type": "image_picker",
          "id": "icon",
          "label": "Icon"
        }
      ]
    }
  ],
  "max_blocks": 6
}

Liquid

<div class="features-grid">
  {`%` for block in section.blocks `%`}
    {`%` if block.type == 'feature' `%`}
      <div class="feature-item" {{ block.fluid_attributes }}>
        {`%` if block.settings.icon `%`}
          <img src="{{ block.settings.icon | image_url: width: 80 }}" alt="Icon">
        {`%` endif `%`}
        <h3>{{ block.settings.title }}</h3>
        <p>{{ block.settings.description }}</p>
      </div>
    {`%` endif `%`}
  {`%` endfor `%`}
</div>

Critical: Always include {{ block.fluid_attributes }} on the block wrapper for editor functionality.

Advanced Block Pattern: Multiple Block Types

Sections can accept different block types for flexibility:

Schema

{
  "name": "Content Section",
  "blocks": [
    {
      "type": "text_block",
      "name": "Text Content",
      "settings": [
        {
          "type": "richtext",
          "id": "content",
          "label": "Content"
        }
      ]
    },
    {
      "type": "image_block",
      "name": "Image",
      "settings": [
        {
          "type": "image_picker",
          "id": "image",
          "label": "Image"
        },
        {
          "type": "text",
          "id": "caption",
          "label": "Caption"
        }
      ]
    },
    {
      "type": "video_block",
      "name": "Video Embed",
      "settings": [
        {
          "type": "text",
          "id": "video_picker",
          "label": "Video URL (YouTube or Vimeo)"
        }
      ]
    }
  ]
}

Liquid

<div class="content-blocks">
  {`%` for block in section.blocks `%`}
    
    {`%` if block.type == 'text_block' `%`}
      <div class="text-block" {{ block.fluid_attributes }}>
        {{ block.settings.content }}
      </div>
    
    {`%` elsif block.type == 'image_block' `%`}
      <div class="image-block" {{ block.fluid_attributes }}>
        {`%` if block.settings.image `%`}
          <img src="{{ block.settings.image | image_url: width: 1200 }}" alt="{{ block.settings.caption }}">
          {`%` if block.settings.caption `%`}
            <p class="caption">{{ block.settings.caption }}</p>
          {`%` endif `%`}
        {`%` endif `%`}
      </div>
    
    {`%` elsif block.type == 'video_block' `%`}
      <div class="video-block" {{ block.fluid_attributes }}>
        {`%` if block.settings.video_url contains 'youtube' `%`}
          <iframe src="{{ block.settings.video_url }}" frameborder="0" allowfullscreen></iframe>
        {`%` elsif block.settings.video_url contains 'vimeo' `%`}
          <iframe src="{{ block.settings.video_url }}" frameborder="0" allowfullscreen></iframe>
        {`%` endif `%`}
      </div>
    
    {`%` endif `%`}
  {`%` endfor `%`}
</div>

Block Limits and Presets

Control how many blocks can be added and provide starting content:

{
  "name": "Testimonials",
  "blocks": [
    {
      "type": "testimonial",
      "name": "Testimonial",
      "limit": 12,
      "settings": [...]
    }
  ],
  "max_blocks": 12,
  "presets": [
    {
      "name": "Testimonials Section",
      "blocks": [
        {
          "type": "testimonial",
          "settings": {
            "quote": "This product changed my life!",
            "author": "Jane Doe",
            "rating": 5
          }
        },
        {
          "type": "testimonial",
          "settings": {
            "quote": "Excellent quality and fast shipping.",
            "author": "John Smith",
            "rating": 5
          }
        }
      ]
    }
  ]
}

Best Practices: Blocks

DO:

  • Always include {{ block.fluid_attributes }}
  • Check block type with {%if block.type == 'name'%}
  • Use section.blocks.size to check if blocks exist
  • Provide meaningful default values
  • Use limit to prevent performance issues
  • Include helpful info text in settings
  • Create useful presets with example content

DON'T:

  • Forget {{ block.fluid_attributes }}
  • Skip block type checking
  • Allow unlimited blocks (causes editor issues)
  • Use generic names like "Block 1", "Block 2"
  • Forget to handle empty blocks gracefully
  • Mix blocks and section settings for the same purpose

Complete Settings Reference

This is the exhaustive list of all supported schema setting types in Fluid.

Text Input Types

TypeDescriptionUse CaseExample
textSingle-line textTitles, labels, short textHeading, button text
textareaMulti-line plain textLonger text without formattingDescriptions, captions
richtext or rich_textRich text editorFormatted contentAbout sections, long descriptions
html or html_textareaRaw HTML inputCustom HTML codeEmbeds, custom widgets
urlURL input with validationLinks, external resourcesButton links, social links

Example:

{
  "type": "text",
  "id": "heading",
  "label": "Section Heading",
  "default": "Welcome to Our Store",
  "info": "This appears at the top of the section"
}

Number & Selection Types

TypeDescriptionUse CaseExample
rangeSlider with min/maxNumeric settings with boundsFont size, opacity, spacing
selectDropdown menuPredefined optionsFont family, color scheme
radioRadio buttonsVisual choice selectionLayout options
checkboxToggle on/offBoolean settingsShow/hide elements

[!NOTE] The number type is not supported. Use range instead for numeric inputs.

Example:

{
  "type": "range",
  "id": "font_size",
  "label": "Font Size",
  "min": 12,
  "max": 72,
  "step": 2,
  "default": 24,
  "unit": "px"
}

Visual & Media Types

TypeDescriptionUse CaseExample
colorColor pickerSimple colorsText color, background
color_backgroundColor with gradient supportComplex backgroundsHero backgrounds
font_pickerFont family selectorTypographyHeading fonts
image or image_pickerImage upload/selectImagesLogos, backgrounds, photos
video_pickerVideo upload/selectVideosHero videos, product demos

Example:

{
  "type": "color",
  "id": "text_color",
  "label": "Text Color",
  "default": "#000000"
}

Resource Selector Types

Single Resource Selectors

Select one item from the specified resource type:

TypeDescriptionReturns
productSingle productProduct ID (need to find from global array)
productsSingle product (alias)Same as product
collectionSingle collectionCollection ID (need to find from global array)
collectionsSingle collection (alias)Same as collection
categorySingle categoryCategory ID (need to find from global array)
categoriesSingle category (alias)Same as category
postSingle blog postPost ID (need to find from global array)
postsSingle blog post (alias)Same as post
blogSingle blogBlog ID
formsSingle formForm ID
enrollmentSingle enrollmentEnrollment ID
enrollmentsSingle enrollment (alias)Same as enrollment
enrollment_packSingle enrollment packEnrollment pack ID

[!TIP] Some types have singular/plural aliases (e.g., product and products both work for single selection). Use whichever feels more natural.

Multiple Resource Selectors (Lists)

Select multiple items - direct iteration possible:

TypeDescriptionReturns
product_list or products_listMultiple productsArray of full product objects
collection_list or collections_listMultiple collectionsArray of full collection objects
category_list or categories_listMultiple categoriesArray of full category objects
posts_listMultiple blog postsArray of full post objects
enrollments_list or enrollment_listMultiple enrollmentsArray of enrollment objects

[!TIP] Use product_list (singular) or products_list (plural) - both work the same way. Same applies to collections and categories.

Example (Single):

{
  "type": "product",
  "id": "featured_product",
  "label": "Select Product"
}

Example (Multiple):

{
  "type": "product_list",
  "id": "featured_products",
  "label": "Featured Products",
  "limit": 8,
  "info": "Select up to 8 products to feature"
}

Organization Types

TypeDescriptionUse Case
headerSection divider with headingGroup related settings

Example:

{
  "type": "header",
  "content": "Typography Settings"
}

Special Types

TypeDescriptionUse Case
text_alignmentText alignment pickerLeft/center/right alignment
link_listMenu selectorNavigation menus

Unsupported Types

The following types from other platforms (like Shopify) are NOT supported in Fluid:

[!CAUTION] Using these types in your {%schema%} will cause errors or prevent the section from rendering properly in the editor.

Not Supported - Use Alternatives Instead

Unsupported TypeAlternativeNotes
numberUse rangeFluid uses range sliders for numeric inputs
paragraphUse header with descriptionHeaders can include instructional text
inline_richtextUse text or richtextNot available in Fluid
articleUse postFluid uses "posts" instead of "articles"
article_listUse posts_listFluid uses "posts" instead of "articles"
videoUse video_picker or urlDifferent implementation in Fluid
video_urlUse url or video_pickerUse URL type for video links
pageNot availablePages are handled differently in Fluid
liquidNot availableCannot inject raw Liquid code
color_schemeUse colorColor schemes not implemented
color_scheme_groupUse multiple color settingsGroup colors manually
metaobjectNot availableMetaobjects not implemented
metaobject_listNot availableMetaobjects not implemented

Common Migration Tips

Coming from Shopify? Here's how to adapt:

// ❌ Shopify (not supported)
{
  "type": "article",
  "id": "featured_article"
}

// ✅ Fluid (correct)
{
  "type": "post",
  "id": "featured_post"
}
// ❌ Shopify (not supported)
{
  "type": "number",
  "id": "quantity"
}

// ✅ Fluid (correct)
{
  "type": "range",
  "id": "quantity",
  "min": 1,
  "max": 10,
  "step": 1,
  "default": 1
}
// ❌ Shopify (not supported)
{
  "type": "video_url",
  "id": "video"
}

// ✅ Fluid (correct)
{
  "type": "url",
  "id": "video_url",
  "label": "Video URL (YouTube or Vimeo)"
}

Global Theme Settings

While section schemas control individual components, Global Settings control site-wide configurations.

Configuration File

Global settings are defined in:

config/settings_schema.json

This file uses an array of objects, where each object represents a "Tab" in the Theme Settings.

Example:

[
  {
    "name": "Colors",
    "settings": [
      {
        "type": "color",
        "id": "color_primary",
        "label": "Primary Brand Color",
        "default": "#1C0F8A"
      },
      {
        "type": "color",
        "id": "color_secondary",
        "label": "Secondary Color",
        "default": "#FF6B6B"
      }
    ]
  },
  {
    "name": "Typography",
    "settings": [
      {
        "type": "font_picker",
        "id": "font_heading",
        "label": "Heading Font",
        "default": "helvetica_n4"
      },
      {
        "type": "font_picker",
        "id": "font_body",
        "label": "Body Font",
        "default": "helvetica_n4"
      }
    ]
  },
  {
    "name": "Social Media",
    "settings": [
      {
        "type": "url",
        "id": "social_instagram",
        "label": "Instagram URL"
      },
      {
        "type": "url",
        "id": "social_facebook",
        "label": "Facebook URL"
      }
    ]
  }
]

Accessing Global Settings

Global settings use the settings object (not section.settings):

<style>
  :root {
    --primary-color: {{ settings.color_primary | default: '#000000' }};
    --secondary-color: {{ settings.color_secondary | default: '#666666' }};
    --font-heading: {{ settings.font_heading.family }};
    --font-body: {{ settings.font_body.family }};
  }
</style>

<a href="{{ settings.social_instagram }}" target="_blank">
  Follow us on Instagram
</a>

[!IMPORTANT] Global settings are perfect for design tokens (colors, fonts, spacing) that should remain consistent across all pages and sections.


Common Mistakes and How to Avoid Them

❌ Mistake 1: Using Generic IDs

Bad:

{
  "type": "text",
  "id": "title",
  "label": "Title"
}

Problem: "title" is too generic and may conflict with other sections.

Good:

{
  "type": "text",
  "id": "hero_title",
  "label": "Hero Title"
}

❌ Mistake 2: Forgetting Money Filter

Bad:

<span class="price">{{ product.price }}</span>

Result: Displays "1999" instead of "$19.99"

Good:

<span class="price">{{ product.price | money }}</span>

❌ Mistake 3: Hardcoding Asset URLs

Bad:

<img src="logo.png">

Result: Image won't load

Good:

<img src="{{ 'logo.png' | asset_url }}">

❌ Mistake 4: Not Using fluid_attributes

Bad:

{`%` for block in section.blocks `%`}
  <div class="block">...</div>
{`%` endfor `%`}

Result: Blocks can't be highlighted in editor

Good:

{`%` for block in section.blocks `%`}
  <div class="block" {{ block.fluid_attributes }}>...</div>
{`%` endfor `%`}

❌ Mistake 5: Assuming Resources Exist

Bad:

<h2>{{ section.settings.featured_collection.title }}</h2>

Result: Crashes if no collection selected

Good:

{`%` if section.settings.featured_collection != blank `%`}
  <h2>{{ section.settings.featured_collection.title }}</h2>
{`%` endif `%`}

❌ Mistake 6: Invalid JSON in Schema

Bad:

{
  "name": "My Section",
  "settings": [
    {
      "type": "text",
      "id": "title"
    },
  ]
}

Problem: Trailing comma after last item

Good:

{
  "name": "My Section",
  "settings": [
    {
      "type": "text",
      "id": "title"
    }
  ]
}

Troubleshooting & FAQ

Q: Why isn't my setting showing up in the editor?

A: Check your JSON syntax. A missing comma, mismatched bracket, or trailing comma will prevent the section from loading. Use a JSON validator.

Q: My product_list is empty even though I selected products.

A: Verify you're using the correct setting ID:

{`%`- comment -`%`} Schema has id: "featured_products" {`%`- endcomment -`%`}
{`%` for product in section.settings.featured_products `%`}
  {`%`- comment -`%`} NOT section.settings.products {`%`- endcomment -`%`}
{`%` endfor `%`}

Q: How do I make a setting only appear if a checkbox is checked?

A: Currently, conditional settings (visible/hidden based on other settings) are not supported. Use clear labels and organization with headers to guide users.

Q: Images from resource selectors look distorted.

A: Use CSS object-fit: cover on images:

.product-image {
  width: 100%;
  height: 300px;
  object-fit: cover;
}

Q: My collection selector isn't working.

A: Remember to loop through the global collections array to find your collection:

{`%` assign collection_id = block.settings.collection `%`}

{`%` for c in collections `%`}
  {`%` if c.id == collection_id `%`}
    {`%` assign current_collection = c `%`}
    {`%` break `%`}
  {`%` endif `%`}
{`%` endfor `%`}

{`%` if current_collection != blank `%`}
  {`%`- comment -`%`} Now use current_collection {`%`- endcomment -`%`}
{`%` endif `%`}

Q: Pagination isn't working.

A: Make sure everything that needs posts or products is inside the {%paginate%} tags:

{`%` paginate posts by 12 `%`}
  {`%`- comment -`%`} All code accessing posts goes here {`%`- endcomment -`%`}
  {`%` for post in posts `%`}...{`%` endfor `%`}
  
  {`%`- comment -`%`} Pagination controls also go here {`%`- endcomment -`%`}
  {`%` if paginate.pages > 1 `%`}
    {`%` render 'pagination', paginate: paginate `%`}
  {`%` endif `%`}
{`%` endpaginate `%`}

Quick Reference: Decision Tree

Need to display products/collections/categories?

├─ Hand-picked by merchant?
│  ├─ YES → Use resource selector (product, collection, category)
│  │        with blocks for flexibility
│  └─ NO → Continue below
│
├─ Show everything automatically?
│  ├─ YES → Use global loop ({`%` for item in items `%`})
│  │        Add pagination if needed ({`%` paginate `%`})
│  └─ NO → Use resource selector
│
└─ Need pagination?
   ├─ YES → Use {`%` paginate `%`} with global loop
   └─ NO → Direct loop or resource selector

Need to let users customize?

├─ Section-level settings?
│  └─ Use section.settings with schema types
│
├─ Repeatable items?
│  └─ Use blocks with block.settings
│
└─ Site-wide settings?
   └─ Use config/settings_schema.json

Final Best Practices Summary

Schema Design

  1. Write for the User: Use labels like "Mobile Heading Font Size" instead of "heading_fs_mb"
  2. Organize with Headers: Use "type": "header" to group related settings
  3. Provide Defaults: Always include sensible default values
  4. Add Info Text: Use "info" to explain complex settings
  5. Validate JSON: Always validate before saving

Liquid Implementation

  1. Check for Blank: Always check if resources exist before using them
  2. Use Filters: Apply | money, | image_url, | unescape correctly
  3. Include Fallbacks: Provide multiple fallback options for images and data
  4. Add fluid_attributes: Include {{ block.fluid_attributes }} on all blocks
  5. Handle Empty States: Show helpful messages when no content exists

Performance

  1. Limit Selections: Use "limit" on resource selectors to prevent huge selections
  2. Lazy Load Images: Add loading="lazy" to images below the fold
  3. Optimize Image Sizes: Use | image_url: width: 600 to request appropriate sizes
  4. Use Pagination: Always paginate large datasets
  5. Minimize Loops: Avoid nested loops when possible

Accessibility

  1. Alt Text: Always provide alt text for images
  2. Semantic HTML: Use <article>, <time>, <nav> appropriately
  3. Keyboard Navigation: Ensure all interactive elements are keyboard accessible
  4. ARIA Labels: Add aria-labels for icon-only buttons
  5. Color Contrast: Ensure text has sufficient contrast against backgrounds

Need More Help?

  • Section Examples: Browse app/themes/templates/ for real production examples
  • Liquid Filters: See Liquid documentation for available filters
  • Schema Validation: Use a JSON validator before saving schemas
  • Community: Check internal documentation or ask the team

All Schema Types: Complete Reference

This section contains the complete liquid implementation and schema settings for all supported schema types in the Fluid theme system.

Liquid Implementation



<section 
    class="all-schema-type-section {{ section.settings.background_color | default: 'bg-white' }}"
    style="padding-top: var(--padding-2xl); padding-bottom: var(--padding-2xl);"
  >
    <div class="all-schema-type-container container">
      
      <!-- Section Heading -->
      {`%` if section.settings.section_heading != blank `%`}
      <div class="section-heading mb-2xl text-center">
        <h1 class="{{ section.settings.heading_font_size | default: 'text-3xl' }} {{ section.settings.heading_font_size_desktop | default: 'lg:text-5xl' }} {{ section.settings.heading_font_weight | default: 'font-bold' }} {{ section.settings.heading_color | default: 'text-black' }}">
          {{ section.settings.section_heading }}
        </h1>
        {`%` if section.settings.section_subheading != blank `%`}
        <p class="{{ section.settings.subheading_font_size | default: 'text-base' }} {{ section.settings.subheading_color | default: 'text-neutral-dark' }} mt-md">
          {{ section.settings.section_subheading }}
        </p>
        {`%` endif `%`}
      </div>
      {`%` endif `%`}
  
      <!-- TEXT INPUT TYPES -->
      <div class="schema-group mb-3xl">
        <h2 class="group-title mb-xl">Text Input Types</h2>
        
        <div class="schema-grid">
          <!-- Text -->
          {`%` if section.settings.text_field != blank `%`}
          <div class="schema-item">
            <label class="schema-label">Text Field:</label>
            <div class="schema-value">{{ section.settings.text_field }}</div>
          </div>
          {`%` endif `%`}
  
          <!-- Textarea -->
          {`%` if section.settings.textarea_field != blank `%`}
          <div class="schema-item">
            <label class="schema-label">Textarea Field:</label>
            <div class="schema-value">{{ section.settings.textarea_field }}</div>
          </div>
          {`%` endif `%`}
  
          <!-- Richtext -->
          {`%` if section.settings.richtext_field != blank `%`}
          <div class="schema-item">
            <label class="schema-label">Richtext Field:</label>
            <div class="schema-value richtext-content">{{ section.settings.richtext_field }}</div>
          </div>
          {`%` endif `%`}
  
          <!-- HTML -->
          {`%` if section.settings.html_field != blank `%`}
          <div class="schema-item">
            <label class="schema-label">HTML Field:</label>
            <div class="schema-value html-content">{{ section.settings.html_field }}</div>
          </div>
          {`%` endif `%`}
  
          <!-- URL -->
          {`%` if section.settings.url_field != blank `%`}
          <div class="schema-item">
            <label class="schema-label">URL Field:</label>
            <div class="schema-value">
              <a href="{{ section.settings.url_field }}" target="_blank" class="text-primary hover:underline">
                {{ section.settings.url_field }}
              </a>
            </div>
          </div>
          {`%` endif `%`}
        </div>
      </div>
  
      <!-- NUMBER & SELECTION TYPES -->
      <div class="schema-group mb-3xl">
        <h2 class="group-title mb-xl">Number & Selection Types</h2>
        
        <div class="schema-grid">
          <!-- Range -->
          {`%` if section.settings.range_field != blank `%`}
          <div class="schema-item">
            <label class="schema-label">Range Field:</label>
            <div class="schema-value">{{ section.settings.range_field }}</div>
          </div>
          {`%` endif `%`}
  
          <!-- Select -->
          {`%` if section.settings.select_field != blank `%`}
          <div class="schema-item">
            <label class="schema-label">Select Field:</label>
            <div class="schema-value">{{ section.settings.select_field }}</div>
          </div>
          {`%` endif `%`}
  
          <!-- Radio -->
          {`%` if section.settings.radio_field != blank `%`}
          <div class="schema-item">
            <label class="schema-label">Radio Field:</label>
            <div class="schema-value">{{ section.settings.radio_field }}</div>
          </div>
          {`%` endif `%`}
  
          <!-- Checkbox -->
          {`%` if section.settings.show_checkbox_field `%`}
          <div class="schema-item">
            <label class="schema-label">Checkbox Field:</label>
            <div class="schema-value">
              {`%` if section.settings.checkbox_field `%`}
                <span class="text-success">✓ Enabled</span>
              {`%` else `%`}
                <span class="text-neutral-medium">✗ Disabled</span>
              {`%` endif `%`}
            </div>
          </div>
          {`%` endif `%`}
        </div>
      </div>
  
      <!-- VISUAL & MEDIA TYPES -->
      <div class="schema-group mb-3xl">
        <h2 class="group-title mb-xl">Visual & Media Types</h2>
        
        <div class="schema-grid">
          <!-- Color -->
          {`%` if section.settings.color_field != blank `%`}
          <div class="schema-item">
            <label class="schema-label">Color Field:</label>
            <div class="schema-value flex items-center gap-sm">
              <div class="color-swatch" style="background-color: {{ section.settings.color_field }}; width: 40px; height: 40px; border-radius: 4px; border: 1px solid #ddd;"></div>
              <span>{{ section.settings.color_field }}</span>
            </div>
          </div>
          {`%` endif `%`}
  
          <!-- Color Background -->
          {`%` if section.settings.color_background_field != blank `%`}
          <div class="schema-item">
            <label class="schema-label">Color Background Field:</label>
            <div class="schema-value">
              <div class="color-background-preview" style="background: {{ section.settings.color_background_field }}; width: 100%; height: 60px; border-radius: 4px; border: 1px solid #ddd;"></div>
            </div>
          </div>
          {`%` endif `%`}
  
          <!-- Font Picker -->
          {`%` if section.settings.font_picker_field != blank `%`}
          <div class="schema-item">
            <label class="schema-label">Font Picker Field:</label>
            <div class="schema-value" style="font-family: {{ section.settings.font_picker_field }};">
              Sample Text: {{ section.settings.font_picker_field }}
            </div>
          </div>
          {`%` endif `%`}
  
          <!-- Image Picker -->
          {`%` if section.settings.image_picker_field != blank `%`}
          <div class="schema-item">
            <label class="schema-label">Image Picker Field:</label>
            <div class="schema-value">
              <img 
                src="{{ section.settings.image_picker_field | image_url: width: 400 }}" 
                alt="{{ section.settings.image_alt_text | default: 'Image' }}"
                class="schema-image"
                loading="lazy">
            </div>
          </div>
          {`%` endif `%`}
  
          <!-- Video Picker -->
          {`%` if section.settings.video_picker_field != blank `%`}
          <div class="schema-item">
            <label class="schema-label">Video Picker Field:</label>
            <div class="schema-value">
              <video 
                controls 
                class="schema-video"
                {`%` if section.settings.video_poster != blank `%`}
                poster="{{ section.settings.video_poster | image_url: width: 800 }}"
                {`%` endif `%`}>
                <source src="{{ section.settings.video_picker_field }}" type="video/mp4">
                Your browser does not support the video tag.
              </video>
            </div>
          </div>
          {`%` endif `%`}
        </div>
      </div>
  
      <!-- RESOURCE SELECTORS - SINGLE -->
      <div class="schema-group mb-3xl">
        <h2 class="group-title mb-xl">Resource Selectors (Single)</h2>
        
        <div class="schema-grid">
          <!-- Product -->
          {`%` if section.settings.product_field != blank `%`}
            {`%`- assign product_setting = section.settings.product_field -`%`}
            {`%`- assign selected_product = blank -`%`}
            
            {`%`- comment -`%`} Check if it's already a full product object {`%`- endcomment -`%`}
            {`%`- if product_setting.title != blank and product_setting.url != blank -`%`}
              {`%`- assign selected_product = product_setting -`%`}
            {`%`- else -`%`}
              {`%`- comment -`%`} Extract ID from object or use directly {`%`- endcomment -`%`}
              {`%`- assign product_id = blank -`%`}
              {`%`- if product_setting.id != blank -`%`}
                {`%`- assign product_id = product_setting.id -`%`}
              {`%`- elsif product_setting != blank -`%`}
                {`%`- assign product_id = product_setting -`%`}
              {`%`- endif -`%`}
              
              {`%`- comment -`%`} Find product from global products array {`%`- endcomment -`%`}
              {`%`- if product_id != blank -`%`}
                {`%`- if products != blank -`%`}
                  {`%`- for p in products -`%`}
                    {`%`- if p.id == product_id -`%`}
                      {`%`- assign selected_product = p -`%`}
                      {`%`- break -`%`}
                    {`%`- endif -`%`}
                  {`%`- endfor -`%`}
                {`%`- endif -`%`}
              {`%`- endif -`%`}
            {`%`- endif -`%`}
            
            {`%` if selected_product != blank `%`}
            <div class="schema-item">
              <label class="schema-label">Product Field:</label>
              <div class="schema-value">
                <div class="resource-grid">
                  <a href="{{ selected_product.url }}" class="resource-card">
                    {`%` assign product_image = '' `%`}
                    {`%` if selected_product.image_url `%`}
                      {`%` assign product_image = selected_product.image_url | image_url: width: 200 `%`}
                    {`%` elsif selected_product.image `%`}
                      {`%` assign product_image = selected_product.image | image_url: width: 200 `%`}
                    {`%` elsif selected_product.images.size > 0 `%`}
                      {`%` assign product_image = selected_product.images.first | image_url: width: 200 `%`}
                    {`%` endif `%`}
                    {`%` if product_image != '' `%`}
                    <img src="{{ product_image }}" alt="{{ selected_product.title }}" class="resource-image">
                    {`%` else `%`}
                    <div class="resource-image-placeholder">
                      <span class="text-neutral-medium">No image</span>
                    </div>
                    {`%` endif `%`}
                    <div class="resource-info">
                      <div class="font-semibold">{{ selected_product.title }}</div>
                      <div class="text-sm text-neutral-medium">{{ selected_product.price | money }}</div>
                    </div>
                  </a>
                </div>
              </div>
            </div>
            {`%` endif `%`}
          {`%` endif `%`}
  
          <!-- Collection -->
          {`%` if section.settings.collection_field != blank `%`}
            {`%`- assign collection_setting = section.settings.collection_field -`%`}
            {`%`- assign selected_collection = blank -`%`}
            
            {`%`- comment -`%`} Check if it's already a full collection object {`%`- endcomment -`%`}
            {`%`- if collection_setting.title != blank -`%`}
              {`%`- assign selected_collection = collection_setting -`%`}
            {`%`- else -`%`}
              {`%`- comment -`%`} Extract ID from object or use directly {`%`- endcomment -`%`}
              {`%`- assign collection_id = blank -`%`}
              {`%`- if collection_setting.id != blank -`%`}
                {`%`- assign collection_id = collection_setting.id -`%`}
              {`%`- elsif collection_setting != blank -`%`}
                {`%`- assign collection_id = collection_setting -`%`}
              {`%`- endif -`%`}
              
              {`%`- comment -`%`} Find collection from global collections array {`%`- endcomment -`%`}
              {`%`- if collection_id != blank -`%`}
                {`%`- if collections != blank -`%`}
                  {`%`- for c in collections -`%`}
                    {`%`- if c.id == collection_id -`%`}
                      {`%`- assign selected_collection = c -`%`}
                      {`%`- break -`%`}
                    {`%`- endif -`%`}
                  {`%`- endfor -`%`}
                {`%`- endif -`%`}
              {`%`- endif -`%`}
            {`%`- endif -`%`}
            
            {`%` if selected_collection != blank `%`}
            <div class="schema-item">
              <label class="schema-label">Collection Field:</label>
              <div class="schema-value">
                <div class="resource-grid">
                  <a href="{{ selected_collection.url }}" class="resource-card">
                    {`%` assign collection_image = '' `%`}
                    {`%` if selected_collection.image `%`}
                      {`%` assign collection_image = selected_collection.image | image_url: width: 200 `%`}
                    {`%` elsif selected_collection.image_url `%`}
                      {`%` assign collection_image = selected_collection.image_url `%`}
                    {`%` elsif selected_collection.products.size > 0 and selected_collection.products.first.images.size > 0 `%`}
                      {`%` assign collection_image = selected_collection.products.first.images.first.src `%`}
                    {`%` endif `%`}
                    {`%` if collection_image != '' `%`}
                    <img src="{{ collection_image }}" alt="{{ selected_collection.title }}" class="resource-image">
                    {`%` else `%`}
                    <div class="resource-image-placeholder">
                      <span class="text-neutral-medium">No image</span>
                    </div>
                    {`%` endif `%`}
                    <div class="resource-info">
                      <div class="font-semibold">{{ selected_collection.title }}</div>
                      <div class="text-sm text-neutral-medium">{{ selected_collection.products_count }} products</div>
                    </div>
                  </a>
                </div>
              </div>
            </div>
            {`%` endif `%`}
          {`%` endif `%`}
  
          <!-- Category -->
          {`%` if section.settings.category_field != blank `%`}
            {`%`- assign category_setting = section.settings.category_field -`%`}
            {`%`- assign selected_category = blank -`%`}
            
            {`%`- comment -`%`} Check if it's already a full category object {`%`- endcomment -`%`}
            {`%`- if category_setting.title != blank -`%`}
              {`%`- assign selected_category = category_setting -`%`}
            {`%`- else -`%`}
              {`%`- comment -`%`} Extract ID from object or use directly {`%`- endcomment -`%`}
              {`%`- assign category_id = blank -`%`}
              {`%`- if category_setting.id != blank -`%`}
                {`%`- assign category_id = category_setting.id -`%`}
              {`%`- elsif category_setting != blank -`%`}
                {`%`- assign category_id = category_setting -`%`}
              {`%`- endif -`%`}
              
              {`%`- comment -`%`} Find category from global categories array {`%`- endcomment -`%`}
              {`%`- if category_id != blank -`%`}
                {`%`- if categories != blank -`%`}
                  {`%`- for cat in categories -`%`}
                    {`%`- if cat.id == category_id -`%`}
                      {`%`- assign selected_category = cat -`%`}
                      {`%`- break -`%`}
                    {`%`- endif -`%`}
                  {`%`- endfor -`%`}
                {`%`- endif -`%`}
              {`%`- endif -`%`}
            {`%`- endif -`%`}
            
            {`%` if selected_category != blank `%`}
            <div class="schema-item">
              <label class="schema-label">Category Field:</label>
              <div class="schema-value">
                <div class="resource-grid">
                  <a href="{{ selected_category.url }}" class="resource-card">
                    {`%` if selected_category.image_url `%`}
                    <img src="{{ selected_category.image_url | image_url: width: 200 }}" alt="{{ selected_category.title }}" class="resource-image">
                    {`%` elsif selected_category.image `%`}
                    <img src="{{ selected_category.image | image_url: width: 200 }}" alt="{{ selected_category.title }}" class="resource-image">
                    {`%` else `%`}
                    <div class="resource-image-placeholder">
                      <span class="text-neutral-medium">No image</span>
                    </div>
                    {`%` endif `%`}
                    <div class="resource-info">
                      <div class="font-semibold">{{ selected_category.title }}</div>
                      {`%` if selected_category.description != blank `%`}
                      <div class="text-sm text-neutral-medium">{{ selected_category.description | strip_html | truncate: 60 }}</div>
                      {`%` endif `%`}
                    </div>
                  </a>
                </div>
              </div>
            </div>
            {`%` endif `%`}
          {`%` endif `%`}
  
          <!-- Post (using posts schema type) -->
          {`%` if section.settings.post_field != blank `%`}
            {`%`- assign post_setting = section.settings.post_field -`%`}
            {`%`- assign selected_post = blank -`%`}
            
            {`%`- comment -`%`} Try to access post - if it's already a full object, use it directly {`%`- endcomment -`%`}
            {`%`- if post_setting.title != blank -`%`}
              {`%`- assign selected_post = post_setting -`%`}
            {`%`- else -`%`}
              {`%`- comment -`%`} If it's an ID, we need to look it up in posts array (only available in paginate) {`%`- endcomment -`%`}
              {`%`- assign post_id = post_setting.id | default: post_setting -`%`}
              
              {`%`- if post_id != blank -`%`}
                {`%`- paginate posts by 1000 -`%`}
                  {`%`- for p in posts -`%`}
                    {`%`- assign id_to_match = post_id | plus: 0 -`%`}
                    {`%`- if p.id == id_to_match or p.id == post_id or p.slug == post_id -`%`}
                      {`%`- assign selected_post = p -`%`}
                      {`%`- break -`%`}
                    {`%`- endif -`%`}
                  {`%`- endfor -`%`}
                {`%`- endpaginate -`%`}
              {`%`- endif -`%`}
            {`%`- endif -`%`}
            
            {`%` if selected_post != blank and selected_post.title != blank `%`}
            <div class="schema-item">
              <label class="schema-label">Post Field (posts type):</label>
              <div class="schema-value">
                <div class="resource-grid">
                  <a href="{{ selected_post.preview_url }}" class="resource-card">
                    {`%` assign image_url = '' `%`}
                    {`%` if selected_post.image_url `%`}
                      {`%` assign image_url = selected_post.image_url `%`}
                    {`%` elsif selected_post.image `%`}
                      {`%` assign image_url = selected_post.image | image_url `%`}
                    {`%` elsif selected_post.images.size > 0 `%`}
                      {`%` assign image_url = selected_post.images[0].src `%`}
                    {`%` endif `%`}
                    {`%` if image_url != '' `%`}
                    <img src="{{ image_url }}" alt="{{ selected_post.title }}" class="resource-image">
                    {`%` else `%`}
                    <div class="resource-image-placeholder">
                      <span class="text-neutral-medium">No image</span>
                    </div>
                    {`%` endif `%`}
                    <div class="resource-info">
                      <div class="font-semibold">{{ selected_post.title }}</div>
                      {`%` if selected_post.summary `%`}
                      <div class="text-sm text-neutral-medium">{{ selected_post.summary | strip_html | truncate: 60 }}</div>
                      {`%` elsif selected_post.description `%`}
                      <div class="text-sm text-neutral-medium">{{ selected_post.description | strip_html | truncate: 60 }}</div>
                      {`%` endif `%`}
                    </div>
                  </a>
                </div>
              </div>
            </div>
            {`%` endif `%`}
          {`%` endif `%`}

          <!-- Enrollment Pack -->
          {`%` if section.settings.enrollment_pack_field != blank `%`}
            {`%`- assign enrollment_pack_setting = section.settings.enrollment_pack_field -`%`}
            {`%`- assign selected_enrollment_pack = blank -`%`}
            
            {`%`- comment -`%`} Check if it's already a full enrollment_pack object {`%`- endcomment -`%`}
            {`%`- if enrollment_pack_setting.title != blank -`%`}
              {`%`- assign selected_enrollment_pack = enrollment_pack_setting -`%`}
            {`%`- else -`%`}
              {`%`- comment -`%`} Extract ID from object or use directly {`%`- endcomment -`%`}
              {`%`- assign enrollment_pack_id = enrollment_pack_setting.id | default: enrollment_pack_setting -`%`}
              
              {`%`- comment -`%`} Enrollment packs are resolved by backend, so if it's an ID, it should be looked up {`%`- endcomment -`%`}
              {`%`- if enrollment_pack_id != blank -`%`}
                {`%`- comment -`%`} Backend resolves enrollment_pack type, so setting should already be full object {`%`- endcomment -`%`}
                {`%`- assign selected_enrollment_pack = enrollment_pack_setting -`%`}
              {`%`- endif -`%`}
            {`%`- endif -`%`}
            
            {`%` if selected_enrollment_pack != blank and selected_enrollment_pack.title != blank `%`}
            <div class="schema-item">
              <label class="schema-label">Enrollment Pack Field:</label>
              <div class="schema-value">
                <div class="resource-grid">
                  <a href="{{ selected_enrollment_pack.url }}" class="resource-card">
                    {`%` assign enrollment_pack_image = '' `%`}
                    {`%` if selected_enrollment_pack.images_array.size > 0 `%`}
                      {`%` assign enrollment_pack_image = selected_enrollment_pack.images_array[0] `%`}
                    {`%` elsif selected_enrollment_pack.images.size > 0 `%`}
                      {`%` assign enrollment_pack_image = selected_enrollment_pack.images[0] `%`}
                    {`%` endif `%`}
                    {`%` if enrollment_pack_image != '' `%`}
                    <img src="{{ enrollment_pack_image }}" alt="{{ selected_enrollment_pack.title }}" class="resource-image">
                    {`%` else `%`}
                    <div class="resource-image-placeholder">
                      <span class="text-neutral-medium">No image</span>
                    </div>
                    {`%` endif `%`}
                    <div class="resource-info">
                      <div class="font-semibold">{{ selected_enrollment_pack.title }}</div>
                      {`%` if selected_enrollment_pack.price `%`}
                      <div class="text-sm text-neutral-medium">{{ selected_enrollment_pack.price }}</div>
                      {`%` endif `%`}
                    </div>
                  </a>
                </div>
              </div>
            </div>
            {`%` endif `%`}
          {`%` endif `%`}

          <!-- Enrollment -->
          {`%` if section.settings.enrollment_field != blank `%`}
            {`%`- assign enrollment_setting = section.settings.enrollment_field -`%`}
            {`%`- assign selected_enrollment = blank -`%`}
            
            {`%`- comment -`%`} Check if it's already a full enrollment object {`%`- endcomment -`%`}
            {`%`- if enrollment_setting.title != blank -`%`}
              {`%`- assign selected_enrollment = enrollment_setting -`%`}
            {`%`- else -`%`}
              {`%`- comment -`%`} Extract ID from object or use directly {`%`- endcomment -`%`}
              {`%`- assign enrollment_id = enrollment_setting.id | default: enrollment_setting -`%`}
              
              {`%`- comment -`%`} Enrollments are resolved by backend, so if it's an ID, it should be looked up {`%`- endcomment -`%`}
              {`%`- if enrollment_id != blank -`%`}
                {`%`- comment -`%`} Backend resolves enrollment type, so setting should already be full object {`%`- endcomment -`%`}
                {`%`- assign selected_enrollment = enrollment_setting -`%`}
              {`%`- endif -`%`}
            {`%`- endif -`%`}
            
            {`%` if selected_enrollment != blank and selected_enrollment.title != blank `%`}
            <div class="schema-item">
              <label class="schema-label">Enrollment Field:</label>
              <div class="schema-value">
                <div class="resource-grid">
                  <a href="{{ selected_enrollment.url }}" class="resource-card">
                    {`%` assign enrollment_image = '' `%`}
                    {`%` if selected_enrollment.images_array.size > 0 `%`}
                      {`%` assign enrollment_image = selected_enrollment.images_array[0] `%`}
                    {`%` elsif selected_enrollment.images.size > 0 `%`}
                      {`%` assign enrollment_image = selected_enrollment.images[0] `%`}
                    {`%` endif `%`}
                    {`%` if enrollment_image != '' `%`}
                    <img src="{{ enrollment_image }}" alt="{{ selected_enrollment.title }}" class="resource-image">
                    {`%` else `%`}
                    <div class="resource-image-placeholder">
                      <span class="text-neutral-medium">No image</span>
                    </div>
                    {`%` endif `%`}
                    <div class="resource-info">
                      <div class="font-semibold">{{ selected_enrollment.title }}</div>
                      {`%` if selected_enrollment.price `%`}
                      <div class="text-sm text-neutral-medium">{{ selected_enrollment.price }}</div>
                      {`%` endif `%`}
                    </div>
                  </a>
                </div>
              </div>
            </div>
            {`%` endif `%`}
          {`%` endif `%`}
        </div>
      </div>
  
      <!-- RESOURCE SELECTORS - MULTIPLE (LISTS) -->
      <div class="schema-group mb-3xl">
        <h2 class="group-title mb-xl">Resource Selectors (Multiple Lists)</h2>
        
        <!-- Product List -->
        {`%` if section.settings.product_list_field.size > 0 `%`}
        <div class="schema-item mb-lg">
          <label class="schema-label">Product List Field:</label>
          <div class="schema-value">
            <div class="resource-grid">
              {`%` for product in section.settings.product_list_field `%`}
              <a href="{{ product.url }}" class="resource-card">
                {`%` if product.image_url `%`}
                <img src="{{ product.image_url | image_url: width: 200 }}" alt="{{ product.title }}" class="resource-image">
                {`%` endif `%`}
                <div class="resource-info">
                  <div class="font-semibold">{{ product.title }}</div>
                  <div class="text-sm text-neutral-medium">{{ product.price | money }}</div>
                </div>
              </a>
              {`%` endfor `%`}
            </div>
          </div>
        </div>
        {`%` endif `%`}
  
        <!-- Collection List -->
        {`%` if section.settings.collection_list_field != blank `%`}
          {`%`- assign collection_list_setting = section.settings.collection_list_field -`%`}
          
          {`%`- comment -`%`} Check if already resolved (has title) or needs ID lookup {`%`- endcomment -`%`}
          {`%`- if collection_list_setting.size > 0 -`%`}
            {`%`- assign first_item = collection_list_setting[0] -`%`}
            {`%`- if first_item.title != blank -`%`}
              {`%`- comment -`%`} Already resolved - use directly {`%`- endcomment -`%`}
              <div class="schema-item mb-lg">
                <label class="schema-label">Collection List Field:</label>
                <div class="schema-value">
                  <div class="resource-grid">
                    {`%` for collection in collection_list_setting `%`}
                    <a href="{{ collection.url }}" class="resource-card">
                      {`%` assign collection_image = '' `%`}
                      {`%` if collection.image `%`}
                        {`%` assign collection_image = collection.image | image_url: width: 200 `%`}
                      {`%` elsif collection.image_url `%`}
                        {`%` assign collection_image = collection.image_url `%`}
                      {`%` elsif collection.products.size > 0 and collection.products.first.images.size > 0 `%`}
                        {`%` assign collection_image = collection.products.first.images.first.src `%`}
                      {`%` endif `%`}
                      {`%` if collection_image != '' `%`}
                      <img src="{{ collection_image }}" alt="{{ collection.title }}" class="resource-image">
                      {`%` else `%`}
                      <div class="resource-image-placeholder">
                        <span class="text-neutral-medium">No image</span>
                      </div>
                      {`%` endif `%`}
                      <div class="resource-info">
                        <div class="font-semibold">{{ collection.title }}</div>
                        <div class="text-sm text-neutral-medium">{{ collection.products_count }} products</div>
                      </div>
                    </a>
                    {`%` endfor `%`}
                  </div>
                </div>
              </div>
            {`%`- elsif collections != blank -`%`}
              {`%`- comment -`%`} Array of IDs - look them up in global collections array {`%`- endcomment -`%`}
              <div class="schema-item mb-lg">
                <label class="schema-label">Collection List Field:</label>
                <div class="schema-value">
                  <div class="resource-grid">
                    {`%`- for collection_id in collection_list_setting -`%`}
                      {`%`- assign id_to_check = collection_id.id | default: collection_id -`%`}
                          {`%`- for c in collections -`%`}
                            {`%`- if c.id == id_to_check or c.slug == id_to_check -`%`}
                              <a href="{{ c.url }}" class="resource-card">
                            {`%` assign collection_image = '' `%`}
                            {`%` if c.image `%`}
                              {`%` assign collection_image = c.image | image_url: width: 200 `%`}
                            {`%` elsif c.image_url `%`}
                              {`%` assign collection_image = c.image_url `%`}
                            {`%` elsif c.products.size > 0 and c.products.first.images.size > 0 `%`}
                              {`%` assign collection_image = c.products.first.images.first.src `%`}
                            {`%` endif `%`}
                            {`%` if collection_image != '' `%`}
                            <img src="{{ collection_image }}" alt="{{ c.title }}" class="resource-image">
                            {`%` else `%`}
                            <div class="resource-image-placeholder">
                              <span class="text-neutral-medium">No image</span>
                            </div>
                            {`%` endif `%`}
                            <div class="resource-info">
                              <div class="font-semibold">{{ c.title }}</div>
                              <div class="text-sm text-neutral-medium">{{ c.products_count }} products</div>
                            </div>
                          </a>
                          {`%`- break -`%`}
                        {`%`- endif -`%`}
                      {`%`- endfor -`%`}
                    {`%`- endfor -`%`}
                  </div>
                </div>
              </div>
            {`%`- endif -`%`}
          {`%`- endif -`%`}
        {`%` endif `%`}
  
        <!-- Category List -->
        {`%` if section.settings.category_list_field != blank `%`}
          {`%`- assign category_list_setting = section.settings.category_list_field -`%`}
          
          {`%`- comment -`%`} Check if already resolved (has title) or needs ID lookup {`%`- endcomment -`%`}
          {`%`- if category_list_setting.size > 0 -`%`}
            {`%`- assign first_item = category_list_setting[0] -`%`}
            {`%`- if first_item.title != blank -`%`}
              {`%`- comment -`%`} Already resolved - use directly {`%`- endcomment -`%`}
              <div class="schema-item mb-lg">
                <label class="schema-label">Category List Field:</label>
                <div class="schema-value">
                  <div class="resource-grid">
                    {`%` for category in category_list_setting `%`}
                    <a href="{{ category.url }}" class="resource-card">
                      {`%` if category.image_url `%`}
                      <img src="{{ category.image_url | image_url: width: 200 }}" alt="{{ category.title }}" class="resource-image">
                      {`%` endif `%`}
                      <div class="resource-info">
                        <div class="font-semibold">{{ category.title }}</div>
                        {`%` if category.description `%`}
                        <div class="text-sm text-neutral-medium">{{ category.description | strip_html | truncate: 60 }}</div>
                        {`%` endif `%`}
                      </div>
                    </a>
                    {`%` endfor `%`}
                  </div>
                </div>
              </div>
            {`%`- elsif categories != blank -`%`}
              {`%`- comment -`%`} Array of IDs - look them up in global categories array {`%`- endcomment -`%`}
              <div class="schema-item mb-lg">
                <label class="schema-label">Category List Field:</label>
                <div class="schema-value">
                  <div class="resource-grid">
                    {`%`- for category_id in category_list_setting -`%`}
                      {`%`- assign id_to_check = category_id.id | default: category_id -`%`}
                      {`%`- for cat in categories -`%`}
                        {`%`- if cat.id == id_to_check or cat.slug == id_to_check -`%`}
                          <a href="{{ cat.url }}" class="resource-card">
                            {`%` if cat.image_url `%`}
                            <img src="{{ cat.image_url | image_url: width: 200 }}" alt="{{ cat.title }}" class="resource-image">
                            {`%` endif `%`}
                            <div class="resource-info">
                              <div class="font-semibold">{{ cat.title }}</div>
                              {`%` if cat.description `%`}
                              <div class="text-sm text-neutral-medium">{{ cat.description | strip_html | truncate: 60 }}</div>
                              {`%` endif `%`}
                            </div>
                          </a>
                          {`%`- break -`%`}
                        {`%`- endif -`%`}
                      {`%`- endfor -`%`}
                    {`%`- endfor -`%`}
                  </div>
                </div>
              </div>
            {`%`- endif -`%`}
          {`%`- endif -`%`}
        {`%` endif `%`}
  
        <!-- Posts List -->
        {`%` if section.settings.posts_list_field != blank `%`}
          {`%`- assign posts_list_setting = section.settings.posts_list_field -`%`}
          
          {`%`- comment -`%`} Check if already resolved (has title) or needs ID lookup {`%`- endcomment -`%`}
          {`%`- if posts_list_setting.size > 0 -`%`}
            {`%`- assign first_item = posts_list_setting[0] -`%`}
            {`%`- if first_item.title != blank -`%`}
              {`%`- comment -`%`} Already resolved - use directly {`%`- endcomment -`%`}
              <div class="schema-item mb-lg">
                <label class="schema-label">Posts List Field:</label>
                <div class="schema-value">
                  <div class="resource-grid">
                    {`%` for post in posts_list_setting `%`}
                    <a href="{{ post.preview_url }}" class="resource-card">
                      {`%` assign image_url = '' `%`}
                      {`%` if post.image_url `%`}
                        {`%` assign image_url = post.image_url `%`}
                      {`%` elsif post.image `%`}
                        {`%` assign image_url = post.image | image_url `%`}
                      {`%` elsif post.images.size > 0 `%`}
                        {`%` assign image_url = post.images[0].src `%`}
                      {`%` endif `%`}
                      {`%` if image_url != '' `%`}
                      <img src="{{ image_url }}" alt="{{ post.title }}" class="resource-image">
                      {`%` else `%`}
                      <div class="resource-image-placeholder">
                        <span class="text-neutral-medium">No image</span>
                      </div>
                      {`%` endif `%`}
                      <div class="resource-info">
                        <div class="font-semibold">{{ post.title }}</div>
                        {`%` if post.summary `%`}
                        <div class="text-sm text-neutral-medium">{{ post.summary | strip_html | truncate: 60 }}</div>
                        {`%` elsif post.description `%`}
                        <div class="text-sm text-neutral-medium">{{ post.description | strip_html | truncate: 60 }}</div>
                        {`%` endif `%`}
                      </div>
                    </a>
                    {`%` endfor `%`}
                  </div>
                </div>
              </div>
            {`%`- else -`%`}
              {`%`- comment -`%`} Array of IDs - need to look them up using paginate {`%`- endcomment -`%`}
              {`%`- paginate posts by 1000 -`%`}
                {`%`- assign found_any = false -`%`}
                {`%`- comment -`%`} Quick check if any posts exist before rendering {`%`- endcomment -`%`}
                {`%`- for post_id in posts_list_setting -`%`}
                  {`%`- assign id_to_check = post_id.id | default: post_id -`%`}
                  {`%`- assign id_to_match = id_to_check | plus: 0 -`%`}
                  {`%`- for p in posts -`%`}
                    {`%`- if p.id == id_to_match or p.id == id_to_check or p.slug == id_to_check -`%`}
                      {`%`- assign found_any = true -`%`}
                      {`%`- break -`%`}
                    {`%`- endif -`%`}
                  {`%`- endfor -`%`}
                  {`%`- if found_any -`%`}{`%`- break -`%`}{`%`- endif -`%`}
                {`%`- endfor -`%`}
                
                {`%` if found_any `%`}
                <div class="schema-item mb-lg">
                  <label class="schema-label">Posts List Field:</label>
                  <div class="schema-value">
                    <div class="resource-grid">
                      {`%`- for post_id in posts_list_setting -`%`}
                        {`%`- assign id_to_check = post_id.id | default: post_id -`%`}
                        {`%`- assign id_to_match = id_to_check | plus: 0 -`%`}
                        {`%`- for p in posts -`%`}
                          {`%`- if p.id == id_to_match or p.id == id_to_check or p.slug == id_to_check -`%`}
                            <a href="{{ p.preview_url }}" class="resource-card">
                              {`%` assign image_url = '' `%`}
                              {`%` if p.image_url `%`}
                                {`%` assign image_url = p.image_url `%`}
                              {`%` elsif p.image `%`}
                                {`%` assign image_url = p.image | image_url `%`}
                              {`%` elsif p.images.size > 0 `%`}
                                {`%` assign image_url = p.images[0].src `%`}
                              {`%` endif `%`}
                              {`%` if image_url != '' `%`}
                              <img src="{{ image_url }}" alt="{{ p.title }}" class="resource-image">
                              {`%` else `%`}
                              <div class="resource-image-placeholder">
                                <span class="text-neutral-medium">No image</span>
                              </div>
                              {`%` endif `%`}
                              <div class="resource-info">
                                <div class="font-semibold">{{ p.title }}</div>
                                {`%` if p.summary `%`}
                                <div class="text-sm text-neutral-medium">{{ p.summary | strip_html | truncate: 60 }}</div>
                                {`%` elsif p.description `%`}
                                <div class="text-sm text-neutral-medium">{{ p.description | strip_html | truncate: 60 }}</div>
                                {`%` endif `%`}
                              </div>
                            </a>
                            {`%`- break -`%`}
                          {`%`- endif -`%`}
                        {`%`- endfor -`%`}
                      {`%`- endfor -`%`}
                    </div>
                  </div>
                </div>
                {`%` endif `%`}
              {`%`- endpaginate -`%`}
            {`%`- endif -`%`}
          {`%`- endif -`%`}
        {`%` endif `%`}

      </div>
  
      <!-- SPECIAL TYPES -->
      <div class="schema-group mb-3xl">
        <h2 class="group-title mb-xl">Special Types</h2>
        
        <div class="schema-grid">
          <!-- Text Alignment -->
          {`%` if section.settings.text_alignment_field != blank `%`}
          <div class="schema-item">
            <label class="schema-label">Text Alignment Field:</label>
            <div class="schema-value" style="text-align: {{ section.settings.text_alignment_field }};">
              This text is aligned {{ section.settings.text_alignment_field }}
            </div>
          </div>
          {`%` endif `%`}
  
          <!-- Link List (Menu) -->
          {`%` if section.settings.link_list_field != blank and section.settings.link_list_field.menu_items.size > 0 `%`}
          <div class="schema-item">
            <label class="schema-label">Link List Field:</label>
            <div class="schema-value">
              <div class="link-list-menu">
                <div class="font-semibold mb-md">{{ section.settings.link_list_field.title }}</div>
                <ul class="link-list-items" style="list-style: none; padding: 0;">
                  {`%` for item in section.settings.link_list_field.menu_items `%`}
                  <li class="mb-sm">
                    <a href="{{ item.url }}" class="text-primary hover:underline">
                      {{ item.title }}
                    </a>
                    {`%` if item.sub_menu_items.size > 0 `%`}
                    <ul class="ml-lg mt-sm" style="list-style: none;">
                      {`%` for sub_item in item.sub_menu_items `%`}
                      <li class="mb-xs">
                        <a href="{{ sub_item.url }}" class="text-sm text-neutral-medium hover:underline">
                          {{ sub_item.title }}
                        </a>
                      </li>
                      {`%` endfor `%`}
                    </ul>
                    {`%` endif `%`}
                  </li>
                  {`%` endfor `%`}
                </ul>
              </div>
            </div>
          </div>
          {`%` endif `%`}
        </div>
      </div>
  
      <!-- BLOCKS DEMONSTRATION - Displayed separately by block type at bottom -->
     
      <div class="schema-group mb-3xl">
        <h2 class="group-title mb-xl">Blocks Demonstration</h2>
       
        <!-- Demo Blocks -->
        {`%` assign demo_blocks = section.blocks | where: "type", "demo_block" `%`}
        {`%` if demo_blocks.size > 0 `%`}
        <div class="block-type-group mb-2xl">
          <h3 class="block-type-title">Demo Blocks</h3>
          <div class="blocks-container">
            {`%` for block in demo_blocks `%`}
            <div class="block-item" {{ block.fluid_attributes }}>
              <div class="block-content">
                <h4 class="block-title">{{ block.settings.block_title | default: 'Demo Block' }}</h4>
                {`%` if block.settings.block_text != blank `%`}
                <p class="block-text">{{ block.settings.block_text }}</p>
                {`%` endif `%`}
                {`%` if block.settings.block_image != blank `%`}
                <img src="{{ block.settings.block_image | image_url: width: 400 }}" alt="{{ block.settings.block_title }}" class="block-image">
                {`%` endif `%`}
              </div>
            </div>
            {`%` endfor `%`}
          </div>
        </div>
        {`%` endif `%`}


      </div>
  
    </div>
  </section>
  

Schema Settings

{`%` schema `%`}
{
  "name": "All Schema Types",
  "tag": "section",
  "class": "all-schema-type-section",
  "settings": [
    {
      "type": "header",
      "content": "Section Heading"
    },
    {
      "type": "text",
      "id": "section_heading",
      "label": "Section Heading",
      "default": "All Schema Types Demonstration"
    },
    {
      "type": "textarea",
      "id": "section_subheading",
      "label": "Section Subheading",
      "default": "This section demonstrates all supported schema types in Fluid theme system"
    },
    {
      "type": "select",
      "id": "heading_font_size",
      "label": "Heading Font Size (Mobile)",
      "options": [
        { "value": "text-xl", "label": "Extra Large" },
        { "value": "text-2xl", "label": "2X Large" },
        { "value": "text-3xl", "label": "3X Large" },
        { "value": "text-4xl", "label": "4X Large" }
      ],
      "default": "text-3xl"
    },
    {
      "type": "select",
      "id": "heading_font_size_desktop",
      "label": "Heading Font Size (Desktop)",
      "options": [
        { "value": "lg:text-3xl", "label": "3X Large" },
        { "value": "lg:text-4xl", "label": "4X Large" },
        { "value": "lg:text-5xl", "label": "5X Large" },
        { "value": "lg:text-6xl", "label": "6X Large" }
      ],
      "default": "lg:text-5xl"
    },
    {
      "type": "select",
      "id": "heading_font_weight",
      "label": "Heading Font Weight",
      "options": [
        { "value": "font-normal", "label": "Normal" },
        { "value": "font-medium", "label": "Medium" },
        { "value": "font-semibold", "label": "Semibold" },
        { "value": "font-bold", "label": "Bold" }
      ],
      "default": "font-bold"
    },
    {
      "type": "select",
      "id": "heading_color",
      "label": "Heading Color",
      "options": [
        { "value": "text-primary", "label": "Primary" },
        { "value": "text-black", "label": "Black" },
        { "value": "text-neutral-dark", "label": "Neutral Dark" }
      ],
      "default": "text-black"
    },
    {
      "type": "select",
      "id": "subheading_font_size",
      "label": "Subheading Font Size",
      "options": [
        { "value": "text-sm", "label": "Small" },
        { "value": "text-base", "label": "Base" },
        { "value": "text-lg", "label": "Large" }
      ],
      "default": "text-base"
    },
    {
      "type": "select",
      "id": "subheading_color",
      "label": "Subheading Color",
      "options": [
        { "value": "text-neutral-dark", "label": "Neutral Dark" },
        { "value": "text-neutral", "label": "Neutral" },
        { "value": "text-neutral-medium", "label": "Neutral Medium" }
      ],
      "default": "text-neutral-dark"
    },
    {
      "type": "header",
      "content": "Text Input Types"
    },
    {
      "type": "text",
      "id": "text_field",
      "label": "Text Field",
      "default": "This is a text field example",
      "info": "Single-line text input"
    },
    {
      "type": "textarea",
      "id": "textarea_field",
      "label": "Textarea Field",
      "default": "This is a textarea field example.\nIt supports multiple lines of plain text.",
      "info": "Multi-line plain text input"
    },
    {
      "type": "richtext",
      "id": "richtext_field",
      "label": "Richtext Field",
      "default": "<p>This is a <strong>richtext</strong> field example with <em>formatting</em> support.</p><ul><li>List item 1</li><li>List item 2</li></ul>",
      "info": "Rich text editor with formatting options"
    },
    {
      "type": "html",
      "id": "html_field",
      "label": "HTML Field",
      "default": "<div style='padding: 16px; background: #f0f0f0; border-radius: 4px;'><p>This is raw HTML content</p></div>",
      "info": "Raw HTML input for custom code"
    },
    {
      "type": "url",
      "id": "url_field",
      "label": "URL Field",
      "default": "https://example.com",
      "info": "URL input with validation"
    },
    {
      "type": "header",
      "content": "Number & Selection Types"
    },
    {
      "type": "range",
      "id": "range_field",
      "label": "Range Field",
      "min": 0,
      "max": 100,
      "step": 5,
      "default": 50,
      "unit": "px",
      "info": "Slider for numeric values"
    },
    {
      "type": "select",
      "id": "select_field",
      "label": "Select Field",
      "options": [
        { "value": "option1", "label": "Option 1" },
        { "value": "option2", "label": "Option 2" },
        { "value": "option3", "label": "Option 3" }
      ],
      "default": "option1",
      "info": "Dropdown menu selection"
    },
    {
      "type": "radio",
      "id": "radio_field",
      "label": "Radio Field",
      "options": [
        { "value": "radio1", "label": "Radio Option 1" },
        { "value": "radio2", "label": "Radio Option 2" },
        { "value": "radio3", "label": "Radio Option 3" }
      ],
      "default": "radio1",
      "info": "Radio button selection"
    },
    {
      "type": "checkbox",
      "id": "checkbox_field",
      "label": "Checkbox Field",
      "default": false,
      "info": "Toggle on/off boolean setting"
    },
    {
      "type": "checkbox",
      "id": "show_checkbox_field",
      "label": "Show Checkbox Field",
      "default": true,
      "info": "Toggle to show/hide the checkbox field display"
    },
    {
      "type": "header",
      "content": "Visual & Media Types"
    },
    {
      "type": "color",
      "id": "color_field",
      "label": "Color Field",
      "default": "#49473e",
      "info": "Color picker for simple colors"
    },
    {
      "type": "color_background",
      "id": "color_background_field",
      "label": "Color Background Field",
      "default": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
      "info": "Color picker with gradient support"
    },
    {
      "type": "font_picker",
      "id": "font_picker_field",
      "label": "Font Picker Field",
      "default": "helvetica_n4",
      "info": "Font family selector"
    },
    {
      "type": "image_picker",
      "id": "image_picker_field",
      "label": "Image Picker Field",
      "info": "Image upload/select"
    },
    {
      "type": "text",
      "id": "image_alt_text",
      "label": "Image Alt Text",
      "default": "Schema demonstration image"
    },
    {
      "type": "video_picker",
      "id": "video_picker_field",
      "label": "Video Picker Field",
      "info": "Video upload/select"
    },
    {
      "type": "image_picker",
      "id": "video_poster",
      "label": "Video Poster Image",
      "info": "Poster/thumbnail image for video"
    },
    {
      "type": "header",
      "content": "Resource Selectors (Single)"
    },
    {
      "type": "product",
      "id": "product_field",
      "label": "Product Field",
      "info": "Select a single product"
    },
    {
      "type": "collection",
      "id": "collection_field",
      "label": "Collection Field",
      "info": "Select a single collection"
    },
    {
      "type": "category",
      "id": "category_field",
      "label": "Category Field",
      "info": "Select a single category"
    },
    {
      "type": "posts",
      "id": "post_field",
      "label": "Post Field",
      "info": "Select a single blog post"
    },
    {
      "type": "enrollment_pack",
      "id": "enrollment_pack_field",
      "label": "Enrollment Pack Field",
      "info": "Select a single enrollment pack"
    },
    {
      "type": "enrollment",
      "id": "enrollment_field",
      "label": "Enrollment Field",
      "info": "Select a single enrollment"
    },
    {
      "type": "header",
      "content": "Resource Selectors (Multiple Lists)"
    },
    {
      "type": "product_list",
      "id": "product_list_field",
      "label": "Product List Field",
      "limit": 8,
      "info": "Select multiple products (up to 8)"
    },
    {
      "type": "collections_list",
      "id": "collection_list_field",
      "label": "Collection List Field",
      "limit": 6,
      "info": "Select multiple collections (up to 6)"
    },
    {
      "type": "category_list",
      "id": "category_list_field",
      "label": "Category List Field",
      "limit": 8,
      "info": "Select multiple categories (up to 8)"
    },
    {
      "type": "posts_list",
      "id": "posts_list_field",
      "label": "Posts List Field",
      "limit": 6,
      "info": "Select multiple blog posts (up to 6)"
    },
    {
      "type": "header",
      "content": "Special Types"
    },
    {
      "type": "text_alignment",
      "id": "text_alignment_field",
      "label": "Text Alignment Field",
      "default": "left",
      "info": "Text alignment picker (left/center/right)"
    },
    {
      "type": "link_list",
      "id": "link_list_field",
      "label": "Link List Field",
      "default": "main-menu",
      "info": "Select a menu/navigation list"
    },
    {
      "type": "header",
      "content": "Layout Settings"
    },
    {
      "type": "select",
      "id": "background_color",
      "label": "Section Background Color",
      "options": [
        { "value": "bg-primary", "label": "Primary" },
        { "value": "bg-secondary", "label": "Secondary" },
        { "value": "bg-secondary-light", "label": "Secondary Light" },
        { "value": "bg-accent-warm", "label": "Accent Warm" },
        { "value": "bg-banner", "label": "Banner" },
        { "value": "bg-success", "label": "Success" },
        { "value": "bg-error", "label": "Error" },
        { "value": "bg-white", "label": "White" },
        { "value": "bg-black", "label": "Black" },
        { "value": "bg-neutral", "label": "Neutral" },
        { "value": "bg-neutral-light", "label": "Neutral Light" },
        { "value": "bg-neutral-dark", "label": "Neutral Dark" },
        { "value": "bg-page", "label": "Page Background" }
      ],
      "default": "bg-white"
    }
  ],
  "blocks": [
    {
      "type": "demo_block",
      "name": "Demo Block",
      "limit": 12,
      "settings": [
        {
          "type": "text",
          "id": "block_title",
          "label": "Block Title",
          "default": "Demo Block Title"
        },
        {
          "type": "textarea",
          "id": "block_text",
          "label": "Block Text",
          "default": "This is a demo block demonstrating block functionality in Fluid schema system."
        },
        {
          "type": "image_picker",
          "id": "block_image",
          "label": "Block Image"
        }
      ]
    }
  ],
  "max_blocks": 12,
  "presets": [
    {
      "name": "All Schema Types",
      "settings": {
        "section_heading": "All Schema Types Demonstration",
        "section_subheading": "This section demonstrates all supported schema types in Fluid theme system",
        "text_field": "Example text field",
        "textarea_field": "Example textarea field with multiple lines",
        "checkbox_field": true,
        "range_field": 50,
        "select_field": "option1",
        "radio_field": "radio1",
        "color_field": "#49473e",
        "text_alignment_field": "left"
      },
      "blocks": [
        {
          "type": "demo_block",
          "settings": {
            "block_title": "Example Demo Block",
            "block_text": "This is a demo block demonstrating block functionality"
          }
        }
      ]
    }
  ]
}
{`%` endschema `%`}

This documentation is based on real production implementations from the YoliOne, Base, Fluid themed and represents current best practices for Fluid theme development.