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:
604
app/modules/cms/docs/architecture.md
Normal file
604
app/modules/cms/docs/architecture.md
Normal 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
|
||||
115
app/modules/cms/docs/data-model.md
Normal file
115
app/modules/cms/docs/data-model.md
Normal 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
|
||||
287
app/modules/cms/docs/email-templates-guide.md
Normal file
287
app/modules/cms/docs/email-templates-guide.md
Normal 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.
|
||||
458
app/modules/cms/docs/email-templates.md
Normal file
458
app/modules/cms/docs/email-templates.md
Normal 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
|
||||
414
app/modules/cms/docs/implementation.md
Normal file
414
app/modules/cms/docs/implementation.md
Normal 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!
|
||||
61
app/modules/cms/docs/index.md
Normal file
61
app/modules/cms/docs/index.md
Normal 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
|
||||
182
app/modules/cms/docs/media-library.md
Normal file
182
app/modules/cms/docs/media-library.md
Normal 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
|
||||
Reference in New Issue
Block a user