Files
orion/models/database/content_page.py
Samir Boulahtit b7bf505a61 feat: implement vendor landing pages with multi-template support and fix shop routing
Major improvements to shop URL routing and vendor landing page system:

## Landing Page System
- Add template field to ContentPage model for flexible landing page designs
- Create 4 landing page templates: default, minimal, modern, and full
- Implement smart root handler to serve landing pages or redirect to shop
- Add create_landing_page.py script for easy landing page management
- Support both domain/subdomain and path-based vendor access
- Add comprehensive landing page documentation

## Route Fixes
- Fix duplicate /shop prefix in shop_pages.py routes
- Correct product detail page routing (was /shop/shop/products/{id})
- Update all shop routes to work with router prefix mounting
- Remove unused public vendor endpoints (/api/v1/public/vendors)

## Template Link Corrections
- Fix all shop template links to include /shop/ prefix
- Update breadcrumb 'Home' links to point to vendor root (landing page)
- Update header navigation 'Home' link to point to vendor root
- Correct CMS page links in footer navigation
- Fix account, cart, and error page navigation links

## Navigation Architecture
- Establish two-tier navigation: landing page (/) and shop (/shop/)
- Document complete navigation flow and URL hierarchy
- Support for vendors with or without landing pages (auto-redirect fallback)
- Consistent breadcrumb and header navigation behavior

## Documentation
- Add vendor-landing-pages.md feature documentation
- Add navigation-flow.md with complete URL hierarchy
- Update shop architecture docs with error handling section
- Add orphaned docs to mkdocs.yml navigation
- Document multi-access routing patterns

## Database
- Migration f68d8da5315a: add template field to content_pages table
- Support template values: default, minimal, modern, full

This establishes a complete landing page system allowing vendors to have
custom marketing homepages separate from their e-commerce shop, with
flexible template options and proper navigation hierarchy.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 00:10:45 +01:00

138 lines
5.2 KiB
Python

# models/database/content_page.py
"""
Content Page Model
Manages static content pages (About, FAQ, Contact, Shipping, Returns, etc.)
with platform-level defaults and vendor-specific overrides.
Features:
- Platform-level default content
- Vendor-specific overrides
- Rich text content (HTML/Markdown)
- SEO metadata
- Published/Draft status
- Version history support
"""
from datetime import datetime, timezone
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, Index
from sqlalchemy.orm import relationship
from app.core.database import Base
class ContentPage(Base):
"""
Content pages for shops (About, FAQ, Contact, etc.)
Two-tier system:
1. Platform-level defaults (vendor_id=NULL)
2. Vendor-specific overrides (vendor_id=123)
Lookup logic:
1. Check for vendor-specific page (vendor_id + slug)
2. If not found, use platform default (slug only)
3. If neither exists, show 404 or default template
"""
__tablename__ = "content_pages"
id = Column(Integer, primary_key=True, index=True)
# Vendor association (NULL = platform default)
vendor_id = Column(Integer, ForeignKey("vendors.id", ondelete="CASCADE"), nullable=True, index=True)
# Page identification
slug = Column(String(100), nullable=False, index=True) # about, faq, contact, shipping, returns, etc.
title = Column(String(200), nullable=False)
# Content
content = Column(Text, nullable=False) # HTML or Markdown
content_format = Column(String(20), default="html") # html, markdown
# Template selection (for landing pages)
# Options: 'default', 'minimal', 'modern', 'full'
# Only used for landing pages (slug='landing' or 'home')
template = Column(String(50), default="default", nullable=False)
# SEO
meta_description = Column(String(300), nullable=True)
meta_keywords = Column(String(300), nullable=True)
# Publishing
is_published = Column(Boolean, default=False, nullable=False)
published_at = Column(DateTime(timezone=True), nullable=True)
# Ordering (for menus, footers)
display_order = Column(Integer, default=0)
show_in_footer = Column(Boolean, default=True)
show_in_header = Column(Boolean, default=False)
# Timestamps
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False)
# Author tracking (admin or vendor user who created/updated)
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
updated_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
# Relationships
vendor = relationship("Vendor", back_populates="content_pages")
creator = relationship("User", foreign_keys=[created_by])
updater = relationship("User", foreign_keys=[updated_by])
# Constraints
__table_args__ = (
# Unique combination: vendor can only have one page per slug
# Platform defaults (vendor_id=NULL) can only have one page per slug
UniqueConstraint('vendor_id', 'slug', name='uq_vendor_slug'),
# Indexes for performance
Index('idx_vendor_published', 'vendor_id', 'is_published'),
Index('idx_slug_published', 'slug', 'is_published'),
)
def __repr__(self):
vendor_name = self.vendor.name if self.vendor else "PLATFORM"
return f"<ContentPage(id={self.id}, vendor={vendor_name}, slug={self.slug}, title={self.title})>"
@property
def is_platform_default(self):
"""Check if this is a platform-level default page."""
return self.vendor_id is None
@property
def is_vendor_override(self):
"""Check if this is a vendor-specific override."""
return self.vendor_id is not None
def to_dict(self):
"""Convert to dictionary for API responses."""
return {
"id": self.id,
"vendor_id": self.vendor_id,
"vendor_name": self.vendor.name if self.vendor else None,
"slug": self.slug,
"title": self.title,
"content": self.content,
"content_format": self.content_format,
"template": self.template,
"meta_description": self.meta_description,
"meta_keywords": self.meta_keywords,
"is_published": self.is_published,
"published_at": self.published_at.isoformat() if self.published_at else None,
"display_order": self.display_order,
"show_in_footer": self.show_in_footer,
"show_in_header": self.show_in_header,
"is_platform_default": self.is_platform_default,
"is_vendor_override": self.is_vendor_override,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"created_by": self.created_by,
"updated_by": self.updated_by,
}
# Add relationship to Vendor model
# This should be added to models/database/vendor.py:
# content_pages = relationship("ContentPage", back_populates="vendor", cascade="all, delete-orphan")