Files
orion/docs/architecture/theme-system/overview.md

17 KiB

Multi-Theme Shop System - Complete Implementation Guide

🎨 Overview

This guide explains how to implement vendor-specific themes in your FastAPI multi-tenant e-commerce platform, allowing each vendor to have their own unique shop design, colors, branding, and layout.

What You're Building

Before:

  • All vendor shops look the same
  • Same colors, fonts, layouts
  • Only vendor name changes

After:

  • Each vendor has unique theme
  • Custom colors, fonts, logos
  • Different layouts per vendor
  • Vendor-specific branding
  • CSS customization support

Architecture Overview

Request → Vendor Middleware → Theme Middleware → Template Rendering
                ↓                    ↓                   ↓
         Sets vendor         Loads theme          Applies styles
         in request          config for           and branding
         state               vendor

Data Flow

1. Customer visits: customdomain1.com
2. Vendor middleware: Identifies Vendor 1
3. Theme middleware: Loads Vendor 1's theme
4. Template receives:
   - vendor: Vendor 1 object
   - theme: Vendor 1 theme config
5. Template renders with:
   - Vendor 1 colors
   - Vendor 1 logo
   - Vendor 1 layout preferences
   - Vendor 1 custom CSS

Implementation Steps

Step 1: Add Theme Database Table

Create the vendor_themes table:

CREATE TABLE vendor_themes (
    id SERIAL PRIMARY KEY,
    vendor_id INTEGER UNIQUE NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,
    theme_name VARCHAR(100) DEFAULT 'default',
    is_active BOOLEAN DEFAULT TRUE,
    
    -- Colors (JSON)
    colors JSONB DEFAULT '{
        "primary": "#6366f1",
        "secondary": "#8b5cf6",
        "accent": "#ec4899",
        "background": "#ffffff",
        "text": "#1f2937",
        "border": "#e5e7eb"
    }'::jsonb,
    
    -- Typography
    font_family_heading VARCHAR(100) DEFAULT 'Inter, sans-serif',
    font_family_body VARCHAR(100) DEFAULT 'Inter, sans-serif',
    
    -- Branding
    logo_url VARCHAR(500),
    logo_dark_url VARCHAR(500),
    favicon_url VARCHAR(500),
    banner_url VARCHAR(500),
    
    -- Layout
    layout_style VARCHAR(50) DEFAULT 'grid',
    header_style VARCHAR(50) DEFAULT 'fixed',
    product_card_style VARCHAR(50) DEFAULT 'modern',
    
    -- Customization
    custom_css TEXT,
    social_links JSONB DEFAULT '{}'::jsonb,
    
    -- Meta
    meta_title_template VARCHAR(200),
    meta_description TEXT,
    
    -- Timestamps
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_vendor_themes_vendor_id ON vendor_themes(vendor_id);
CREATE INDEX idx_vendor_themes_active ON vendor_themes(vendor_id, is_active);

Step 2: Create VendorTheme Model

File: models/database/vendor_theme.py

See the complete model in /home/claude/vendor_theme_model.py

Key features:

  • JSON fields for flexible color schemes
  • Brand asset URLs (logo, favicon, banner)
  • Layout preferences
  • Custom CSS support
  • CSS variables generator
  • to_dict() for template rendering

Step 3: Update Vendor Model

Add theme relationship to models/database/vendor.py:

from sqlalchemy.orm import relationship

class Vendor(Base):
    # ... existing fields ...
    
    # Add theme relationship
    theme = relationship(
        "VendorTheme",
        back_populates="vendor",
        uselist=False,  # One-to-one relationship
        cascade="all, delete-orphan"
    )
    
    @property
    def active_theme(self):
        """Get vendor's active theme or return None"""
        if self.theme and self.theme.is_active:
            return self.theme
        return None

Step 4: Create Theme Context Middleware

File: middleware/theme_context.py

See complete middleware in /home/claude/theme_context_middleware.py

What it does:

  1. Runs AFTER vendor_context_middleware
  2. Loads theme for detected vendor
  3. Injects theme into request.state
  4. Falls back to default theme if needed

Add to main.py:

from middleware.theme_context import theme_context_middleware

# AFTER vendor_context_middleware
app.middleware("http")(theme_context_middleware)

Step 5: Create Shop Base Template

File: app/templates/shop/base.html

See complete template in /home/claude/shop_base_template.html

Key features:

  • Injects CSS variables from theme
  • Vendor-specific logo (light/dark mode)
  • Theme-aware header/footer
  • Social links from theme config
  • Custom CSS injection
  • Dynamic favicon
  • SEO meta tags

Template receives:

{
    "vendor": vendor_object,    # From vendor middleware
    "theme": theme_dict,         # From theme middleware
}

Step 6: Create Shop Layout JavaScript

File: static/shop/js/shop-layout.js

See complete code in /home/claude/shop_layout.js

Provides:

  • Theme toggling (light/dark)
  • Cart management
  • Mobile menu
  • Search overlay
  • Toast notifications
  • Price formatting
  • Date formatting

Step 7: Update Route Handlers

Ensure theme is passed to templates:

from middleware.theme_context import get_current_theme

@router.get("/")
async def shop_home(request: Request, db: Session = Depends(get_db)):
    vendor = request.state.vendor
    theme = get_current_theme(request)  # or request.state.theme
    
    # Get products for vendor
    products = db.query(Product).filter(
        Product.vendor_id == vendor.id,
        Product.is_active == True
    ).all()
    
    return templates.TemplateResponse("shop/home.html", {
        "request": request,
        "vendor": vendor,
        "theme": theme,
        "products": products
    })

Note: If middleware is set up correctly, theme is already in request.state.theme, so you may not need to explicitly pass it!

How Themes Work

CSS Variables System

Each theme generates CSS custom properties:

:root {
    --color-primary: #6366f1;
    --color-secondary: #8b5cf6;
    --color-accent: #ec4899;
    --color-background: #ffffff;
    --color-text: #1f2937;
    --color-border: #e5e7eb;
    --font-heading: Inter, sans-serif;
    --font-body: Inter, sans-serif;
}

Usage in HTML/CSS:

<!-- In templates -->
<button style="background-color: var(--color-primary)">
    Click Me
</button>

<h1 style="font-family: var(--font-heading)">
    Welcome
</h1>
/* In stylesheets */
.btn-primary {
    background-color: var(--color-primary);
    color: var(--color-background);
}

.heading {
    font-family: var(--font-heading);
    color: var(--color-text);
}

Theme Configuration Example

# Example theme for "Modern Electronics Store"
theme = {
    "theme_name": "tech-modern",
    "colors": {
        "primary": "#2563eb",     # Blue
        "secondary": "#0ea5e9",   # Sky Blue
        "accent": "#f59e0b",      # Amber
        "background": "#ffffff",
        "text": "#111827",
        "border": "#e5e7eb"
    },
    "fonts": {
        "heading": "Roboto, sans-serif",
        "body": "Open Sans, sans-serif"
    },
    "branding": {
        "logo": "/media/vendors/tech-store/logo.png",
        "logo_dark": "/media/vendors/tech-store/logo-dark.png",
        "favicon": "/media/vendors/tech-store/favicon.ico",
        "banner": "/media/vendors/tech-store/banner.jpg"
    },
    "layout": {
        "style": "grid",
        "header": "fixed",
        "product_card": "modern"
    },
    "social_links": {
        "facebook": "https://facebook.com/techstore",
        "instagram": "https://instagram.com/techstore",
        "twitter": "https://twitter.com/techstore"
    },
    "custom_css": """
        .product-card {
            border-radius: 12px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }
        
        .product-card:hover {
            transform: translateY(-4px);
            box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
        }
    """
}

Creating Theme Presets

You can create predefined theme templates:

# app/core/theme_presets.py

THEME_PRESETS = {
    "modern": {
        "colors": {
            "primary": "#6366f1",
            "secondary": "#8b5cf6",
            "accent": "#ec4899",
        },
        "fonts": {
            "heading": "Inter, sans-serif",
            "body": "Inter, sans-serif"
        },
        "layout": {
            "style": "grid",
            "header": "fixed"
        }
    },
    
    "classic": {
        "colors": {
            "primary": "#1e40af",
            "secondary": "#7c3aed",
            "accent": "#dc2626",
        },
        "fonts": {
            "heading": "Georgia, serif",
            "body": "Arial, sans-serif"
        },
        "layout": {
            "style": "list",
            "header": "static"
        }
    },
    
    "minimal": {
        "colors": {
            "primary": "#000000",
            "secondary": "#404040",
            "accent": "#666666",
        },
        "fonts": {
            "heading": "Helvetica, sans-serif",
            "body": "Helvetica, sans-serif"
        },
        "layout": {
            "style": "grid",
            "header": "transparent"
        }
    },
    
    "vibrant": {
        "colors": {
            "primary": "#f59e0b",
            "secondary": "#ef4444",
            "accent": "#8b5cf6",
        },
        "fonts": {
            "heading": "Poppins, sans-serif",
            "body": "Open Sans, sans-serif"
        },
        "layout": {
            "style": "masonry",
            "header": "fixed"
        }
    }
}

def apply_preset(theme: VendorTheme, preset_name: str):
    """Apply a preset to a vendor theme"""
    if preset_name not in THEME_PRESETS:
        raise ValueError(f"Unknown preset: {preset_name}")
    
    preset = THEME_PRESETS[preset_name]
    
    theme.theme_name = preset_name
    theme.colors = preset["colors"]
    theme.font_family_heading = preset["fonts"]["heading"]
    theme.font_family_body = preset["fonts"]["body"]
    theme.layout_style = preset["layout"]["style"]
    theme.header_style = preset["layout"]["header"]
    
    return theme

Admin Interface for Theme Management

Create admin endpoints for managing themes:

# app/api/v1/admin/vendor_themes.py

@router.get("/vendors/{vendor_id}/theme")
def get_vendor_theme(vendor_id: int, db: Session = Depends(get_db)):
    """Get theme configuration for vendor"""
    theme = db.query(VendorTheme).filter(
        VendorTheme.vendor_id == vendor_id
    ).first()
    
    if not theme:
        # Return default theme
        return get_default_theme()
    
    return theme.to_dict()


@router.put("/vendors/{vendor_id}/theme")
def update_vendor_theme(
    vendor_id: int,
    theme_data: dict,
    db: Session = Depends(get_db)
):
    """Update or create theme for vendor"""
    
    theme = db.query(VendorTheme).filter(
        VendorTheme.vendor_id == vendor_id
    ).first()
    
    if not theme:
        theme = VendorTheme(vendor_id=vendor_id)
        db.add(theme)
    
    # Update fields
    if "colors" in theme_data:
        theme.colors = theme_data["colors"]
    
    if "fonts" in theme_data:
        theme.font_family_heading = theme_data["fonts"].get("heading")
        theme.font_family_body = theme_data["fonts"].get("body")
    
    if "branding" in theme_data:
        theme.logo_url = theme_data["branding"].get("logo")
        theme.logo_dark_url = theme_data["branding"].get("logo_dark")
        theme.favicon_url = theme_data["branding"].get("favicon")
    
    if "layout" in theme_data:
        theme.layout_style = theme_data["layout"].get("style")
        theme.header_style = theme_data["layout"].get("header")
    
    if "custom_css" in theme_data:
        theme.custom_css = theme_data["custom_css"]
    
    db.commit()
    db.refresh(theme)
    
    return theme.to_dict()


@router.post("/vendors/{vendor_id}/theme/preset/{preset_name}")
def apply_theme_preset(
    vendor_id: int,
    preset_name: str,
    db: Session = Depends(get_db)
):
    """Apply a preset theme to vendor"""
    from app.core.theme_presets import apply_preset, THEME_PRESETS
    
    if preset_name not in THEME_PRESETS:
        raise HTTPException(400, f"Unknown preset: {preset_name}")
    
    theme = db.query(VendorTheme).filter(
        VendorTheme.vendor_id == vendor_id
    ).first()
    
    if not theme:
        theme = VendorTheme(vendor_id=vendor_id)
        db.add(theme)
    
    apply_preset(theme, preset_name)
    db.commit()
    db.refresh(theme)
    
    return {
        "message": f"Applied {preset_name} preset",
        "theme": theme.to_dict()
    }

Example: Different Themes for Different Vendors

Vendor 1: Tech Electronics Store

{
    "colors": {
        "primary": "#2563eb",  # Blue
        "secondary": "#0ea5e9",
        "accent": "#f59e0b"
    },
    "fonts": {
        "heading": "Roboto, sans-serif",
        "body": "Open Sans, sans-serif"
    },
    "layout": {
        "style": "grid",
        "header": "fixed"
    }
}

Vendor 2: Fashion Boutique

{
    "colors": {
        "primary": "#ec4899",  # Pink
        "secondary": "#f472b6",
        "accent": "#fbbf24"
    },
    "fonts": {
        "heading": "Playfair Display, serif",
        "body": "Lato, sans-serif"
    },
    "layout": {
        "style": "masonry",
        "header": "transparent"
    }
}

Vendor 3: Organic Food Store

{
    "colors": {
        "primary": "#10b981",  # Green
        "secondary": "#059669",
        "accent": "#f59e0b"
    },
    "fonts": {
        "heading": "Merriweather, serif",
        "body": "Source Sans Pro, sans-serif"
    },
    "layout": {
        "style": "list",
        "header": "static"
    }
}

Testing Themes

Test 1: View Different Vendor Themes

# Visit Vendor 1 (Tech store with blue theme)
curl http://vendor1.localhost:8000/

# Visit Vendor 2 (Fashion with pink theme)
curl http://vendor2.localhost:8000/

# Each should have different:
# - Colors in CSS variables
# - Logo
# - Fonts
# - Layout

Test 2: Theme API

# Get vendor theme
curl http://localhost:8000/api/v1/admin/vendors/1/theme

# Update colors
curl -X PUT http://localhost:8000/api/v1/admin/vendors/1/theme \
  -H "Content-Type: application/json" \
  -d '{
    "colors": {
      "primary": "#ff0000",
      "secondary": "#00ff00"
    }
  }'

# Apply preset
curl -X POST http://localhost:8000/api/v1/admin/vendors/1/theme/preset/modern

Benefits

For Platform Owner

  • Premium feature for enterprise vendors
  • Differentiate vendor packages (basic vs premium themes)
  • Additional revenue stream
  • Competitive advantage

For Vendors

  • Unique brand identity
  • Professional appearance
  • Better customer recognition
  • Customizable to match brand

For Customers

  • Distinct shopping experiences
  • Better brand recognition
  • More engaging designs
  • Professional appearance

Advanced Features

1. Theme Preview

Allow vendors to preview themes before applying:

@router.get("/vendors/{vendor_id}/theme/preview/{preset_name}")
def preview_theme(vendor_id: int, preset_name: str):
    """Generate preview URL for theme"""
    # Return preview HTML with preset applied
    pass

2. Theme Marketplace

Create a marketplace of premium themes:

class PremiumTheme(Base):
    __tablename__ = "premium_themes"
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    description = Column(Text)
    price = Column(Numeric(10, 2))
    preview_image = Column(String(500))
    config = Column(JSON)

3. Dark Mode Auto-Detection

Respect user's system preferences:

// Detect system dark mode preference
if (window.matchMedia && 
    window.matchMedia('(prefers-color-scheme: dark)').matches) {
    this.dark = true;
}

4. Theme Analytics

Track which themes perform best:

class ThemeAnalytics(Base):
    __tablename__ = "theme_analytics"
    
    theme_id = Column(Integer, ForeignKey("vendor_themes.id"))
    conversion_rate = Column(Numeric(5, 2))
    avg_session_duration = Column(Integer)
    bounce_rate = Column(Numeric(5, 2))

Summary

What you've built:

  • Vendor-specific theme system
  • CSS variables for dynamic styling
  • Custom branding (logos, colors, fonts)
  • Layout customization
  • Custom CSS support
  • Theme presets
  • Admin theme management

Each vendor now has:

  • Unique colors and fonts
  • Custom logo and branding
  • Layout preferences
  • Social media links
  • Custom CSS overrides

All controlled by:

  • Database configuration
  • No code changes needed per vendor
  • Admin panel management
  • Preview and testing

Your architecture supports this perfectly! The vendor context + theme middleware pattern works seamlessly with your existing Alpine.js frontend.

Start with the default theme, then let vendors customize their shops! 🎨