- Add "Show in Legal" checkbox to content page editor UI - Update API schemas (ContentPageCreate, ContentPageUpdate, ContentPageResponse) - Add show_in_legal parameter to service methods (create_page, update_page, etc.) - Fix ContentPageNotFoundException to pass identifier correctly - Fix UnauthorizedContentPageAccessException to use correct AuthorizationException API - Add comprehensive unit tests for ContentPageService (35 tests) - Add content page fixtures for testing - Update CMS documentation with navigation categories diagram 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
17 KiB
Content Management System (CMS)
Overview
The Content Management System allows platform administrators and vendors to manage static content pages like About, FAQ, Contact, Shipping, Returns, Privacy Policy, Terms of Service, etc.
Key Features:
- ✅ Platform-level default content
- ✅ Vendor-specific overrides
- ✅ Fallback system (vendor → 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 vendor-specific override
↓
SELECT * FROM content_pages
WHERE vendor_id = 123 AND slug = 'about' AND is_published = true
↓
Found? ✅ Return vendor content
❌ Continue to step 2
2. Check for platform default
↓
SELECT * FROM content_pages
WHERE vendor_id IS NULL AND slug = 'about' AND is_published = true
↓
Found? ✅ Return platform content
❌ Return 404 or default template
Database Schema
CREATE TABLE content_pages (
id SERIAL PRIMARY KEY,
-- Vendor association (NULL = platform default)
vendor_id INTEGER REFERENCES vendors(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_vendor_slug UNIQUE (vendor_id, slug)
);
CREATE INDEX idx_vendor_published ON content_pages (vendor_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 vendors inherit:
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 Uscontact- Contact Usfaq- Frequently Asked Questionsshipping- Shipping Informationreturns- Return Policyprivacy- Privacy Policyterms- Terms of Servicehelp- Help Center
2. View All Content Pages
GET /api/v1/admin/content-pages/
GET /api/v1/admin/content-pages/?vendor_id=123 # Filter by vendor
GET /api/v1/admin/content-pages/platform # Only platform defaults
3. Update Platform Default
PUT /api/v1/admin/content-pages/1
{
"title": "Updated About Us",
"content": "<h1>About Our Platform</h1>...",
"is_published": true
}
Vendor Workflow
1. View Available Pages
Vendors see their overrides + platform defaults:
GET /api/v1/vendor/{code}/content-pages/
Response:
[
{
"id": 15,
"slug": "about",
"title": "About Wizamart", // Vendor override
"is_vendor_override": true,
"is_platform_default": false
},
{
"id": 2,
"slug": "shipping",
"title": "Shipping Information", // Platform default
"is_vendor_override": false,
"is_platform_default": true
}
]
2. Create Vendor Override
Vendor creates custom "About" page:
POST /api/v1/vendor/{code}/content-pages/
{
"slug": "about",
"title": "About Wizamart",
"content": "<h1>About Wizamart</h1><p>We specialize in...</p>",
"is_published": true
}
This overrides the platform default for this vendor only.
3. View Only Vendor Overrides
GET /api/v1/vendor/{code}/content-pages/overrides
Shows what the vendor has customized (excludes platform defaults).
4. Delete Override (Revert to Platform Default)
DELETE /api/v1/vendor/{code}/content-pages/15
After deletion, platform default will be shown again.
Shop Frontend (Public)
1. Get Page Content
GET /api/v1/shop/content-pages/about
Automatically uses vendor context from middleware:
- Returns vendor override if exists
- Falls back to platform default
- Returns 404 if neither exists
2. Get Navigation Links
# Get all navigation pages
GET /api/v1/shop/content-pages/navigation
# Filter by placement
GET /api/v1/shop/content-pages/navigation?header_only=true
GET /api/v1/shop/content-pages/navigation?footer_only=true
GET /api/v1/shop/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
│ ├── vendor/
│ │ └── content_pages.py ← Vendor API endpoints
│ └── shop/
│ └── content_pages.py ← Public API endpoints
│
└── templates/shop/
├── about.html ← Content page template
├── faq.html
├── contact.html
└── ...
Template Integration
Generic Content Page Template
Create a reusable template for all content pages:
{# app/templates/shop/content-page.html #}
{% extends "shop/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
# app/routes/shop_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 vendor override support.
"""
vendor = getattr(request.state, 'vendor', None)
vendor_id = vendor.id if vendor else None
page = content_page_service.get_page_for_vendor(
db,
slug=slug,
vendor_id=vendor_id,
include_unpublished=False
)
if not page:
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
return templates.TemplateResponse(
"shop/content-page.html",
get_shop_context(request, page=page)
)
Dynamic Footer Navigation
Update footer to load links from database:
{# app/templates/shop/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:
<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:
# About Us
We are a **leading marketplace** for...
- Quality products
- Fast shipping
- Great support
2. SEO Optimization
Always provide meta descriptions:
{
"meta_description": "Learn about our marketplace, mission, and values. We connect vendors with customers worldwide.",
"meta_keywords": "about us, marketplace, e-commerce, mission"
}
3. Draft → Published Workflow
- Create page with
is_published: false - Preview using
include_unpublished=trueparameter - Review and edit
- 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 │ • Vendor │ • Phone │ • LinkedIn │ │
│ │ • Contact │ │ │ │ │
│ │ • Shipping │ │ │ │ │
│ └──────────────┴──────────────┴────────────┴──────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ LEGAL BAR (show_in_legal=true) │
│ © 2025 Wizamart 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:
# 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 vendor override back to platform default:
# Vendor deletes their custom page
DELETE /api/v1/vendor/{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
- HTML Sanitization: If using HTML format, sanitize user input to prevent XSS
- Authorization: Vendors can only edit their own pages
- Published Status: Only published pages visible to public
- Vendor Isolation: Vendors cannot see/edit other vendor's content
Migration Strategy
Initial Setup
- Create Platform Defaults:
python scripts/create_default_content_pages.py
- Migrate Existing Static Templates:
- Convert existing HTML templates to database content
- Preserve existing URLs and SEO
- 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/vendor 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 vendor 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
Vendor Endpoints
GET /api/v1/vendor/{code}/content-pages/ # List all (vendor + platform)
GET /api/v1/vendor/{code}/content-pages/overrides # List vendor overrides only
GET /api/v1/vendor/{code}/content-pages/{slug} # Get specific page
POST /api/v1/vendor/{code}/content-pages/ # Create vendor override
PUT /api/v1/vendor/{code}/content-pages/{id} # Update vendor page
DELETE /api/v1/vendor/{code}/content-pages/{id} # Delete vendor page
Shop (Public) Endpoints
GET /api/v1/shop/content-pages/navigation # Get navigation links
GET /api/v1/shop/content-pages/{slug} # Get page content
Example: Complete Workflow
1. Platform Admin Creates Defaults:
# 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 Vendors See Platform Default:
- Vendor A visits:
vendor-a.com/about→ Shows platform default - Vendor B visits:
vendor-b.com/about→ Shows platform default
3. Vendor A Creates Override:
curl -X POST /api/v1/vendor/vendor-a/content-pages/ \
-H "Authorization: Bearer <vendor_token>" \
-d '{
"slug": "about",
"title": "About Vendor A",
"content": "<h1>About Vendor A</h1><p>Custom content...</p>",
"is_published": true
}'
4. Now:
- Vendor A visits:
vendor-a.com/about→ Shows Vendor A custom content - Vendor B visits:
vendor-b.com/about→ Still shows platform default
5. Vendor A Reverts to Default:
curl -X DELETE /api/v1/vendor/vendor-a/content-pages/15 \
-H "Authorization: Bearer <vendor_token>"
6. Result:
- Vendor A visits:
vendor-a.com/about→ Shows platform default again