Files
orion/docs/architecture/theme-system/overview.md
Samir Boulahtit d648c921b7
Some checks failed
CI / ruff (push) Successful in 10s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
docs: add consolidated dev URL reference and migrate /shop to /storefront
- Add Development URL Quick Reference section to url-routing overview
  with all login URLs, entry points, and full examples
- Replace /shop/ path segments with /storefront/ across 50 docs files
- Update file references: shop_pages.py → storefront_pages.py,
  templates/shop/ → templates/storefront/, api/v1/shop/ → api/v1/storefront/
- Preserve domain references (orion.shop) and /store/ staff dashboard paths
- Archive docs left unchanged (historical)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 13:23:44 +01:00

16 KiB

Multi-Theme Shop System - Complete Implementation Guide

🎨 Overview

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

What You're Building

Before:

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

After:

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

Architecture Overview

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

Data Flow

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

Implementation Steps

Step 1: Add Theme Database Table

Create the store_themes table:

CREATE TABLE store_themes (
    id SERIAL PRIMARY KEY,
    store_id INTEGER UNIQUE NOT NULL REFERENCES stores(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_store_themes_store_id ON store_themes(store_id);
CREATE INDEX idx_store_themes_active ON store_themes(store_id, is_active);

Step 2: Create StoreTheme Model

File: models/database/store_theme.py

See the complete model in /home/claude/store_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 Store Model

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

from sqlalchemy.orm import relationship

class Store(Base):
    # ... existing fields ...

    # Add theme relationship
    theme = relationship(
        "StoreTheme",
        back_populates="store",
        uselist=False,  # One-to-one relationship
        cascade="all, delete-orphan"
    )

    @property
    def active_theme(self):
        """Get store'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 store_context_middleware
  2. Loads theme for detected store
  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 store_context_middleware
app.middleware("http")(theme_context_middleware)

Step 5: Create Shop Base Template

File: app/templates/storefront/base.html

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

Key features:

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

Template receives:

{
    "store": store_object,    # From store middleware
    "theme": theme_dict,         # From theme middleware
}

Step 6: Create Shop Layout JavaScript

File: static/storefront/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)):
    store = request.state.store
    theme = get_current_theme(request)  # or request.state.theme

    # Get products for store
    products = db.query(Product).filter(
        Product.store_id == store.id,
        Product.is_active == True
    ).all()

    return templates.TemplateResponse("shop/home.html", {
        "request": request,
        "store": store,
        "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/stores/tech-store/logo.png",
        "logo_dark": "/media/stores/tech-store/logo-dark.png",
        "favicon": "/media/stores/tech-store/favicon.ico",
        "banner": "/media/stores/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: StoreTheme, preset_name: str):
    """Apply a preset to a store 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/store_themes.py

@router.get("/stores/{store_id}/theme")
def get_store_theme(store_id: int, db: Session = Depends(get_db)):
    """Get theme configuration for store"""
    theme = db.query(StoreTheme).filter(
        StoreTheme.store_id == store_id
    ).first()

    if not theme:
        # Return default theme
        return get_default_theme()

    return theme.to_dict()


@router.put("/stores/{store_id}/theme")
def update_store_theme(
    store_id: int,
    theme_data: dict,
    db: Session = Depends(get_db)
):
    """Update or create theme for store"""

    theme = db.query(StoreTheme).filter(
        StoreTheme.store_id == store_id
    ).first()

    if not theme:
        theme = StoreTheme(store_id=store_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("/stores/{store_id}/theme/preset/{preset_name}")
def apply_theme_preset(
    store_id: int,
    preset_name: str,
    db: Session = Depends(get_db)
):
    """Apply a preset theme to store"""
    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(StoreTheme).filter(
        StoreTheme.store_id == store_id
    ).first()

    if not theme:
        theme = StoreTheme(store_id=store_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 Stores

Store 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"
    }
}

Store 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"
    }
}

Store 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 Store Themes

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

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

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

Test 2: Theme API

# Get store theme
curl http://localhost:8000/api/v1/admin/stores/1/theme

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

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

Benefits

For Platform Owner

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

For Stores

  • 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 stores to preview themes before applying:

@router.get("/stores/{store_id}/theme/preview/{preset_name}")
def preview_theme(store_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("store_themes.id"))
    conversion_rate = Column(Numeric(5, 2))
    avg_session_duration = Column(Integer)
    bounce_rate = Column(Numeric(5, 2))

Summary

What you've built:

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

Each store 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 store
  • Admin panel management
  • Preview and testing

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

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