Some checks failed
- 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>
699 lines
16 KiB
Markdown
699 lines
16 KiB
Markdown
# 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:
|
|
|
|
```sql
|
|
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`:
|
|
|
|
```python
|
|
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:**
|
|
```python
|
|
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:**
|
|
```python
|
|
{
|
|
"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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```css
|
|
: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:**
|
|
```html
|
|
<!-- In templates -->
|
|
<button style="background-color: var(--color-primary)">
|
|
Click Me
|
|
</button>
|
|
|
|
<h1 style="font-family: var(--font-heading)">
|
|
Welcome
|
|
</h1>
|
|
```
|
|
|
|
```css
|
|
/* 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
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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
|
|
```python
|
|
{
|
|
"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
|
|
```python
|
|
{
|
|
"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
|
|
```python
|
|
{
|
|
"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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```python
|
|
@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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```python
|
|
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! 🎨
|