- 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>
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:
- Runs AFTER store_context_middleware
- Loads theme for detected store
- Injects theme into request.state
- 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! 🎨