docs: migrate module documentation to single source of truth

Move 39 documentation files from top-level docs/ into each module's
docs/ folder, accessible via symlinks from docs/modules/. Create
data-model.md files for 10 modules with full schema documentation.
Replace originals with redirect stubs. Remove empty guide stubs.

Modules migrated: tenancy, billing, loyalty, marketplace, orders,
messaging, cms, catalog, inventory, hosting, prospecting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 23:38:37 +01:00
parent 2287f4597d
commit f141cc4e6a
140 changed files with 19921 additions and 17723 deletions

View File

@@ -0,0 +1,604 @@
# Content Management System (CMS)
## Overview
The Content Management System allows platform administrators and stores to manage static content pages like About, FAQ, Contact, Shipping, Returns, Privacy Policy, Terms of Service, etc.
**Key Features:**
- ✅ Platform-level default content
- ✅ Store-specific overrides
- ✅ Fallback system (store → platform default)
- ✅ Rich text content (HTML/Markdown)
- ✅ SEO metadata
- ✅ Published/Draft status
- ✅ Navigation management (footer/header)
- ✅ Display order control
## Architecture
### Two-Tier Content System
```
┌─────────────────────────────────────────────────────────────┐
│ CONTENT LOOKUP FLOW │
└─────────────────────────────────────────────────────────────┘
Request: /about
1. Check for store-specific override
SELECT * FROM content_pages
WHERE store_id = 123 AND slug = 'about' AND is_published = true
Found? ✅ Return store content
❌ Continue to step 2
2. Check for platform default
SELECT * FROM content_pages
WHERE store_id IS NULL AND slug = 'about' AND is_published = true
Found? ✅ Return platform content
❌ Return 404 or default template
```
### Database Schema
```sql
CREATE TABLE content_pages (
id SERIAL PRIMARY KEY,
-- Store association (NULL = platform default)
store_id INTEGER REFERENCES stores(id) ON DELETE CASCADE,
-- Page identification
slug VARCHAR(100) NOT NULL, -- about, faq, contact, shipping, returns
title VARCHAR(200) NOT NULL,
-- Content
content TEXT NOT NULL, -- HTML or Markdown
content_format VARCHAR(20) DEFAULT 'html', -- html, markdown
-- SEO
meta_description VARCHAR(300),
meta_keywords VARCHAR(300),
-- Publishing
is_published BOOLEAN DEFAULT FALSE NOT NULL,
published_at TIMESTAMP WITH TIME ZONE,
-- Navigation placement
display_order INTEGER DEFAULT 0,
show_in_footer BOOLEAN DEFAULT TRUE, -- Quick Links column
show_in_header BOOLEAN DEFAULT FALSE, -- Top navigation
show_in_legal BOOLEAN DEFAULT FALSE, -- Bottom bar with copyright
-- Timestamps
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
-- Author tracking
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
updated_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
-- Constraints
CONSTRAINT uq_store_slug UNIQUE (store_id, slug)
);
CREATE INDEX idx_store_published ON content_pages (store_id, is_published);
CREATE INDEX idx_slug_published ON content_pages (slug, is_published);
```
## Usage
### Platform Administrator Workflow
**1. Create Platform Default Pages**
Platform admins create default content that all stores inherit:
```bash
POST /api/v1/admin/content-pages/platform
{
"slug": "about",
"title": "About Us",
"content": "<h1>About Us</h1><p>We are a marketplace...</p>",
"content_format": "html",
"meta_description": "Learn more about our marketplace",
"is_published": true,
"show_in_header": true,
"show_in_footer": true,
"show_in_legal": false,
"display_order": 1
}
```
**Common Platform Defaults:**
- `about` - About Us
- `contact` - Contact Us
- `faq` - Frequently Asked Questions
- `shipping` - Shipping Information
- `returns` - Return Policy
- `privacy` - Privacy Policy
- `terms` - Terms of Service
- `help` - Help Center
**2. View All Content Pages**
```bash
GET /api/v1/admin/content-pages/
GET /api/v1/admin/content-pages/?store_id=123 # Filter by store
GET /api/v1/admin/content-pages/platform # Only platform defaults
```
**3. Update Platform Default**
```bash
PUT /api/v1/admin/content-pages/1
{
"title": "Updated About Us",
"content": "<h1>About Our Platform</h1>...",
"is_published": true
}
```
### Store Workflow
**1. View Available Pages**
Stores see their overrides + platform defaults:
```bash
GET /api/v1/store/{code}/content-pages/
```
Response:
```json
[
{
"id": 15,
"slug": "about",
"title": "About Orion", // Store override
"is_store_override": true,
"is_platform_page": false
},
{
"id": 2,
"slug": "shipping",
"title": "Shipping Information", // Platform default
"is_store_override": false,
"is_platform_page": true
}
]
```
**2. Create Store Override**
Store creates custom "About" page:
```bash
POST /api/v1/store/{code}/content-pages/
{
"slug": "about",
"title": "About Orion",
"content": "<h1>About Orion</h1><p>We specialize in...</p>",
"is_published": true
}
```
This overrides the platform default for this store only.
**3. View Only Store Overrides**
```bash
GET /api/v1/store/{code}/content-pages/overrides
```
Shows what the store has customized (excludes platform defaults).
**4. Delete Override (Revert to Platform Default)**
```bash
DELETE /api/v1/store/{code}/content-pages/15
```
After deletion, platform default will be shown again.
### Storefront (Public)
**1. Get Page Content**
```bash
GET /api/v1/storefront/content-pages/about
```
Automatically uses store context from middleware:
- Returns store override if exists
- Falls back to platform default
- Returns 404 if neither exists
**2. Get Navigation Links**
```bash
# Get all navigation pages
GET /api/v1/storefront/content-pages/navigation
# Filter by placement
GET /api/v1/storefront/content-pages/navigation?header_only=true
GET /api/v1/storefront/content-pages/navigation?footer_only=true
GET /api/v1/storefront/content-pages/navigation?legal_only=true
```
Returns published pages filtered by navigation placement.
## File Structure
```
app/
├── models/database/
│ └── content_page.py ← Database model
├── services/
│ └── content_page_service.py ← Business logic
├── api/v1/
│ ├── admin/
│ │ └── content_pages.py ← Admin API endpoints
│ ├── store/
│ │ └── content_pages.py ← Store API endpoints
│ └── storefront/
│ └── content_pages.py ← Public API endpoints
└── templates/storefront/
├── about.html ← Content page template
├── faq.html
├── contact.html
└── ...
```
## Template Integration
### Generic Content Page Template
Create a reusable template for all content pages:
```jinja2
{# app/templates/storefront/content-page.html #}
{% extends "storefront/base.html" %}
{% block title %}{{ page.title }}{% endblock %}
{% block meta_description %}
{{ page.meta_description or page.title }}
{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{# Breadcrumbs #}
<nav class="mb-6">
<a href="{{ base_url }}" class="text-primary hover:underline">Home</a>
<span class="mx-2">/</span>
<span class="text-gray-600">{{ page.title }}</span>
</nav>
{# Page Title #}
<h1 class="text-4xl font-bold text-gray-900 dark:text-gray-100 mb-8">
{{ page.title }}
</h1>
{# Content #}
<div class="prose dark:prose-invert max-w-none">
{% if page.content_format == 'markdown' %}
{{ page.content | markdown }}
{% else %}
{{ page.content | safe }}
{% endif %}
</div>
{# Last updated #}
{% if page.updated_at %}
<div class="mt-12 pt-6 border-t text-sm text-gray-500">
Last updated: {{ page.updated_at }}
</div>
{% endif %}
</div>
{% endblock %}
```
### Route Handler
```python
# app/routes/storefront_pages.py
from app.services.content_page_service import content_page_service
@router.get("/{slug}", response_class=HTMLResponse)
async def content_page(
slug: str,
request: Request,
db: Session = Depends(get_db)
):
"""
Generic content page handler.
Loads content from database with store override support.
"""
store = getattr(request.state, 'store', None)
store_id = store.id if store else None
page = content_page_service.get_page_for_store(
db,
slug=slug,
store_id=store_id,
include_unpublished=False
)
if not page:
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
return templates.TemplateResponse(
"storefront/content-page.html",
get_storefront_context(request, page=page)
)
```
### Dynamic Footer Navigation
Update footer to load links from database:
```jinja2
{# app/templates/storefront/base.html #}
<footer>
<div class="grid grid-cols-3">
<div>
<h4>Quick Links</h4>
<ul>
{% for page in footer_pages %}
<li>
<a href="{{ base_url }}{{ page.slug }}">
{{ page.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</footer>
```
## Best Practices
### 1. Content Formatting
**HTML Content:**
```html
<h1>About Us</h1>
<p>We are a <strong>leading marketplace</strong> for...</p>
<ul>
<li>Quality products</li>
<li>Fast shipping</li>
<li>Great support</li>
</ul>
```
**Markdown Content:**
```markdown
# About Us
We are a **leading marketplace** for...
- Quality products
- Fast shipping
- Great support
```
### 2. SEO Optimization
Always provide meta descriptions:
```json
{
"meta_description": "Learn about our marketplace, mission, and values. We connect stores with customers worldwide.",
"meta_keywords": "about us, marketplace, e-commerce, mission"
}
```
### 3. Draft → Published Workflow
1. Create page with `is_published: false`
2. Preview using `include_unpublished=true` parameter
3. Review and edit
4. Publish with `is_published: true`
### 4. Navigation Management
The CMS supports three navigation placement categories:
```
┌─────────────────────────────────────────────────────────────────┐
│ HEADER (show_in_header=true) │
│ [Logo] About Us Contact [Login] [Sign Up] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ PAGE CONTENT │
│ │
├─────────────────────────────────────────────────────────────────┤
│ FOOTER (show_in_footer=true) │
│ ┌──────────────┬──────────────┬────────────┬──────────────┐ │
│ │ Quick Links │ Platform │ Contact │ Social │ │
│ │ • About │ • Admin │ • Email │ • Twitter │ │
│ │ • FAQ │ • Store │ • Phone │ • LinkedIn │ │
│ │ • Contact │ │ │ │ │
│ │ • Shipping │ │ │ │ │
│ └──────────────┴──────────────┴────────────┴──────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ LEGAL BAR (show_in_legal=true) │
│ © 2025 Orion Privacy Policy │ Terms │
└─────────────────────────────────────────────────────────────────┘
```
**Navigation Categories:**
| Category | Field | Location | Typical Pages |
|----------|-------|----------|---------------|
| Header | `show_in_header` | Top navigation bar | About, Contact |
| Footer | `show_in_footer` | Quick Links column | FAQ, Shipping, Returns |
| Legal | `show_in_legal` | Bottom bar with © | Privacy, Terms |
**Use `display_order` to control link ordering within each category:**
```python
# Platform defaults with navigation placement
"about": display_order=1, show_in_header=True, show_in_footer=True
"contact": display_order=2, show_in_header=True, show_in_footer=True
"faq": display_order=3, show_in_footer=True
"shipping": display_order=4, show_in_footer=True
"returns": display_order=5, show_in_footer=True
"privacy": display_order=6, show_in_legal=True
"terms": display_order=7, show_in_legal=True
```
### 5. Content Reversion
To revert store override back to platform default:
```bash
# Store deletes their custom page
DELETE /api/v1/store/{code}/content-pages/15
# Platform default will now be shown automatically
```
## Common Page Slugs
Standard slugs to implement:
| Slug | Title | Header | Footer | Legal | Order |
|------|-------|--------|--------|-------|-------|
| `about` | About Us | ✅ | ✅ | ❌ | 1 |
| `contact` | Contact Us | ✅ | ✅ | ❌ | 2 |
| `faq` | FAQ | ❌ | ✅ | ❌ | 3 |
| `shipping` | Shipping Info | ❌ | ✅ | ❌ | 4 |
| `returns` | Returns | ❌ | ✅ | ❌ | 5 |
| `privacy` | Privacy Policy | ❌ | ❌ | ✅ | 6 |
| `terms` | Terms of Service | ❌ | ❌ | ✅ | 7 |
| `help` | Help Center | ❌ | ✅ | ❌ | 8 |
| `size-guide` | Size Guide | ❌ | ❌ | ❌ | - |
| `careers` | Careers | ❌ | ❌ | ❌ | - |
| `cookies` | Cookie Policy | ❌ | ❌ | ✅ | 8 |
## Security Considerations
1. **HTML Sanitization**: If using HTML format, sanitize user input to prevent XSS
2. **Authorization**: Stores can only edit their own pages
3. **Published Status**: Only published pages visible to public
4. **Store Isolation**: Stores cannot see/edit other store's content
## Migration Strategy
### Initial Setup
1. **Create Platform Defaults**:
```bash
python scripts/seed/create_default_content_pages.py
```
2. **Migrate Existing Static Templates**:
- Convert existing HTML templates to database content
- Preserve existing URLs and SEO
3. **Update Routes**:
- Add generic content page route handler
- Remove individual route handlers for each page
## Future Enhancements
Possible improvements:
- **Version History**: Track content changes over time
- **Rich Text Editor**: WYSIWYG editor in admin/store panel
- **Image Management**: Upload and insert images
- **Templates**: Pre-built page templates for common pages
- **Localization**: Multi-language content support
- **Scheduled Publishing**: Publish pages at specific times
- **Content Approval**: Admin review before store pages go live
## API Reference Summary
### Admin Endpoints
```
GET /api/v1/admin/content-pages/ # List all pages
GET /api/v1/admin/content-pages/platform # List platform defaults
POST /api/v1/admin/content-pages/platform # Create platform default
GET /api/v1/admin/content-pages/{id} # Get specific page
PUT /api/v1/admin/content-pages/{id} # Update page
DELETE /api/v1/admin/content-pages/{id} # Delete page
```
### Store Endpoints
```
GET /api/v1/store/{code}/content-pages/ # List all (store + platform)
GET /api/v1/store/{code}/content-pages/overrides # List store overrides only
GET /api/v1/store/{code}/content-pages/{slug} # Get specific page
POST /api/v1/store/{code}/content-pages/ # Create store override
PUT /api/v1/store/{code}/content-pages/{id} # Update store page
DELETE /api/v1/store/{code}/content-pages/{id} # Delete store page
```
### Storefront (Public) Endpoints
```
GET /api/v1/storefront/content-pages/navigation # Get navigation links
GET /api/v1/storefront/content-pages/{slug} # Get page content
```
## Example: Complete Workflow
**1. Platform Admin Creates Defaults:**
```bash
# Create "About" page
curl -X POST /api/v1/admin/content-pages/platform \
-H "Authorization: Bearer <admin_token>" \
-d '{
"slug": "about",
"title": "About Our Marketplace",
"content": "<h1>About</h1><p>Default content...</p>",
"is_published": true
}'
```
**2. All Stores See Platform Default:**
- Store A visits: `store-a.com/about` → Shows platform default
- Store B visits: `store-b.com/about` → Shows platform default
**3. Store A Creates Override:**
```bash
curl -X POST /api/v1/store/store-a/content-pages/ \
-H "Authorization: Bearer <store_token>" \
-d '{
"slug": "about",
"title": "About Store A",
"content": "<h1>About Store A</h1><p>Custom content...</p>",
"is_published": true
}'
```
**4. Now:**
- Store A visits: `store-a.com/about` → Shows Store A custom content
- Store B visits: `store-b.com/about` → Still shows platform default
**5. Store A Reverts to Default:**
```bash
curl -X DELETE /api/v1/store/store-a/content-pages/15 \
-H "Authorization: Bearer <store_token>"
```
**6. Result:**
- Store A visits: `store-a.com/about` → Shows platform default again

View File

@@ -0,0 +1,115 @@
# CMS Data Model
Entity relationships and database schema for the CMS module.
## Entity Relationship Overview
```
Platform 1──* ContentPage
Store 1──* ContentPage
Store 1──* MediaFile
Store 1──1 StoreTheme
```
## Models
### ContentPage
Multi-language content pages with platform/store hierarchy. Pages can be platform marketing pages, store defaults, or store-specific overrides.
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | Integer | PK | Primary key |
| `platform_id` | Integer | FK, not null, indexed | Platform this page belongs to |
| `store_id` | Integer | FK, nullable, indexed | Store association (null = platform/default) |
| `is_platform_page` | Boolean | not null, default False | Platform marketing page vs store default |
| `slug` | String(100) | not null, indexed | Page identifier (about, faq, contact, etc.) |
| `title` | String(200) | not null | Page title |
| `content` | Text | not null | HTML or Markdown content |
| `content_format` | String(20) | default "html" | Format: html, markdown |
| `template` | String(50) | default "default" | Template: default, minimal, modern, full |
| `sections` | JSON | nullable | Structured homepage sections with i18n |
| `title_translations` | JSON | nullable | Language-keyed title dict {en, fr, de, lb} |
| `content_translations` | JSON | nullable | Language-keyed content dict {en, fr, de, lb} |
| `meta_description` | String(300) | nullable | SEO meta description |
| `meta_keywords` | String(300) | nullable | SEO keywords |
| `is_published` | Boolean | not null, default False | Publication status |
| `published_at` | DateTime | nullable, tz-aware | Publication timestamp |
| `display_order` | Integer | not null, default 0 | Menu/footer ordering |
| `show_in_footer` | Boolean | not null, default True | Footer visibility |
| `show_in_header` | Boolean | not null, default False | Header navigation |
| `show_in_legal` | Boolean | not null, default False | Legal bar visibility |
| `created_at` | DateTime | tz-aware | Record creation time |
| `updated_at` | DateTime | tz-aware | Record update time |
| `created_by` | Integer | FK, nullable | Creator user ID |
| `updated_by` | Integer | FK, nullable | Updater user ID |
**Unique Constraint**: `(platform_id, store_id, slug)`
**Composite Indexes**: `(platform_id, store_id, is_published)`, `(platform_id, slug, is_published)`, `(platform_id, is_platform_page)`
**Page tiers**: platform → store_default (store_id null, not platform) → store_override (store_id set)
### MediaFile
Media files (images, videos, documents) managed per-store.
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | Integer | PK | Primary key |
| `store_id` | Integer | FK, not null, indexed | Store owner |
| `filename` | String(255) | not null, indexed | UUID-based stored filename |
| `original_filename` | String(255) | nullable | Original upload name |
| `file_path` | String(500) | not null | Relative path from uploads/ |
| `media_type` | String(20) | not null | image, video, document |
| `mime_type` | String(100) | nullable | MIME type |
| `file_size` | Integer | nullable | File size in bytes |
| `width` | Integer | nullable | Image/video width in pixels |
| `height` | Integer | nullable | Image/video height in pixels |
| `thumbnail_path` | String(500) | nullable | Path to thumbnail |
| `alt_text` | String(500) | nullable | Alt text for images |
| `description` | Text | nullable | File description |
| `folder` | String(100) | default "general" | Folder: products, general, etc. |
| `tags` | JSON | nullable | Tags for categorization |
| `extra_metadata` | JSON | nullable | Additional metadata (EXIF, etc.) |
| `is_optimized` | Boolean | default False | Optimization status |
| `optimized_size` | Integer | nullable | Size after optimization |
| `usage_count` | Integer | default 0 | Usage tracking |
| `created_at` | DateTime | tz-aware | Record creation time |
| `updated_at` | DateTime | tz-aware | Record update time |
**Composite Indexes**: `(store_id, folder)`, `(store_id, media_type)`
### StoreTheme
Per-store theme configuration including colors, fonts, layout, and branding. One-to-one with Store.
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | Integer | PK | Primary key |
| `store_id` | Integer | FK, unique, not null | One-to-one with store |
| `theme_name` | String(100) | default "default" | Preset: default, modern, classic, minimal, vibrant |
| `is_active` | Boolean | default True | Theme active status |
| `colors` | JSON | default {...} | Color scheme: primary, secondary, accent, background, text, border |
| `font_family_heading` | String(100) | default "Inter, sans-serif" | Heading font |
| `font_family_body` | String(100) | default "Inter, sans-serif" | Body font |
| `logo_url` | String(500) | nullable | Store logo path |
| `logo_dark_url` | String(500) | nullable | Dark mode logo |
| `favicon_url` | String(500) | nullable | Favicon path |
| `banner_url` | String(500) | nullable | Homepage banner |
| `layout_style` | String(50) | default "grid" | Layout: grid, list, masonry |
| `header_style` | String(50) | default "fixed" | Header: fixed, static, transparent |
| `product_card_style` | String(50) | default "modern" | Card: modern, classic, minimal |
| `custom_css` | Text | nullable | Custom CSS overrides |
| `social_links` | JSON | default {} | Social media URLs |
| `meta_title_template` | String(200) | nullable | SEO title template |
| `meta_description` | Text | nullable | SEO meta description |
| `created_at` | DateTime | tz-aware | Record creation time |
| `updated_at` | DateTime | tz-aware | Record update time |
## Design Patterns
- **Three-tier content hierarchy**: Platform pages → store defaults → store overrides
- **JSON translations**: Title and content translations stored as JSON dicts with language keys
- **Media organization**: Files organized by store and folder with type classification
- **Theme presets**: Named presets with full customization via JSON color scheme and CSS overrides
- **SEO support**: Meta description, keywords, and title templates on pages and themes

View File

@@ -0,0 +1,287 @@
# Email Templates Guide
## Overview
The Orion platform provides a comprehensive email template system that allows:
- **Platform Administrators**: Manage all email templates across the platform
- **Stores**: Customize customer-facing emails with their own branding
This guide covers how to use the email template system from both perspectives.
---
## For Stores
### Accessing Email Templates
1. Log in to your store dashboard
2. Navigate to **Settings** > **Email Templates** in the sidebar
3. You'll see a list of all customizable email templates
### Understanding Template Status
Each template shows its customization status:
| Status | Description |
|--------|-------------|
| **Platform Default** | Using the standard Orion template |
| **Customized** | You have created a custom version |
| Language badges (green) | Languages where you have customizations |
### Customizing a Template
1. Click on any template to open the edit modal
2. Select the language tab you want to customize (EN, FR, DE, LB)
3. Edit the following fields:
- **Subject**: The email subject line
- **HTML Body**: The rich HTML content
- **Plain Text Body**: Fallback for email clients that don't support HTML
4. Click **Save** to save your customization
### Template Variables
Templates use special variables that are automatically replaced with actual values. Common variables include:
| Variable | Description |
|----------|-------------|
| `{{ customer_name }}` | Customer's first name |
| `{{ order_number }}` | Order reference number |
| `{{ store_name }}` | Your store name |
| `{{ platform_name }}` | Platform name (Orion or your whitelabel name) |
Each template shows its available variables in the reference panel.
### Previewing Templates
Before saving, you can preview your template:
1. Click **Preview** in the edit modal
2. A preview window shows how the email will look
3. Sample data is used for all variables
### Testing Templates
To send a test email:
1. Click **Send Test Email** in the edit modal
2. Enter your email address
3. Click **Send**
4. Check your inbox to see the actual email
### Reverting to Platform Default
If you want to remove your customization and use the platform default:
1. Open the template edit modal
2. Click **Revert to Default**
3. Confirm the action
Your customization will be deleted and the platform template will be used.
### Available Templates for Stores
| Template | Category | Description |
|----------|----------|-------------|
| Welcome Email | AUTH | Sent when a customer registers |
| Password Reset | AUTH | Password reset link |
| Order Confirmation | ORDERS | Sent after order placement |
| Shipping Notification | ORDERS | Sent when order is shipped |
**Note:** Billing and subscription emails are platform-only and cannot be customized.
---
## For Platform Administrators
### Accessing Email Templates
1. Log in to the admin dashboard
2. Navigate to **System** > **Email Templates** in the sidebar
3. You'll see all platform templates grouped by category
### Template Categories
| Category | Description | Store Override |
|----------|-------------|-----------------|
| AUTH | Authentication emails | Allowed |
| ORDERS | Order-related emails | Allowed |
| BILLING | Subscription/payment emails | **Not Allowed** |
| SYSTEM | System notifications | Allowed |
| MARKETING | Promotional emails | Allowed |
### Editing Platform Templates
1. Click on any template to open the edit modal
2. Select the language tab (EN, FR, DE, LB)
3. Edit the subject and body content
4. Click **Save**
**Important:** Changes to platform templates affect:
- All stores who haven't customized the template
- New stores automatically
### Creating New Templates
To add a new template:
1. Use the database seed script or migration
2. Define the template code, category, and languages
3. Set `is_platform_only` if stores shouldn't override it
### Viewing Email Logs
To see email delivery history:
1. Open a template
2. Click **View Logs**
3. See recent emails sent using this template
Logs show:
- Recipient email
- Send date/time
- Delivery status
- Store (if applicable)
### Template Best Practices
1. **Use all 4 languages**: Provide content in EN, FR, DE, and LB
2. **Test before publishing**: Always send test emails
3. **Include plain text**: Not all email clients support HTML
4. **Use consistent branding**: Follow Orion brand guidelines
5. **Keep subjects short**: Under 60 characters for mobile
---
## Language Resolution
When sending an email, the system determines the language in this order:
1. **Customer's preferred language** (if set in their profile)
2. **Store's storefront language** (if customer doesn't have preference)
3. **Platform default** (French - "fr")
### Template Resolution for Stores
1. System checks if store has a custom override
2. If yes, uses store's template
3. If no, falls back to platform template
4. If requested language unavailable, falls back to English
---
## Branding
### Standard Stores
Standard stores' emails include Orion branding:
- Orion logo in header
- "Powered by Orion" footer
### Whitelabel Stores
Enterprise-tier stores with whitelabel enabled:
- No Orion branding
- Store's logo in header
- Custom footer (if configured)
---
## Email Template Variables Reference
### Authentication Templates
#### signup_welcome
```
{{ first_name }} - Customer's first name
{{ merchant_name }} - Store merchant name
{{ email }} - Customer's email
{{ login_url }} - Link to login page
{{ trial_days }} - Trial period length
{{ tier_name }} - Subscription tier
```
#### password_reset
```
{{ customer_name }} - Customer's name
{{ reset_link }} - Password reset URL
{{ expiry_hours }} - Link expiration time
```
### Order Templates
#### order_confirmation
```
{{ customer_name }} - Customer's name
{{ order_number }} - Order reference
{{ order_total }} - Order total amount
{{ order_items_count }} - Number of items
{{ order_date }} - Order date
{{ shipping_address }} - Delivery address
```
### Common Variables (All Templates)
```
{{ platform_name }} - "Orion" or whitelabel name
{{ platform_logo_url }} - Platform logo URL
{{ support_email }} - Support email address
{{ store_name }} - Store's business name
{{ store_logo_url }} - Store's logo URL
```
---
## Troubleshooting
### Email Not Received
1. Check spam/junk folder
2. Verify email address is correct
3. Check email logs in admin dashboard
4. Verify SMTP configuration
### Template Not Applying
1. Clear browser cache
2. Verify the correct language is selected
3. Check if store override exists
4. Verify template is not platform-only
### Variables Not Replaced
1. Check variable spelling (case-sensitive)
2. Ensure variable is available for this template
3. Wrap variables in `{{ }}` syntax
4. Check for typos in variable names
---
## API Reference
For developers integrating with the email system:
### Sending a Template Email
```python
from app.services.email_service import EmailService
email_service = EmailService(db)
email_service.send_template(
template_code="order_confirmation",
to_email="customer@example.com",
to_name="John Doe",
language="fr",
variables={
"customer_name": "John",
"order_number": "ORD-12345",
"order_total": "99.99",
},
store_id=store.id,
related_type="order",
related_id=order.id,
)
```
See [Email Templates Architecture](../cms/email-templates.md) for full technical documentation.

View File

@@ -0,0 +1,458 @@
# Email Template System
## Overview
The email template system provides comprehensive email customization for the Orion platform with the following features:
- **Platform-level templates** with store overrides
- **Orion branding** by default (removed for Enterprise whitelabel tier)
- **Platform-only templates** that cannot be overridden (billing, subscriptions)
- **Admin UI** for editing platform templates
- **Store UI** for customizing customer-facing emails
- **4-language support** (en, fr, de, lb)
- **Smart language resolution** (customer → store → platform default)
---
## Architecture
### Database Models
#### EmailTemplate (Platform Templates)
**File:** `models/database/email.py`
| Column | Type | Description |
|--------|------|-------------|
| `id` | Integer | Primary key |
| `code` | String(100) | Unique template identifier |
| `language` | String(5) | Language code (en, fr, de, lb) |
| `name` | String(255) | Human-readable name |
| `description` | Text | Template description |
| `category` | Enum | AUTH, ORDERS, BILLING, SYSTEM, MARKETING |
| `subject` | String(500) | Email subject line (Jinja2) |
| `body_html` | Text | HTML body (Jinja2) |
| `body_text` | Text | Plain text body (Jinja2) |
| `variables` | JSON | List of available variables |
| `is_platform_only` | Boolean | Cannot be overridden by stores |
| `required_variables` | Text | Comma-separated required variables |
**Key Methods:**
- `get_by_code_and_language(db, code, language)` - Get specific template
- `get_overridable_templates(db)` - Get templates stores can customize
#### StoreEmailTemplate (Store Overrides)
**File:** `models/database/store_email_template.py`
| Column | Type | Description |
|--------|------|-------------|
| `id` | Integer | Primary key |
| `store_id` | Integer | FK to stores.id |
| `template_code` | String(100) | References EmailTemplate.code |
| `language` | String(5) | Language code |
| `name` | String(255) | Custom name (optional) |
| `subject` | String(500) | Custom subject |
| `body_html` | Text | Custom HTML body |
| `body_text` | Text | Custom plain text body |
| `created_at` | DateTime | Creation timestamp |
| `updated_at` | DateTime | Last update timestamp |
**Key Methods:**
- `get_override(db, store_id, code, language)` - Get store override
- `create_or_update(db, store_id, code, language, ...)` - Upsert override
- `delete_override(db, store_id, code, language)` - Revert to platform default
- `get_all_overrides_for_store(db, store_id)` - List all store overrides
### Unique Constraint
```sql
UNIQUE (store_id, template_code, language)
```
---
## Email Template Service
**File:** `app/services/email_template_service.py`
The `EmailTemplateService` encapsulates all email template business logic, keeping API endpoints clean and focused on request/response handling.
### Admin Methods
| Method | Description |
|--------|-------------|
| `list_platform_templates()` | List all platform templates grouped by code |
| `get_template_categories()` | Get list of template categories |
| `get_platform_template(code)` | Get template with all language versions |
| `update_platform_template(code, language, data)` | Update platform template content |
| `preview_template(code, language, variables)` | Generate preview with sample data |
| `get_template_logs(code, limit)` | Get email logs for template |
### Store Methods
| Method | Description |
|--------|-------------|
| `list_overridable_templates(store_id)` | List templates store can customize |
| `get_store_template(store_id, code, language)` | Get template (override or platform default) |
| `create_or_update_store_override(store_id, code, language, data)` | Save store customization |
| `delete_store_override(store_id, code, language)` | Revert to platform default |
| `preview_store_template(store_id, code, language, variables)` | Preview with store branding |
### Usage Example
```python
from app.services.email_template_service import EmailTemplateService
service = EmailTemplateService(db)
# List templates for admin
templates = service.list_platform_templates()
# Get store's view of a template
template_data = service.get_store_template(store_id, "order_confirmation", "fr")
# Create store override
service.create_or_update_store_override(
store_id=store.id,
code="order_confirmation",
language="fr",
subject="Votre commande {{ order_number }}",
body_html="<html>...</html>",
body_text="Plain text...",
)
```
---
## Email Service
**File:** `app/services/email_service.py`
### Language Resolution
Priority order for determining email language:
1. **Customer preferred language** (if customer exists)
2. **Store storefront language** (store.storefront_language)
3. **Platform default** (`en`)
```python
def resolve_language(
self,
customer_id: int | None,
store_id: int | None,
explicit_language: str | None = None
) -> str
```
### Template Resolution
```python
def resolve_template(
self,
template_code: str,
language: str,
store_id: int | None = None
) -> ResolvedTemplate
```
Resolution order:
1. If `store_id` provided and template **not** platform-only:
- Look for `StoreEmailTemplate` override
- Fall back to platform `EmailTemplate`
2. If no store or platform-only:
- Use platform `EmailTemplate`
3. Language fallback: `requested_language``en`
### Branding Resolution
```python
def get_branding(self, store_id: int | None) -> BrandingContext
```
| Scenario | Platform Name | Platform Logo |
|----------|--------------|---------------|
| No store | Orion | Orion logo |
| Standard store | Orion | Orion logo |
| Whitelabel store | Store name | Store logo |
Whitelabel is determined by the `white_label` feature flag on the store.
---
## API Endpoints
### Admin API
**File:** `app/api/v1/admin/email_templates.py`
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/v1/admin/email-templates` | List all platform templates |
| GET | `/api/v1/admin/email-templates/categories` | Get template categories |
| GET | `/api/v1/admin/email-templates/{code}` | Get template (all languages) |
| GET | `/api/v1/admin/email-templates/{code}/{language}` | Get specific language version |
| PUT | `/api/v1/admin/email-templates/{code}/{language}` | Update template |
| POST | `/api/v1/admin/email-templates/{code}/preview` | Preview with sample data |
| POST | `/api/v1/admin/email-templates/{code}/test` | Send test email |
| GET | `/api/v1/admin/email-templates/{code}/logs` | View email logs for template |
### Store API
**File:** `app/api/v1/store/email_templates.py`
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/v1/store/email-templates` | List overridable templates |
| GET | `/api/v1/store/email-templates/{code}` | Get template with override status |
| GET | `/api/v1/store/email-templates/{code}/{language}` | Get specific language (override or default) |
| PUT | `/api/v1/store/email-templates/{code}/{language}` | Create/update override |
| DELETE | `/api/v1/store/email-templates/{code}/{language}` | Reset to platform default |
| POST | `/api/v1/store/email-templates/{code}/preview` | Preview with store branding |
| POST | `/api/v1/store/email-templates/{code}/test` | Send test email |
---
## User Interface
### Admin UI
**Page:** `/admin/email-templates`
**Template:** `app/templates/admin/email-templates.html`
**JavaScript:** `static/admin/js/email-templates.js`
Features:
- Template list with category filtering
- Edit modal with language tabs (en, fr, de, lb)
- Platform-only indicator badge
- Variable reference panel
- HTML preview in iframe
- Send test email functionality
### Store UI
**Page:** `/store/{store_code}/email-templates`
**Template:** `app/templates/store/email-templates.html`
**JavaScript:** `static/store/js/email-templates.js`
Features:
- List of overridable templates with customization status
- Language override badges (green = customized)
- Edit modal with:
- Language tabs
- Source indicator (store override vs platform default)
- Platform template reference
- Revert to default button
- Preview and test email functionality
---
## Template Categories
| Category | Description | Platform-Only |
|----------|-------------|---------------|
| AUTH | Authentication emails (welcome, password reset) | No |
| ORDERS | Order-related emails (confirmation, shipped) | No |
| BILLING | Subscription/payment emails | Yes |
| SYSTEM | System emails (team invites, alerts) | No |
| MARKETING | Marketing/promotional emails | No |
---
## Available Templates
### Customer-Facing (Overridable)
| Code | Category | Languages | Description |
|------|----------|-----------|-------------|
| `signup_welcome` | AUTH | en, fr, de, lb | Welcome email after store signup |
| `order_confirmation` | ORDERS | en, fr, de, lb | Order confirmation to customer |
| `password_reset` | AUTH | en, fr, de, lb | Password reset link |
| `team_invite` | SYSTEM | en | Team member invitation |
### Platform-Only (Not Overridable)
| Code | Category | Languages | Description |
|------|----------|-----------|-------------|
| `subscription_welcome` | BILLING | en | Subscription confirmation |
| `payment_failed` | BILLING | en | Failed payment notification |
| `subscription_cancelled` | BILLING | en | Cancellation confirmation |
| `trial_ending` | BILLING | en | Trial ending reminder |
---
## Template Variables
### Common Variables (Injected Automatically)
| Variable | Description |
|----------|-------------|
| `platform_name` | "Orion" or store name (whitelabel) |
| `platform_logo_url` | Platform logo URL |
| `support_email` | Support email address |
| `store_name` | Store business name |
| `store_logo_url` | Store logo URL |
### Template-Specific Variables
#### signup_welcome
- `first_name`, `merchant_name`, `email`, `store_code`
- `login_url`, `trial_days`, `tier_name`
#### order_confirmation
- `customer_name`, `order_number`, `order_total`
- `order_items_count`, `order_date`, `shipping_address`
#### password_reset
- `customer_name`, `reset_link`, `expiry_hours`
#### team_invite
- `invitee_name`, `inviter_name`, `store_name`
- `role`, `accept_url`, `expires_in_days`
---
## Migration
**File:** `alembic/versions/u9c0d1e2f3g4_add_store_email_templates.py`
Run migration:
```bash
alembic upgrade head
```
The migration:
1. Adds `is_platform_only` and `required_variables` columns to `email_templates`
2. Creates `store_email_templates` table
3. Adds unique constraint on `(store_id, template_code, language)`
4. Creates indexes for performance
---
## Seeding Templates
**File:** `scripts/seed/seed_email_templates.py`
Run seed script:
```bash
python scripts/seed/seed_email_templates.py
```
The script:
- Creates/updates all platform templates
- Supports all 4 languages for customer-facing templates
- Sets `is_platform_only` flag for billing templates
---
## Security Considerations
1. **XSS Prevention**: HTML templates are rendered server-side with Jinja2 escaping
2. **Access Control**: Stores can only view/edit their own overrides
3. **Platform-only Protection**: API enforces `is_platform_only` flag
4. **Template Validation**: Jinja2 syntax validated before save
5. **Rate Limiting**: Test email sending subject to rate limits
6. **Token Hashing**: Password reset tokens stored as SHA256 hashes
---
## Usage Examples
### Sending a Template Email
```python
from app.services.email_service import EmailService
email_svc = EmailService(db)
email_log = email_svc.send_template(
template_code="order_confirmation",
to_email="customer@example.com",
variables={
"customer_name": "John Doe",
"order_number": "ORD-12345",
"order_total": "€99.99",
"order_items_count": "3",
"order_date": "2024-01-15",
"shipping_address": "123 Main St, Luxembourg"
},
store_id=store.id, # Optional: enables store override lookup
customer_id=customer.id, # Optional: for language resolution
language="fr" # Optional: explicit language override
)
```
### Creating a Store Override
```python
from models.database.store_email_template import StoreEmailTemplate
override = StoreEmailTemplate.create_or_update(
db=db,
store_id=store.id,
template_code="order_confirmation",
language="fr",
subject="Confirmation de votre commande {{ order_number }}",
body_html="<html>...</html>",
body_text="Plain text version..."
)
db.commit()
```
### Reverting to Platform Default
```python
StoreEmailTemplate.delete_override(
db=db,
store_id=store.id,
template_code="order_confirmation",
language="fr"
)
db.commit()
```
---
## File Structure
```
├── alembic/versions/
│ └── u9c0d1e2f3g4_add_store_email_templates.py
├── app/
│ ├── api/v1/
│ │ ├── admin/
│ │ │ └── email_templates.py
│ │ └── store/
│ │ └── email_templates.py
│ ├── routes/
│ │ ├── admin_pages.py (route added)
│ │ └── store_pages.py (route added)
│ ├── services/
│ │ ├── email_service.py (enhanced)
│ │ └── email_template_service.py (new - business logic)
│ └── templates/
│ ├── admin/
│ │ ├── email-templates.html
│ │ └── partials/sidebar.html (link added)
│ └── store/
│ ├── email-templates.html
│ └── partials/sidebar.html (link added)
├── models/
│ ├── database/
│ │ ├── email.py (enhanced)
│ │ └── store_email_template.py
│ └── schema/
│ └── email.py
├── scripts/
│ └── seed_email_templates.py (enhanced)
└── static/
├── admin/js/
│ └── email-templates.js
└── store/js/
└── email-templates.js
```
---
## Related Documentation
- [Email Templates User Guide](email-templates-guide.md) - How to use the email template system
- [Password Reset Implementation](../../implementation/password-reset-implementation.md) - Password reset feature using email templates
- [Architecture Fixes (January 2026)](../../development/architecture-fixes-2026-01.md) - Architecture validation fixes

View File

@@ -0,0 +1,414 @@
# CMS Implementation Guide
## Quick Start
This guide shows you how to implement the Content Management System for static pages.
## What Was Implemented
**Database Model**: `models/database/content_page.py`
**Service Layer**: `app/services/content_page_service.py`
**Admin API**: `app/api/v1/admin/content_pages.py`
**Store API**: `app/api/v1/store/content_pages.py`
**Storefront API**: `app/api/v1/storefront/content_pages.py`
**Documentation**: Full CMS documentation in `docs/features/content-management-system.md`
## Next Steps to Activate
### 1. Create Database Migration
```bash
# Create Alembic migration
alembic revision --autogenerate -m "Add content_pages table"
# Review the generated migration in alembic/versions/
# Run migration
alembic upgrade head
```
### 2. Add Relationship to Store Model
Edit `models/database/store.py` and add this relationship:
```python
# Add this import
from sqlalchemy.orm import relationship
# Add this relationship to Store class
content_pages = relationship("ContentPage", back_populates="store", cascade="all, delete-orphan")
```
### 3. Register API Routers
Edit the appropriate router files to include the new endpoints:
**Admin Router** (`app/api/v1/admin/__init__.py`):
```python
from app.api.v1.admin import content_pages
api_router.include_router(
content_pages.router,
prefix="/content-pages",
tags=["admin-content-pages"]
)
```
**Store Router** (`app/api/v1/store/__init__.py`):
```python
from app.api.v1.store import content_pages
api_router.include_router(
content_pages.router,
prefix="/{store_code}/content-pages",
tags=["store-content-pages"]
)
```
**Storefront Router** (`app/api/v1/storefront/__init__.py` or create if doesn't exist):
```python
from app.api.v1.storefront import content_pages
api_router.include_router(
content_pages.router,
prefix="/content-pages",
tags=["storefront-content-pages"]
)
```
### 4. Update Storefront Routes to Use CMS
Edit `app/routes/storefront_pages.py` to add a generic content page handler:
```python
from app.services.content_page_service import content_page_service
@router.get("/{slug}", response_class=HTMLResponse, include_in_schema=False)
async def generic_content_page(
slug: str,
request: Request,
db: Session = Depends(get_db)
):
"""
Generic content page handler.
Handles: /about, /faq, /contact, /shipping, /returns, /privacy, /terms, etc.
"""
store = getattr(request.state, 'store', None)
store_id = store.id if store else None
page = content_page_service.get_page_for_store(
db,
slug=slug,
store_id=store_id,
include_unpublished=False
)
if not page:
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
return templates.TemplateResponse(
"storefront/content-page.html",
get_storefront_context(request, page=page)
)
```
### 5. Create Generic Content Page Template
Create `app/templates/storefront/content-page.html`:
```jinja2
{# app/templates/storefront/content-page.html #}
{% extends "storefront/base.html" %}
{% block title %}{{ page.title }}{% endblock %}
{% block meta_description %}
{{ page.meta_description or page.title }}
{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{# Breadcrumbs #}
<nav class="mb-6 text-sm">
<a href="{{ base_url }}" class="text-primary hover:underline">Home</a>
<span class="mx-2 text-gray-400">/</span>
<span class="text-gray-600 dark:text-gray-300">{{ page.title }}</span>
</nav>
{# Page Title #}
<h1 class="text-4xl font-bold text-gray-900 dark:text-gray-100 mb-8">
{{ page.title }}
</h1>
{# Content #}
<div class="prose prose-lg dark:prose-invert max-w-none">
{{ page.content | safe }}
</div>
{# Last updated #}
{% if page.updated_at %}
<div class="mt-12 pt-6 border-t border-gray-200 dark:border-gray-700 text-sm text-gray-500 dark:text-gray-400">
Last updated: {{ page.updated_at.strftime('%B %d, %Y') }}
</div>
{% endif %}
</div>
{% endblock %}
```
### 6. Update Footer to Load Navigation Dynamically
Edit `app/templates/storefront/base.html` to load navigation from database.
First, update the context helper to include footer pages:
```python
# app/routes/storefront_pages.py
def get_storefront_context(request: Request, **extra_context) -> dict:
# ... existing code ...
# Load footer navigation pages
db = next(get_db())
try:
footer_pages = content_page_service.list_pages_for_store(
db,
store_id=store.id if store else None,
include_unpublished=False,
footer_only=True
)
finally:
db.close()
context = {
"request": request,
"store": store,
"theme": theme,
"clean_path": clean_path,
"access_method": access_method,
"base_url": base_url,
"footer_pages": footer_pages, # Add this
**extra_context
}
return context
```
Then update the footer template:
```jinja2
{# app/templates/storefront/base.html - Footer section #}
<div>
<h4 class="font-semibold mb-4">Quick Links</h4>
<ul class="space-y-2">
{% for page in footer_pages %}
<li>
<a href="{{ base_url }}{{ page.slug }}"
class="text-gray-600 hover:text-primary dark:text-gray-400">
{{ page.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
```
### 7. Create Default Platform Pages (Script)
Create `scripts/seed/create_default_content_pages.py`:
```python
#!/usr/bin/env python3
"""Create default platform content pages."""
from sqlalchemy.orm import Session
from app.core.database import SessionLocal
from app.services.content_page_service import content_page_service
def create_defaults():
db: Session = SessionLocal()
try:
# About Us
content_page_service.create_page(
db,
slug="about",
title="About Us",
content="""
<h2>Welcome to Our Marketplace</h2>
<p>We connect quality stores with customers worldwide.</p>
<p>Our mission is to provide a seamless shopping experience...</p>
""",
is_published=True,
show_in_footer=True,
display_order=1
)
# Shipping Information
content_page_service.create_page(
db,
slug="shipping",
title="Shipping Information",
content="""
<h2>Shipping Policy</h2>
<p>We offer fast and reliable shipping...</p>
""",
is_published=True,
show_in_footer=True,
display_order=2
)
# Returns
content_page_service.create_page(
db,
slug="returns",
title="Returns & Refunds",
content="""
<h2>Return Policy</h2>
<p>30-day return policy on all items...</p>
""",
is_published=True,
show_in_footer=True,
display_order=3
)
# Privacy Policy
content_page_service.create_page(
db,
slug="privacy",
title="Privacy Policy",
content="""
<h2>Privacy Policy</h2>
<p>Your privacy is important to us...</p>
""",
is_published=True,
show_in_footer=True,
display_order=4
)
# Terms of Service
content_page_service.create_page(
db,
slug="terms",
title="Terms of Service",
content="""
<h2>Terms of Service</h2>
<p>By using our platform, you agree to...</p>
""",
is_published=True,
show_in_footer=True,
display_order=5
)
# Contact
content_page_service.create_page(
db,
slug="contact",
title="Contact Us",
content="""
<h2>Get in Touch</h2>
<p>Have questions? We'd love to hear from you!</p>
<p>Email: support@example.com</p>
""",
is_published=True,
show_in_footer=True,
display_order=6
)
# FAQ
content_page_service.create_page(
db,
slug="faq",
title="Frequently Asked Questions",
content="""
<h2>FAQ</h2>
<h3>How do I place an order?</h3>
<p>Simply browse our products...</p>
""",
is_published=True,
show_in_footer=True,
display_order=7
)
print("✅ Created default content pages successfully")
except Exception as e:
print(f"❌ Error: {e}")
db.rollback()
finally:
db.close()
if __name__ == "__main__":
create_defaults()
```
Run it:
```bash
python scripts/seed/create_default_content_pages.py
```
## Testing
### 1. Test Platform Defaults
```bash
# Create platform default
curl -X POST http://localhost:8000/api/v1/admin/content-pages/platform \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"slug": "about",
"title": "About Our Marketplace",
"content": "<h1>About</h1><p>Platform default content</p>",
"is_published": true,
"show_in_footer": true
}'
# View in storefront
curl http://localhost:8000/store/orion/about
```
### 2. Test Store Override
```bash
# Create store override
curl -X POST http://localhost:8000/api/v1/store/orion/content-pages/ \
-H "Authorization: Bearer <store_token>" \
-H "Content-Type: application/json" \
-d '{
"slug": "about",
"title": "About Orion",
"content": "<h1>About Orion</h1><p>Custom store content</p>",
"is_published": true
}'
# View in storefront (should show store content)
curl http://localhost:8000/store/orion/about
```
### 3. Test Fallback
```bash
# Delete store override
curl -X DELETE http://localhost:8000/api/v1/store/orion/content-pages/{id} \
-H "Authorization: Bearer <store_token>"
# View in storefront (should fall back to platform default)
curl http://localhost:8000/store/orion/about
```
## Summary
You now have a complete CMS system that allows:
1. **Platform admins** to create default content for all stores
2. **Stores** to override specific pages with custom content
3. **Automatic fallback** to platform defaults when store hasn't customized
4. **Dynamic navigation** loading from database
5. **SEO optimization** with meta tags
6. **Draft/Published workflow** for content management
All pages are accessible via their slug: `/about`, `/faq`, `/contact`, etc. with proper store context and routing support!

View File

@@ -0,0 +1,61 @@
# Content Management
Content pages, media library, and store themes.
## Overview
| Aspect | Detail |
|--------|--------|
| Code | `cms` |
| Classification | Core |
| Dependencies | None |
| Status | Active |
## Features
- `cms_basic` — Basic content page management
- `cms_custom_pages` — Custom page creation
- `cms_unlimited_pages` — Unlimited pages (tier-gated)
- `cms_templates` — Page templates
- `cms_seo` — SEO metadata management
- `media_library` — Media file upload and management
## Permissions
| Permission | Description |
|------------|-------------|
| `cms.view_pages` | View content pages |
| `cms.manage_pages` | Create/edit/delete pages |
| `cms.view_media` | View media library |
| `cms.manage_media` | Upload/delete media files |
| `cms.manage_themes` | Manage store themes |
## Data Model
See [Data Model](data-model.md) for full entity relationships and schema.
- **ContentPage** — Multi-language content pages with platform/store hierarchy
- **MediaFile** — Media files with optimization and folder organization
- **StoreTheme** — Theme presets, colors, fonts, and branding
## API Endpoints
| Method | Path | Description |
|--------|------|-------------|
| `*` | `/api/v1/admin/content-pages/*` | Content page CRUD |
| `*` | `/api/v1/admin/media/*` | Media library management |
| `*` | `/api/v1/admin/images/*` | Image upload/management |
| `*` | `/api/v1/admin/store-themes/*` | Theme management |
## Configuration
No module-specific configuration.
## Additional Documentation
- [Data Model](data-model.md) — Entity relationships and database schema
- [Architecture](architecture.md) — CMS architecture and database schema
- [Implementation](implementation.md) — Implementation checklist and status
- [Email Templates](email-templates.md) — Email template system architecture
- [Email Templates Guide](email-templates-guide.md) — Template customization guide
- [Media Library](media-library.md) — Media library usage guide

View File

@@ -0,0 +1,182 @@
# Media Library
The media library provides centralized management of uploaded files (images, documents) for stores. Each store has their own isolated media storage.
## Overview
- **Storage Location**: `uploads/stores/{store_id}/{folder}/`
- **Supported Types**: Images (JPG, PNG, GIF, WebP), Documents (PDF)
- **Max File Size**: 10MB per file
- **Automatic Thumbnails**: Generated for images (200x200px)
## API Endpoints
### Admin Media Management
Admins can manage media for any store:
```
GET /api/v1/admin/media/stores/{store_id} # List store's media
POST /api/v1/admin/media/stores/{store_id}/upload # Upload file
GET /api/v1/admin/media/stores/{store_id}/{id} # Get media details
DELETE /api/v1/admin/media/stores/{store_id}/{id} # Delete media
```
### Query Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `skip` | int | Pagination offset (default: 0) |
| `limit` | int | Items per page (default: 100, max: 1000) |
| `media_type` | string | Filter by type: `image`, `video`, `document` |
| `folder` | string | Filter by folder: `products`, `general`, etc. |
| `search` | string | Search by filename |
### Upload Response
```json
{
"success": true,
"message": "File uploaded successfully",
"media": {
"id": 1,
"filename": "product-image.jpg",
"file_url": "/uploads/stores/1/products/abc123.jpg",
"url": "/uploads/stores/1/products/abc123.jpg",
"thumbnail_url": "/uploads/stores/1/thumbnails/thumb_abc123.jpg",
"media_type": "image",
"file_size": 245760,
"width": 1200,
"height": 800
}
}
```
## Media Picker Component
A reusable Alpine.js component for selecting images from the media library.
### Usage in Templates
```jinja2
{% from 'shared/macros/modals.html' import media_picker_modal %}
{# Single image selection #}
{{ media_picker_modal(
id='media-picker-main',
show_var='showMediaPicker',
store_id_var='storeId',
title='Select Image'
) }}
{# Multiple image selection #}
{{ media_picker_modal(
id='media-picker-additional',
show_var='showMediaPickerAdditional',
store_id_var='storeId',
multi_select=true,
title='Select Additional Images'
) }}
```
### JavaScript Integration
Include the media picker mixin in your Alpine.js component:
```javascript
function myComponent() {
return {
...data(),
// Include media picker functionality
...mediaPickerMixin(() => this.storeId, false),
storeId: null,
// Override to handle selected image
setMainImage(media) {
this.form.image_url = media.url;
},
// Override for multiple images
addAdditionalImages(mediaList) {
const urls = mediaList.map(m => m.url);
this.form.additional_images.push(...urls);
}
};
}
```
### Media Picker Mixin API
| Property/Method | Description |
|-----------------|-------------|
| `showMediaPicker` | Boolean to show/hide main image picker modal |
| `showMediaPickerAdditional` | Boolean to show/hide additional images picker |
| `mediaPickerState` | Object containing loading, media array, selected items |
| `openMediaPickerMain()` | Open picker for main image |
| `openMediaPickerAdditional()` | Open picker for additional images |
| `loadMediaLibrary()` | Fetch media from API |
| `uploadMediaFile(event)` | Handle file upload |
| `toggleMediaSelection(media)` | Select/deselect a media item |
| `confirmMediaSelection()` | Confirm selection and call callbacks |
| `setMainImage(media)` | Override to handle main image selection |
| `addAdditionalImages(mediaList)` | Override to handle multiple selections |
## File Storage
### Directory Structure
```
uploads/
└── stores/
└── {store_id}/
├── products/ # Product images
├── general/ # General uploads
└── thumbnails/ # Auto-generated thumbnails
```
### URL Paths
Files are served from `/uploads/` path:
- Full image: `/uploads/stores/1/products/image.jpg`
- Thumbnail: `/uploads/stores/1/thumbnails/thumb_image.jpg`
## Database Model
```python
class MediaFile(Base):
id: int
store_id: int
filename: str # Stored filename (UUID-based)
original_filename: str # Original upload name
file_path: str # Relative path from uploads/
thumbnail_path: str # Thumbnail relative path
media_type: str # image, video, document
mime_type: str # image/jpeg, etc.
file_size: int # Bytes
width: int # Image width
height: int # Image height
folder: str # products, general, etc.
```
## Product Images
Products support both a main image and additional images:
```python
class Product(Base):
primary_image_url: str # Main product image
additional_images: list[str] # Array of additional image URLs
```
### In Product Forms
The product create/edit forms include:
1. **Main Image**: Single image with preview and media picker
2. **Additional Images**: Grid of images with add/remove functionality
Both support:
- Browsing the store's media library
- Uploading new images directly
- Entering external URLs manually