feat: implement /platforms/ URL prefix routing strategy

- Update middleware to use /platforms/{code}/ prefix for dev routing
- Change DEFAULT_PLATFORM_CODE from 'oms' to 'main'
- Add 'main' platform for main marketing site (wizamart.lu)
- Remove hardcoded /oms and /loyalty routes from main.py
- Update platform_pages.py homepage to handle vendor landing pages

URL structure:
- localhost:9999/ → Main marketing site ('main' platform)
- localhost:9999/platforms/oms/ → OMS platform
- localhost:9999/platforms/loyalty/ → Loyalty platform
- oms.lu/ → OMS platform (production)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 18:25:56 +01:00
parent 3875ad91df
commit a2407ae418
4 changed files with 749 additions and 498 deletions

402
main.py
View File

@@ -373,320 +373,29 @@ async def vendor_root_path(
# ============================================================================
# PLATFORM PUBLIC PAGES (Platform Homepage, About, FAQ, etc.)
# PLATFORM ROUTING (via PlatformContextMiddleware)
#
# The PlatformContextMiddleware handles all platform routing by rewriting paths:
# - /platforms/oms/ → rewrites to / (served by platform_pages router)
# - /platforms/oms/pricing → rewrites to /pricing (served by platform_pages router)
# - /platforms/loyalty/ → rewrites to / (served by platform_pages router)
#
# The middleware also sets request.state.platform so handlers know which
# platform's content to serve. All platform page routes are defined in
# app/routes/platform_pages.py which is included above.
#
# URL Structure (Development - localhost:9999):
# - / → Main marketing site ('main' platform)
# - /about → Main marketing site about page
# - /platforms/oms/ → OMS platform homepage
# - /platforms/oms/pricing → OMS platform pricing page
# - /platforms/loyalty/ → Loyalty platform homepage
#
# URL Structure (Production - domain-based):
# - wizamart.lu/ → Main marketing site
# - oms.lu/ → OMS platform homepage
# - loyalty.lu/ → Loyalty platform homepage
# ============================================================================
logger.info("Registering platform public page routes:")
logger.info(" - / (platform homepage)")
logger.info(" - /{platform_code}/ (platform-prefixed homepage for dev mode)")
logger.info(" - /{platform_code}/{slug} (platform-prefixed content pages)")
logger.info(" - /{slug} (platform content pages: /about, /faq, /terms, /contact)")
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
async def platform_homepage(request: Request, db: Session = Depends(get_db)):
"""
Platform homepage at localhost:8000 or platform.com
Uses multi-platform CMS with three-tier resolution:
1. Platform marketing pages (is_platform_page=True)
2. Vendor default pages (fallback)
3. Vendor override pages
Falls back to default static template if not found.
"""
from app.services.content_page_service import content_page_service
logger.debug("[PLATFORM] Homepage requested")
# Get platform from middleware (multi-platform support)
platform = getattr(request.state, "platform", None)
if platform:
# Try to load platform homepage from CMS (platform marketing page)
homepage = content_page_service.get_platform_page(
db,
platform_id=platform.id,
slug="home",
include_unpublished=False,
)
# Also try platform_homepage slug for backwards compatibility
if not homepage:
homepage = content_page_service.get_platform_page(
db,
platform_id=platform.id,
slug="platform_homepage",
include_unpublished=False,
)
# Load header and footer navigation (platform marketing pages)
header_pages = content_page_service.list_platform_pages(
db, platform_id=platform.id, header_only=True, include_unpublished=False
)
footer_pages = content_page_service.list_platform_pages(
db, platform_id=platform.id, footer_only=True, include_unpublished=False
)
else:
# Fallback for when no platform context (shouldn't happen normally)
homepage = None
header_pages = []
footer_pages = []
# Get language from request state and build i18n context
language = getattr(request.state, "language", "fr")
i18n_globals = get_jinja2_globals(language)
if homepage:
# Use template selection from CMS
template_name = homepage.template or "default"
template_path = f"platform/homepage-{template_name}.html"
logger.info(f"[PLATFORM] Rendering CMS homepage with template: {template_path}")
context = {
"request": request,
"page": homepage,
"header_pages": header_pages,
"footer_pages": footer_pages,
}
context.update(i18n_globals)
return templates.TemplateResponse(template_path, context)
# Fallback to default static template
logger.info("[PLATFORM] No CMS homepage found, using default template")
context = {
"request": request,
"header_pages": header_pages,
"footer_pages": footer_pages,
}
context.update(i18n_globals)
return templates.TemplateResponse("platform/homepage-default.html", context)
# ============================================================================
# PLATFORM-PREFIXED ROUTES (Development mode: /oms/, /loyalty/, etc.)
# These routes handle path-based platform routing in development.
# In production, domain-based routing means these won't be needed.
# ============================================================================
async def _serve_platform_homepage(request: Request, platform_code: str, db: Session):
"""Helper function to serve platform homepage."""
from app.services.content_page_service import content_page_service
from models.database.platform import Platform
# Get platform from middleware or query directly
platform = getattr(request.state, "platform", None)
if not platform:
platform = db.query(Platform).filter(
Platform.code == platform_code.lower(),
Platform.is_active == True
).first()
if not platform:
raise HTTPException(status_code=404, detail=f"Platform not found: {platform_code}")
# Load platform homepage from CMS
homepage = content_page_service.get_platform_page(
db, platform_id=platform.id, slug="home", include_unpublished=False
)
if not homepage:
homepage = content_page_service.get_platform_page(
db, platform_id=platform.id, slug="platform_homepage", include_unpublished=False
)
# Load navigation
header_pages = content_page_service.list_platform_pages(
db, platform_id=platform.id, header_only=True, include_unpublished=False
)
footer_pages = content_page_service.list_platform_pages(
db, platform_id=platform.id, footer_only=True, include_unpublished=False
)
# Build context
language = getattr(request.state, "language", "fr")
i18n_globals = get_jinja2_globals(language)
if homepage:
template_name = homepage.template or "default"
template_path = f"platform/homepage-{template_name}.html"
logger.info(f"[PLATFORM] Rendering {platform.code} homepage: {template_path}")
context = {
"request": request,
"page": homepage,
"platform": platform,
"header_pages": header_pages,
"footer_pages": footer_pages,
}
context.update(i18n_globals)
return templates.TemplateResponse(template_path, context)
# Fallback
logger.info(f"[PLATFORM] No CMS homepage for {platform.code}, using default")
context = {
"request": request,
"platform": platform,
"header_pages": header_pages,
"footer_pages": footer_pages,
}
context.update(i18n_globals)
return templates.TemplateResponse("platform/homepage-default.html", context)
async def _serve_platform_content_page(request: Request, platform_code: str, slug: str, db: Session):
"""Helper function to serve platform content page."""
from app.services.content_page_service import content_page_service
from models.database.platform import Platform
# Get platform
platform = getattr(request.state, "platform", None)
if not platform:
platform = db.query(Platform).filter(
Platform.code == platform_code.lower(),
Platform.is_active == True
).first()
if not platform:
raise HTTPException(status_code=404, detail=f"Platform not found: {platform_code}")
# Load content page
page = content_page_service.get_platform_page(
db, platform_id=platform.id, slug=slug, include_unpublished=False
)
if not page:
logger.warning(f"[PLATFORM] Page not found: /{platform_code}/{slug}")
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
# Load navigation
header_pages = content_page_service.list_platform_pages(
db, platform_id=platform.id, header_only=True, include_unpublished=False
)
footer_pages = content_page_service.list_platform_pages(
db, platform_id=platform.id, footer_only=True, include_unpublished=False
)
logger.info(f"[PLATFORM] Rendering {platform.code} page: {page.title} (/{slug})")
language = getattr(request.state, "language", "fr")
i18n_globals = get_jinja2_globals(language)
context = {
"request": request,
"page": page,
"platform": platform,
"header_pages": header_pages,
"footer_pages": footer_pages,
}
context.update(i18n_globals)
return templates.TemplateResponse("platform/content-page.html", context)
# Explicit routes for OMS platform
@app.get("/oms", response_class=RedirectResponse, include_in_schema=False)
async def oms_redirect():
"""Redirect /oms to /oms/"""
return RedirectResponse(url="/oms/", status_code=307)
@app.get("/oms/", response_class=HTMLResponse, include_in_schema=False)
async def oms_homepage(request: Request, db: Session = Depends(get_db)):
"""OMS platform homepage"""
return await _serve_platform_homepage(request, "oms", db)
@app.get("/oms/{slug}", response_class=HTMLResponse, include_in_schema=False)
async def oms_content_page(request: Request, slug: str, db: Session = Depends(get_db)):
"""OMS platform content pages"""
return await _serve_platform_content_page(request, "oms", slug, db)
# Explicit routes for Loyalty platform
@app.get("/loyalty", response_class=RedirectResponse, include_in_schema=False)
async def loyalty_redirect():
"""Redirect /loyalty to /loyalty/"""
return RedirectResponse(url="/loyalty/", status_code=307)
@app.get("/loyalty/", response_class=HTMLResponse, include_in_schema=False)
async def loyalty_homepage(request: Request, db: Session = Depends(get_db)):
"""Loyalty platform homepage"""
return await _serve_platform_homepage(request, "loyalty", db)
@app.get("/loyalty/{slug}", response_class=HTMLResponse, include_in_schema=False)
async def loyalty_content_page(request: Request, slug: str, db: Session = Depends(get_db)):
"""Loyalty platform content pages"""
return await _serve_platform_content_page(request, "loyalty", slug, db)
# ============================================================================
# GENERIC PLATFORM CONTENT PAGE (must be LAST - catches all /{slug})
# ============================================================================
@app.get("/{slug}", response_class=HTMLResponse, include_in_schema=False)
async def platform_content_page(
request: Request, slug: str, db: Session = Depends(get_db)
):
"""
Platform content pages: /about, /faq, /terms, /contact, etc.
Uses multi-platform CMS with three-tier resolution.
Returns 404 if page not found.
This route MUST be defined LAST to avoid conflicts with other routes.
"""
from app.services.content_page_service import content_page_service
logger.debug(f"[PLATFORM] Content page requested: /{slug}")
# Get platform from middleware (multi-platform support)
platform = getattr(request.state, "platform", None)
if not platform:
logger.warning(f"[PLATFORM] No platform context for content page: {slug}")
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
# Load platform marketing page from CMS
page = content_page_service.get_platform_page(
db,
platform_id=platform.id,
slug=slug,
include_unpublished=False,
)
if not page:
logger.warning(f"[PLATFORM] Content page not found: {slug}")
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
# Load header and footer navigation (platform marketing pages)
header_pages = content_page_service.list_platform_pages(
db, platform_id=platform.id, header_only=True, include_unpublished=False
)
footer_pages = content_page_service.list_platform_pages(
db, platform_id=platform.id, footer_only=True, include_unpublished=False
)
logger.info(f"[PLATFORM] Rendering content page: {page.title} (/{slug})")
# Get language from request state and build i18n context
language = getattr(request.state, "language", "fr")
i18n_globals = get_jinja2_globals(language)
context = {
"request": request,
"page": page,
"header_pages": header_pages,
"footer_pages": footer_pages,
}
context.update(i18n_globals)
return templates.TemplateResponse("platform/content-page.html", context)
logger.info("=" * 80)
@@ -701,73 +410,6 @@ for route in app.routes:
logger.info(f" {methods:<10} {route.path:<60}")
logger.info("=" * 80)
# ============================================================================
# API ROUTES (JSON Responses)
# ============================================================================
# Public Routes (no authentication required)
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
async def root(request: Request, db: Session = Depends(get_db)):
"""
Smart root handler:
- If vendor detected (domain/subdomain): Show vendor landing page or redirect to shop
- If no vendor (platform root): Redirect to documentation
"""
vendor = getattr(request.state, "vendor", None)
platform = getattr(request.state, "platform", None)
if vendor:
# Vendor context detected - serve landing page
from app.services.content_page_service import content_page_service
# Get platform_id (use platform from context or default to 1 for OMS)
platform_id = platform.id if platform else 1
# Try to find landing page (slug='landing' or 'home') with three-tier resolution
landing_page = content_page_service.get_page_for_vendor(
db, platform_id=platform_id, slug="landing", vendor_id=vendor.id, include_unpublished=False
)
if not landing_page:
# Try 'home' slug as fallback
landing_page = content_page_service.get_page_for_vendor(
db, platform_id=platform_id, slug="home", vendor_id=vendor.id, include_unpublished=False
)
if landing_page:
# Render landing page with selected template
from app.routes.shop_pages import get_shop_context
template_name = landing_page.template or "default"
template_path = f"vendor/landing-{template_name}.html"
return templates.TemplateResponse(
template_path, get_shop_context(request, db=db, page=landing_page)
)
# No landing page - redirect to shop
vendor_context = getattr(request.state, "vendor_context", None)
access_method = (
vendor_context.get("detection_method", "unknown")
if vendor_context
else "unknown"
)
if access_method == "path":
full_prefix = (
vendor_context.get("full_prefix", "/vendor/")
if vendor_context
else "/vendor/"
)
return RedirectResponse(
url=f"{full_prefix}{vendor.subdomain}/shop/", status_code=302
)
# Domain/subdomain
return RedirectResponse(url="/shop/", status_code=302)
# No vendor - platform root
return RedirectResponse(url="/documentation")
@app.get("/documentation", response_class=HTMLResponse, include_in_schema=False)
async def documentation():
"""Redirect to documentation"""