Themes System Architecture
Table of Contents
Overview
The Themes System provides a flexible, multi-layered architecture for customizing the appearance and behavior of pages across the platform. It separates theme management from template content, enabling companies to customize their user-facing pages through a combination of theme selection, template customization, and variable configuration. The system supports both traditional Liquid templating and modern JSON-based section components.
Core Concepts
Themes vs Application Themes
The system maintains two distinct theme models representing different generations of theming functionality:
Legacy Theme Model (Theme):
- Simple theme selection system with predefined options
- Used for basic theme switching in older parts of the system
- Contains hardcoded theme names like
fashion,cosmetics,minimalistic, etc. - Primarily used by
UserCompanyassociations for individual user theme preferences
Modern Application Theme Model (ApplicationTheme):
- Full-featured theme system with template management, versioning, and customization
- Supports company-specific theme customization and template overrides
- Includes CSS variable injection, custom stylesheets, and rich media support
- Primary system for all current theme functionality
Application Themes
Application Themes represent complete theme packages that define the visual appearance and layout structure for a company's pages. Each theme can exist as either a root theme (shared across the platform) or a company-specific theme (customized for individual companies).
Key characteristics:
- Themes contain collections of templates for different page types
- Support CSS variable injection for dynamic styling
- Include versioning and publishing workflows
- Can be cloned and customized per company
- Maintain audit trails for change tracking
Theme Hierarchy:
- Root Themes: Platform-provided base themes (
fluid,vox,base) that serve as templates - Company Themes: Company-specific themes that clone and customize root themes
- Template Inheritance: Company themes can override specific templates while inheriting others
Root Theme Definition: A theme becomes a "root theme" when it has company_id: nil in the database. Root themes are platform-wide resources that:
- Are created and maintained by the platform administrators
- Serve as the foundation for all company-specific theme customization
- Cannot be directly modified by individual companies
- Provide the base templates and styling that companies clone and customize
- Are accessible to all companies for cloning purposes
- Include complete sets of templates for all major page types (product, home_page, etc.)
Application Theme Templates
Application Theme Templates are individual page templates within a theme that define the layout and content structure for specific page types. Each template can render different types of content and supports both Liquid and JSON formats.
Key characteristics:
- Templates belong to specific themes and handle particular content types
- Support both Liquid templating and JSON-based section systems
- Include schema definitions for dynamic content configuration
- Can be assigned to specific content items (products, media, pages)
- Maintain status tracking (draft/active) and versioning
Template Types:
- Content Templates:
product,medium,enrollment_pack,library- Render specific content items - Page Templates:
home_page,shop_page,cart_page,category_page,collection_page,join_page- Handle page types - Layout Components:
navbar,footer,layouts- Provide page structure and navigation - Structural Components:
sections,components,config- Reusable template parts and configuration
Template Processing System
The system processes templates through a sophisticated pipeline that combines content resolution, variable injection, and rendering:
Liquid Template Processing:
- Traditional server-side templating using Liquid syntax
- Variables injected as
{{ variable_name }}expressions - Control flow through
{% tag %}blocks - Support for layouts, includes, and custom filters
Custom Liquid Filters: The system provides specialized filters for common theming tasks:
{{ product.price | money }}- Formats currency values with proper symbols and localization{{ image.url | img_url: '300x200' }}- Resizes images to specified dimensions via ImageKit{{ 'common.add_to_cart' | t }}- Translates text based on current locale{{ product.created_at | date: '%B %d, %Y' }}- Formats dates with custom patterns{{ content | truncate: 150 }}- Truncates text to specified character length{{ product.tags | join: ', ' }}- Joins array elements with specified separator
JSON Section Processing: JSON templates represent a modern, component-based approach that separates content structure from presentation logic:
- Schema-Driven Configuration: Templates define sections with JSON schemas that specify three key components:
- Settings: Individual configuration options that control section appearance and behavior (colors, text, toggles, etc.)
- Blocks: Repeatable content elements within a section that can be added, removed, and reordered dynamically
- Presets: Pre-configured combinations of settings and blocks that provide starting templates for common use cases
- Component-Based Architecture: Each section type corresponds to a reusable Liquid component template
- Dynamic Content Blocks: Sections can contain configurable blocks that are rendered with context-specific data
- Visual Page Builder Integration: JSON structure enables drag-and-drop page building interfaces
- Settings Management: Schema definitions automatically generate form interfaces for template customization
JSON Template Structure:
{ "layout": "theme", "sections": { "header": { "type": "product_header", "settings": { "show_vendor": true, "show_price": true, "title_size": "large" } }, "gallery": { "type": "product_gallery", "settings": { "gallery_type": "slideshow", "show_thumbnails": true }, "blocks": { "block_1": { "type": "image_slide", "settings": { "image": "{{ product.featured_image }}", "caption": "Main product image" } } }, "block_order": ["block_1"] } }, "order": ["header", "gallery"] }
Section Processing Flow:
- Section Resolution: Each section
typemaps to a Liquid template in thesectionsthemeable_type - Settings Injection: Section settings are merged with schema defaults and injected as variables
- Block Processing: Dynamic blocks are processed with their individual settings and rendered within sections
- Schema Validation: Settings are validated against the section's schema definition
- Template Rendering: The resolved section template is rendered with populated variables
Understanding Schema Components: Settings, Blocks, and Presets
Settings: Individual Configuration Options Settings are individual form fields that control specific aspects of a section's appearance or behavior. Each setting has a type, default value, and user-facing label.
{ "name": "Product Gallery", "settings": [ { "type": "select", "id": "gallery_layout", "label": "Gallery Layout", "options": [ { "value": "grid", "label": "Grid" }, { "value": "carousel", "label": "Carousel" }, { "value": "stacked", "label": "Stacked" } ], "default": "grid" }, { "type": "checkbox", "id": "show_thumbnails", "label": "Show thumbnail navigation", "default": true }, { "type": "color", "id": "border_color", "label": "Border color", "default": "#e0e0e0" }, { "type": "text", "id": "heading_text", "label": "Section heading", "default": "Product Images" }, { "type": "range", "id": "columns_count", "label": "Columns per row", "min": 2, "max": 6, "step": 1, "default": 4 } ] }
Common Setting Types:
text: Single-line text inputtextarea: Multi-line text inputselect: Dropdown with predefined optionscheckbox: Boolean togglecolor: Color pickerrange: Slider with min/max valuesnumber: Numeric inputurl: URL input with validationimage_picker: Asset selection interface
Blocks: Repeatable Content Elements Blocks are dynamic content items that can be added, removed, and reordered within a section. Each block has its own settings and can represent different content types.
{ "name": "Testimonials Section", "settings": [ { "type": "text", "id": "section_title", "label": "Section Title", "default": "What Our Customers Say" } ], "blocks": [ { "type": "testimonial", "name": "Customer Testimonial", "limit": 10, "settings": [ { "type": "text", "id": "customer_name", "label": "Customer Name" }, { "type": "textarea", "id": "testimonial_text", "label": "Testimonial" }, { "type": "image_picker", "id": "customer_photo", "label": "Customer Photo" }, { "type": "text", "id": "customer_title", "label": "Customer Title/Company" }, { "type": "range", "id": "star_rating", "label": "Star Rating", "min": 1, "max": 5, "default": 5 } ] }, { "type": "divider", "name": "Section Divider", "settings": [ { "type": "select", "id": "divider_style", "label": "Divider Style", "options": [ { "value": "line", "label": "Line" }, { "value": "dots", "label": "Dots" }, { "value": "stars", "label": "Stars" } ], "default": "line" } ] } ] }
Block Characteristics:
- Dynamic Quantity: Users can add/remove blocks as needed
- Reorderable: Blocks can be dragged to change their sequence
- Type-Specific: Different block types have different settings schemas
- Limited: Optional
limitproperty restricts maximum number of blocks
Presets: Pre-Configured Starting Points Presets provide ready-to-use configurations that combine section settings with a predefined set of blocks, giving users a starting point for common use cases.
{ "name": "Feature Showcase", "settings": [ { "type": "text", "id": "section_heading", "label": "Section Heading", "default": "Key Features" } ], "blocks": [ { "type": "feature_item", "name": "Feature", "settings": [ { "type": "text", "id": "feature_title", "label": "Feature Title" }, { "type": "textarea", "id": "feature_description", "label": "Feature Description" }, { "type": "image_picker", "id": "feature_icon", "label": "Feature Icon" } ] } ], "presets": [ { "name": "Basic Features (3 items)", "settings": { "section_heading": "Why Choose Us" }, "blocks": [ { "type": "feature_item", "settings": { "feature_title": "Fast Delivery", "feature_description": "Get your order in 24 hours or less" } }, { "type": "feature_item", "settings": { "feature_title": "Quality Guarantee", "feature_description": "100% satisfaction or your money back" } }, { "type": "feature_item", "settings": { "feature_title": "Expert Support", "feature_description": "24/7 customer service from our team" } } ] }, { "name": "Product Benefits (5 items)", "settings": { "section_heading": "Product Benefits" }, "blocks": [ { "type": "feature_item", "settings": { "feature_title": "Eco-Friendly", "feature_description": "Made from sustainable materials" } }, { "type": "feature_item", "settings": { "feature_title": "Durable Design", "feature_description": "Built to last for years of use" } }, { "type": "feature_item", "settings": { "feature_title": "Easy to Use", "feature_description": "Simple setup in under 5 minutes" } }, { "type": "feature_item", "settings": { "feature_title": "Versatile", "feature_description": "Works in multiple environments" } }, { "type": "feature_item", "settings": { "feature_title": "Cost Effective", "feature_description": "Saves money compared to alternatives" } } ] } ] }
Preset Benefits:
- Quick Setup: Users can start with pre-configured content instead of building from scratch
- Best Practices: Presets demonstrate optimal configurations for common scenarios
- Inspiration: Show users what's possible with the section
- Consistency: Ensure common use cases follow established patterns
How They Work Together in Practice:
- User Selects Preset: When adding a section, user chooses "Product Benefits (5 items)" preset
- System Applies Configuration: Section gets the preset's settings and 5 pre-configured feature blocks
- User Customizes Settings: User modifies section heading, colors, layout options through the settings
- User Manages Blocks: User can edit individual features, reorder them, add more, or remove some
- Final Rendering: Section template renders with the customized settings and blocks
This three-tier system (settings + blocks + presets) provides maximum flexibility while maintaining usability for non-technical users.
Layout Resolution:
- Templates can specify layout wrappers using
{% layout 'theme' %}tags - Layouts are separate templates with
themeable_type: "layouts" - Content injection through
{{ content_for_layout }}variable - Supports nested layouts and template inheritance
Variable System
The themes system includes a comprehensive variable injection system that populates templates with dynamic content:
Variable Categories:
- Global Variables: Company information, site settings, localization data
- Content Variables: Product details, page content, media information
- Request Variables: URL paths, query parameters, user session data
- Theme Variables: CSS custom properties, color schemes, typography settings
Variable Processing:
- Variables are resolved by specialized service classes in
app/services/themes/templates/variables/ - Different content types have dedicated variable providers
- Caching mechanisms prevent N+1 queries and improve performance
- Variables support both simple values and complex objects (drops)
System Architecture
Theme-Company Relationships
Companies can have multiple themes but only one active theme at a time:
Template Hierarchy and Inheritance
Templates follow a hierarchical resolution system:
File and Asset Management
Theme assets are managed through the broader file resource system:
ApplicationTheme ├── has_many :images (cover images, theme previews) ├── has_many :file_resources (CSS files, JavaScript, fonts) └── has_many :ordered_images (positioned theme gallery images) ApplicationThemeTemplate ├── has_many :file_references -> FileResource (template-specific assets) ├── has_many :images (template preview images) └── content field contains template code (Liquid/JSON)
Request Flow Architecture
Step 1: Request Routing and Controller Setup
Request Entry Points:
PublicController- Handles company homepage, join pages, privacy policySharesController- Handles content pages (products, media, libraries, custom pages)
Controller Processing: Both controllers include the Themeable concern which provides theme resolution functionality:
set_theme_template_and_variables()- Main orchestration methodtheme_template_for()- Template resolution logictheme_template_variables_for()- Variable population
Step 2: Theme and Template Resolution
Theme Resolution:
# In Themeable concern def current_theme_for_company @current_theme ||= @company&.current_theme || default_system_theme end def theme_template_for(content_type, content_item = nil) # 1. Check URL parameters for template override return find_template_by_id(params[:template_id]) if params[:template_id] # 2. Check content-specific template assignment return content_item.application_theme_template if content_item&.application_theme_template # 3. Find default template for content type in company's active theme ApplicationThemeTemplate.default_for_company_themes( company: @company, themeable_type: content_type ).first end
Template Selection Logic:
- Templates are resolved based on
themeable_type(product, medium, home_page, etc.) - Default templates are marked with
default: truewithin each theme - Only
activetemplates are considered for rendering - Falls back to root theme defaults if company theme lacks specific templates
Step 3: Variable Population and Context Building
Variable Service Orchestration:
# Variables are created by specialized service classes def theme_template_variables_for(template, content_item = nil) Themes::Templates::Variable.new( template: template, request: request, company: @company, record: content_item, params: params ).call end
Variable Categories and Sources:
- Base Variables: URLs, paths, request info, company details
- Content Variables: Product data, page content, media info (based on content_item)
- Global Variables: Site settings, localization, social media links
- Theme Variables: CSS custom properties, layout settings from theme configuration
Step 4: Template Rendering Pipeline
Rendering Orchestration:
# PageBuilder handles the core rendering logic def render_template(template, variables, layout = nil) if template.json? render_json_template(template, variables) else render_liquid_template(template, variables) end end
Liquid Template Processing:
- Templates processed through
LiquidTemplate.parse(template.content, variables) - Variables accessible as
{{ variable_name }}in templates - Support for control flow:
{% if condition %},{% for item in collection %} - Custom filters available for formatting, localization, and image processing
- Layout injection via
{{ content_for_layout }}if layout specified
JSON Template Processing: JSON templates use a sophisticated section-based rendering system that separates content structure from presentation:
- Section-by-Section Rendering: The JSON structure defines discrete sections that are processed and rendered individually
- Schema-Based Configuration: Each section type has a corresponding schema that defines available settings, blocks, and presets
- Dynamic Block Management: Sections can contain configurable blocks with their own settings and rendering logic
- Settings Inheritance: Block settings inherit defaults from their section schema and can be overridden per instance
- Fluid Attributes: Blocks receive special
fluid_attributesfor frontend editing integration (section IDs, block types, etc.)
Processing Steps:
- JSON Parsing: Template content is parsed as JSON structure
- Section Iteration: Each section in the
sectionsobject is processed according to theorderarray - Schema Loading: Section schema is loaded from corresponding
sectionsthemeable_type templates - Settings Merging: Section settings are merged with schema defaults
- Block Processing: Dynamic blocks are processed with individual settings and fluid attributes
- Template Resolution: Each section type maps to a Liquid template that renders the final HTML
- Variable Injection: Processed settings become available as
{{ section.settings.setting_name }}variables
Step 5: Layout Resolution and Content Wrapping
Layout Processing:
def resolve_theme_layout(template) return nil unless template.templateable_type? layout_name = template.layout_name || 'theme' theme = template.theme theme&.application_theme_templates&.find_by( name: layout_name, themeable_type: 'layouts' )&.published end
Layout Integration:
- Templates can specify layouts via
{% layout 'theme' %}tags or JSON configuration - Layout templates wrap content using
{{ content_for_layout }}variable - Supports nested layouts and template inheritance
- Layout resolution respects theme hierarchy (company themes inherit root layouts)
Step 6: Stylesheet and Asset Integration
CSS Processing:
# ApplicationTheme methods for stylesheet processing def global_stylesheet_with_variables cache_key = "theme_stylesheet:#{id}:global:#{cache_key_suffix}" Rails.cache.fetch(cache_key, expires_in: 1.hour) do stylesheet_with_variables(global_stylesheet) end end def stylesheet_with_variables(stylesheet) variables = safe_parse_json(self.variables) variables.each { |key, value| stylesheet.gsub!("$#{key}", value) } stylesheet end
Asset Integration:
- Global stylesheets with CSS variable substitution
- Custom stylesheets for theme-specific overrides
- File resources (fonts, images, scripts) linked to themes and templates
- Caching with variable-based cache invalidation
Implementation Details
Data Model Structure
-- ApplicationTheme: Main theme container CREATE TABLE application_themes ( id SERIAL PRIMARY KEY, company_id INTEGER REFERENCES companies(id), -- NULL for root themes name VARCHAR NOT NULL, description TEXT, variables TEXT, -- JSON string with CSS variables custom_stylesheet TEXT, global_stylesheet TEXT, version VARCHAR DEFAULT '1.0', status INTEGER DEFAULT 0, -- 0: draft, 1: active created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); -- Only one active theme per company CREATE UNIQUE INDEX idx_app_themes_company_active ON application_themes (company_id) WHERE status = 1; -- ApplicationThemeTemplate: Individual templates within themes CREATE TABLE application_theme_templates ( id SERIAL PRIMARY KEY, company_id INTEGER REFERENCES companies(id), application_theme_id INTEGER REFERENCES application_themes(id), parent_id INTEGER REFERENCES application_theme_templates(id), -- For nested templates name VARCHAR NOT NULL, themeable_type INTEGER NOT NULL, -- Enum: product, medium, home_page, etc. content TEXT, -- Template code (Liquid or JSON) head TEXT, -- HTML head content stylesheet TEXT, -- Template-specific CSS format VARCHAR DEFAULT 'liquid', -- 'liquid' or 'json' status INTEGER DEFAULT 0, -- 0: draft, 1: active applicable INTEGER DEFAULT 0, -- 0: everything, 1: specific "default" BOOLEAN DEFAULT FALSE, -- Default template for this themeable_type fluid BOOLEAN DEFAULT FALSE, -- System template flag created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); -- Index for finding default templates by company and type CREATE INDEX idx_app_theme_templates_theme_company_type_default ON application_theme_templates (application_theme_id, company_id, themeable_type, "default") WHERE status = 1; -- Legacy Theme model (maintained for backwards compatibility) CREATE TABLE themes ( id SERIAL PRIMARY KEY, company_id INTEGER REFERENCES companies(id), application_theme_template_id INTEGER REFERENCES application_theme_templates(id), name VARCHAR NOT NULL, public BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() );
Key Schema Features:
- Company Isolation: Templates and themes are scoped by
company_id - Root Theme Support: Root themes have
company_id: NULLand are shared across the platform - Template Hierarchy: Templates can have parent-child relationships via
parent_id - Status Management: Draft/active status for both themes and templates
- Default Template Resolution: Index optimized for finding default templates by type
- Format Flexibility: Templates can be either Liquid or JSON format
Themeable Types Enumeration
# ApplicationThemeTemplate themeable_type enum enum :themeable_type, { product: 0, # Individual product pages medium: 1, # Media post pages enrollment_pack: 2, # Course/enrollment pages shop_page: 3, # Shop/catalog pages navbar: 4, # Navigation header library: 5, # Library/playlist pages page: 6, # Custom pages components: 7, # Reusable components library_navbar: 8, # Library-specific navigation sections: 9, # Template sections locales: 10, # Localization templates footer: 11, # Page footer layouts: 12, # Page wrapper layouts category_page: 13, # Category browsing collection_page: 14, # Collection browsing cart_page: 15, # Shopping cart config: 16, # Theme configuration home_page: 17, # Homepage layout mysite: 18, # MySite pages join_page: 19 # Registration pages }
Caching Architecture
Multi-Level Caching Strategy:
Stylesheet Caching (1 hour expiration):
cache_key = "theme_stylesheet:#{theme_id}:global:#{updated_at.to_i}:#{variables_hash}" Rails.cache.fetch(cache_key, expires_in: 1.hour) do process_stylesheet_with_variables(stylesheet) end
Global Embeds Caching (1 hour / 5 seconds in development):
cache_key = "global_embeds:#{company_id}:#{global_embeds_updated_at}" Rails.cache.fetch(cache_key, expires_in: cache_duration) do fetch_and_process_global_embeds end
Variable Optimization:
- Language and country data memoized to prevent N+1 queries
- Base URL resolution cached per request
- Template resolution cached within request scope
Cache Invalidation:
- Stylesheet cache invalidated when theme variables or CSS content changes
- Global embeds cache invalidated when company embed settings change
- Theme template cache invalidated on publish/unpublish actions
Publishing and Versioning System
Publishing Workflow:
# ApplicationTheme publishing def publish update!(status: :active) application_theme_templates.each(&:publish) # Publish all templates end # ApplicationThemeTemplate publishing def make_default ApplicationRecord.transaction do # Set this as the active default template active! update!(default: true) # Deactivate other default templates of same type application_theme.application_theme_templates .where(themeable_type: themeable_type) .where.not(id: id) .update_all(default: false) end end
Version Management:
- Themes use semantic versioning (major.minor format)
- Minor versions automatically increment on template changes
- Major versions increment on significant theme updates
- Audit trail maintains complete change history
- Auto-upgrade system for compatible theme versions
Template Development Patterns
Liquid Template Structure
Basic Template Format:
Product template with layout, filters, and sections <article class="product-page"> <header class="product-header"> <h1>{{ product.name }}</h1> <div class="product-price"> {{ product.price | money }} </div> <div class="product-meta"> <span class="product-date">{{ product.created_at | date: '%B %d, %Y' }}</span> <div class="product-tags">{{ product.tags | join: ', ' }}</div> </div> </header> <div class="product-content"> <div class="product-images"> <img src="{{ image.url | img_url: '400x300' }}" alt="{{ image.alt }}" /> </div> <div class="product-description"> {{ product.description | truncate: 250 }} </div> </div> <div class="product-purchase"> <button class="btn-purchase" data-product-id="{{ product.id }}"> {{ 'products.add_to_cart' | t }} </button> </div> </article>