From adbecd360b409e5b4181b5092987bcb58eb6aed3 Mon Sep 17 00:00:00 2001
From: Samir Boulahtit
Date: Sun, 1 Mar 2026 12:12:20 +0100
Subject: [PATCH] feat(cms): CMS-driven homepages, products section,
placeholder resolution
- Add ProductCard/ProductsSection schema and _products.html section macro
- Rewrite seed script with 3-platform homepage sections (wizard, OMS, loyalty),
platform marketing pages, and store defaults with {{store_name}} placeholders
- Add resolve_placeholders() to ContentPageService for store default pages
- Fix SQLAlchemy filter bugs: replace Python `is None` with `.is_(None)` across
all ContentPageService query methods (was silently breaking all platform page lookups)
- Remove hardcoded orion fallback and delete homepage-orion.html
- Add placeholder hint box with click-to-copy in admin content page editor
- Export ProductCard/ProductsSection from cms schemas __init__
Co-Authored-By: Claude Opus 4.6
---
app/modules/cms/routes/pages/platform.py | 39 +-
app/modules/cms/routes/pages/storefront.py | 10 +-
app/modules/cms/schemas/__init__.py | 4 +
app/modules/cms/schemas/homepage_sections.py | 26 +-
.../cms/services/content_page_service.py | 66 +-
.../cms/admin/content-page-edit.html | 29 +
.../cms/platform/homepage-default.html | 6 +
.../cms/platform/homepage-orion.html | 427 ------
.../cms/platform/sections/_products.html | 91 ++
.../cms/storefront/content-page.html | 9 +-
scripts/seed/create_default_content_pages.py | 1341 +++++++++++------
11 files changed, 1132 insertions(+), 916 deletions(-)
delete mode 100644 app/modules/cms/templates/cms/platform/homepage-orion.html
create mode 100644 app/modules/cms/templates/cms/platform/sections/_products.html
diff --git a/app/modules/cms/routes/pages/platform.py b/app/modules/cms/routes/pages/platform.py
index d9929a1a..50939617 100644
--- a/app/modules/cms/routes/pages/platform.py
+++ b/app/modules/cms/routes/pages/platform.py
@@ -164,46 +164,13 @@ async def homepage(
logger.info(f"[HOMEPAGE] Rendering CMS homepage with template: {template_path}")
return templates.TemplateResponse(template_path, context)
- # Fallback: Default orion homepage (no CMS content)
- logger.info("[HOMEPAGE] No CMS homepage found, using default orion template")
+ # Fallback: Default homepage template with placeholder content
+ logger.info("[HOMEPAGE] No CMS homepage found, using default template with placeholders")
context = get_platform_context(request, db)
context["tiers"] = _get_tiers_data(db)
- # Add-ons (hardcoded for now, will come from DB)
- context["addons"] = [
- {
- "code": "domain",
- "name": "Custom Domain",
- "description": "Use your own domain (mydomain.com)",
- "price": 15,
- "billing_period": "year",
- "icon": "globe",
- },
- {
- "code": "ssl_premium",
- "name": "Premium SSL",
- "description": "EV certificate for trust badges",
- "price": 49,
- "billing_period": "year",
- "icon": "shield-check",
- },
- {
- "code": "email",
- "name": "Email Package",
- "description": "Professional email addresses",
- "price": 5,
- "billing_period": "month",
- "icon": "mail",
- "options": [
- {"quantity": 5, "price": 5},
- {"quantity": 10, "price": 9},
- {"quantity": 25, "price": 19},
- ],
- },
- ]
-
return templates.TemplateResponse(
- "cms/platform/homepage-orion.html",
+ "cms/platform/homepage-default.html",
context,
)
diff --git a/app/modules/cms/routes/pages/storefront.py b/app/modules/cms/routes/pages/storefront.py
index a9acb254..92dbd1cf 100644
--- a/app/modules/cms/routes/pages/storefront.py
+++ b/app/modules/cms/routes/pages/storefront.py
@@ -101,9 +101,17 @@ async def generic_content_page(
},
)
+ # Resolve placeholders in store default pages ({{store_name}}, etc.)
+ page_content = page.content
+ if page.is_store_default and store:
+ page_content = content_page_service.resolve_placeholders(page.content, store)
+
+ context = get_storefront_context(request, db=db, page=page)
+ context["page_content"] = page_content
+
return templates.TemplateResponse(
"cms/storefront/content-page.html",
- get_storefront_context(request, db=db, page=page),
+ context,
)
diff --git a/app/modules/cms/schemas/__init__.py b/app/modules/cms/schemas/__init__.py
index de04a9ec..3cb28100 100644
--- a/app/modules/cms/schemas/__init__.py
+++ b/app/modules/cms/schemas/__init__.py
@@ -31,6 +31,8 @@ from app.modules.cms.schemas.homepage_sections import (
HomepageSections,
HomepageSectionsResponse,
PricingSection,
+ ProductCard,
+ ProductsSection,
# API schemas
SectionUpdateRequest,
# Translatable text
@@ -92,6 +94,8 @@ __all__ = [
"HeroSection",
"FeatureCard",
"FeaturesSection",
+ "ProductCard",
+ "ProductsSection",
"PricingSection",
"CTASection",
"HomepageSections",
diff --git a/app/modules/cms/schemas/homepage_sections.py b/app/modules/cms/schemas/homepage_sections.py
index 4f9b9e14..ff81f0c6 100644
--- a/app/modules/cms/schemas/homepage_sections.py
+++ b/app/modules/cms/schemas/homepage_sections.py
@@ -77,6 +77,25 @@ class FeatureCard(BaseModel):
description: TranslatableText = Field(default_factory=TranslatableText)
+class ProductCard(BaseModel):
+ """Single product/offering card in products section."""
+
+ icon: str = ""
+ title: TranslatableText = Field(default_factory=TranslatableText)
+ description: TranslatableText = Field(default_factory=TranslatableText)
+ url: str = ""
+ badge: TranslatableText | None = None
+
+
+class ProductsSection(BaseModel):
+ """Product/offering showcase section (e.g. wizard.lu multi-product landing)."""
+
+ enabled: bool = True
+ title: TranslatableText = Field(default_factory=TranslatableText)
+ subtitle: TranslatableText | None = None
+ products: list[ProductCard] = Field(default_factory=list)
+
+
class FeaturesSection(BaseModel):
"""Features section configuration."""
@@ -114,6 +133,7 @@ class HomepageSections(BaseModel):
"""Complete homepage sections structure."""
hero: HeroSection | None = None
+ products: ProductsSection | None = None
features: FeaturesSection | None = None
pricing: PricingSection | None = None
cta: CTASection | None = None
@@ -139,6 +159,10 @@ class HomepageSections(BaseModel):
subtitle=make_translatable(languages),
buttons=[],
),
+ products=ProductsSection(
+ title=make_translatable(languages),
+ products=[],
+ ),
features=FeaturesSection(
title=make_translatable(languages),
features=[],
@@ -162,7 +186,7 @@ class HomepageSections(BaseModel):
class SectionUpdateRequest(BaseModel):
"""Request to update a single section."""
- section_name: str = Field(..., description="hero, features, pricing, or cta")
+ section_name: str = Field(..., description="hero, products, features, pricing, or cta")
section_data: dict = Field(..., description="Section configuration")
diff --git a/app/modules/cms/services/content_page_service.py b/app/modules/cms/services/content_page_service.py
index 8631ecd2..e42274f7 100644
--- a/app/modules/cms/services/content_page_service.py
+++ b/app/modules/cms/services/content_page_service.py
@@ -142,8 +142,8 @@ class ContentPageService:
db.query(ContentPage)
.filter(
and_(
- ContentPage.store_id is None,
- ContentPage.is_platform_page == False,
+ ContentPage.store_id.is_(None),
+ ContentPage.is_platform_page.is_(False),
*base_filters,
)
)
@@ -182,12 +182,12 @@ class ContentPageService:
filters = [
ContentPage.platform_id == platform_id,
ContentPage.slug == slug,
- ContentPage.store_id is None,
- ContentPage.is_platform_page == True,
+ ContentPage.store_id.is_(None),
+ ContentPage.is_platform_page.is_(True),
]
if not include_unpublished:
- filters.append(ContentPage.is_published == True)
+ filters.append(ContentPage.is_published.is_(True))
page = db.query(ContentPage).filter(and_(*filters)).first()
@@ -255,8 +255,8 @@ class ContentPageService:
db.query(ContentPage)
.filter(
and_(
- ContentPage.store_id is None,
- ContentPage.is_platform_page == False,
+ ContentPage.store_id.is_(None),
+ ContentPage.is_platform_page.is_(False),
*base_filters,
)
)
@@ -298,12 +298,12 @@ class ContentPageService:
"""
filters = [
ContentPage.platform_id == platform_id,
- ContentPage.store_id is None,
- ContentPage.is_platform_page == True,
+ ContentPage.store_id.is_(None),
+ ContentPage.is_platform_page.is_(True),
]
if not include_unpublished:
- filters.append(ContentPage.is_published == True)
+ filters.append(ContentPage.is_published.is_(True))
if footer_only:
filters.append(ContentPage.show_in_footer == True)
@@ -377,12 +377,12 @@ class ContentPageService:
"""
filters = [
ContentPage.platform_id == platform_id,
- ContentPage.store_id is None,
- ContentPage.is_platform_page == False,
+ ContentPage.store_id.is_(None),
+ ContentPage.is_platform_page.is_(False),
]
if not include_unpublished:
- filters.append(ContentPage.is_published == True)
+ filters.append(ContentPage.is_published.is_(True))
return (
db.query(ContentPage)
@@ -845,13 +845,13 @@ class ContentPageService:
filters.append(ContentPage.is_published == True)
if page_tier == "platform":
- filters.append(ContentPage.is_platform_page == True)
- filters.append(ContentPage.store_id is None)
+ filters.append(ContentPage.is_platform_page.is_(True))
+ filters.append(ContentPage.store_id.is_(None))
elif page_tier == "store_default":
- filters.append(ContentPage.is_platform_page == False)
- filters.append(ContentPage.store_id is None)
+ filters.append(ContentPage.is_platform_page.is_(False))
+ filters.append(ContentPage.store_id.is_(None))
elif page_tier == "store_override":
- filters.append(ContentPage.store_id is not None)
+ filters.append(ContentPage.store_id.isnot(None))
return (
db.query(ContentPage)
@@ -958,6 +958,34 @@ class ContentPageService:
if not success:
raise ContentPageNotFoundException(identifier=page_id)
+ # =========================================================================
+ # Placeholder Resolution (for store default pages)
+ # =========================================================================
+
+ @staticmethod
+ def resolve_placeholders(content: str, store) -> str:
+ """
+ Replace {{store_name}}, {{store_email}}, {{store_phone}} placeholders
+ in store default page content with actual store values.
+
+ Args:
+ content: HTML content with placeholders
+ store: Store object with name, contact_email, phone attributes
+
+ Returns:
+ Content with placeholders replaced
+ """
+ if not content or not store:
+ return content or ""
+ replacements = {
+ "{{store_name}}": store.name or "Our Store",
+ "{{store_email}}": getattr(store, "contact_email", "") or "",
+ "{{store_phone}}": getattr(store, "phone", "") or "",
+ }
+ for placeholder, value in replacements.items():
+ content = content.replace(placeholder, value)
+ return content
+
# =========================================================================
# Homepage Sections Management
# =========================================================================
@@ -1032,10 +1060,12 @@ class ContentPageService:
FeaturesSection,
HeroSection,
PricingSection,
+ ProductsSection,
)
SECTION_SCHEMAS = {
"hero": HeroSection,
+ "products": ProductsSection,
"features": FeaturesSection,
"pricing": PricingSection,
"cta": CTASection,
diff --git a/app/modules/cms/templates/cms/admin/content-page-edit.html b/app/modules/cms/templates/cms/admin/content-page-edit.html
index 35580152..dabe531c 100644
--- a/app/modules/cms/templates/cms/admin/content-page-edit.html
+++ b/app/modules/cms/templates/cms/admin/content-page-edit.html
@@ -187,6 +187,35 @@
+
+ {# Available Placeholders (for store default pages) #}
+ {% set placeholders = [
+ ('store_name', "The store's display name"),
+ ('store_email', "The store's contact email"),
+ ('store_phone', "The store's phone number"),
+ ] %}
+
+
+
+ Available Placeholders
+
+
+ Use these placeholders in store default pages. They will be automatically replaced with the store's actual information when displayed.
+
+
+ {% for name, description in placeholders %}
+
+ {% raw %}{{{% endraw %}{{ name }}{% raw %}}}{% endraw %}
+
+ {% endfor %}
+
+
+ Click a placeholder to copy it to your clipboard.
+
+
diff --git a/app/modules/cms/templates/cms/platform/homepage-default.html b/app/modules/cms/templates/cms/platform/homepage-default.html
index feca7f36..df5dc22d 100644
--- a/app/modules/cms/templates/cms/platform/homepage-default.html
+++ b/app/modules/cms/templates/cms/platform/homepage-default.html
@@ -4,6 +4,7 @@
{# Import section partials #}
{% from 'cms/platform/sections/_hero.html' import render_hero %}
+{% from 'cms/platform/sections/_products.html' import render_products %}
{% from 'cms/platform/sections/_features.html' import render_features %}
{% from 'cms/platform/sections/_pricing.html' import render_pricing %}
{% from 'cms/platform/sections/_cta.html' import render_cta %}
@@ -35,6 +36,11 @@
{{ render_hero(page.sections.hero, lang, default_lang) }}
{% endif %}
+ {# Products Section #}
+ {% if page.sections.products %}
+ {{ render_products(page.sections.products, lang, default_lang) }}
+ {% endif %}
+
{# Features Section #}
{% if page.sections.features %}
{{ render_features(page.sections.features, lang, default_lang) }}
diff --git a/app/modules/cms/templates/cms/platform/homepage-orion.html b/app/modules/cms/templates/cms/platform/homepage-orion.html
deleted file mode 100644
index bb815265..00000000
--- a/app/modules/cms/templates/cms/platform/homepage-orion.html
+++ /dev/null
@@ -1,427 +0,0 @@
-{# app/templates/platform/homepage-orion.html #}
-{# Orion Marketing Homepage - Letzshop OMS Platform #}
-{% extends "platform/base.html" %}
-{% from 'shared/macros/inputs.html' import toggle_switch %}
-
-{% block title %}Orion - Order Management for Letzshop Sellers{% endblock %}
-{% block meta_description %}Lightweight OMS for Letzshop stores. Manage orders, inventory, and invoicing. Start your 30-day free trial today.{% endblock %}
-
-{% block content %}
-
-
- {# =========================================================================
- HERO SECTION
- ========================================================================= #}
-
-
-
- {# Badge #}
-
-
-
-
- {{ _("cms.platform.hero.badge", trial_days=trial_days) }}
-
-
- {# Headline #}
-
-
- {# Subheadline #}
-
- {{ _("cms.platform.hero.subtitle") }}
-
-
- {# CTA Buttons #}
-
-
-
-
- {# Background Decoration #}
-
-
-
- {# =========================================================================
- PRICING SECTION
- ========================================================================= #}
-
-
- {# Section Header #}
-
-
- {{ _("cms.platform.pricing.title") }}
-
-
- {{ _("cms.platform.pricing.subtitle", trial_days=trial_days) }}
-
-
- {# Billing Toggle #}
-
- {{ toggle_switch(
- model='annual',
- left_label=_("cms.platform.pricing.monthly"),
- right_label=_("cms.platform.pricing.annual"),
- right_badge=_("cms.platform.pricing.save_months")
- ) }}
-
-
-
- {# Pricing Cards Grid #}
-
- {% for tier in tiers %}
-
-
- {# Popular Badge #}
- {% if tier.is_popular %}
-
-
- {{ _("cms.platform.pricing.most_popular") }}
-
-
- {% endif %}
-
- {# Tier Name #}
-
{{ tier.name }}
-
- {# Price #}
-
-
-
-
- {{ _("cms.platform.pricing.per_month") }}
-
-
-
-
- {% if tier.price_annual %}
-
-
{{ _("cms.platform.pricing.per_month") }}
-
- {{ tier.price_annual|int }}€ {{ _("cms.platform.pricing.per_year") }}
-
- {% else %}
-
{{ _("cms.platform.pricing.custom") }}
- {% endif %}
-
-
-
-
- {# Features List - Show all features, grey out unavailable #}
-
- {# Orders #}
-
-
-
-
- {% if tier.orders_per_month %}{{ _("cms.platform.pricing.orders_per_month", count=tier.orders_per_month) }}{% else %}{{ _("cms.platform.pricing.unlimited_orders") }}{% endif %}
-
- {# Products #}
-
-
-
-
- {% if tier.products_limit %}{{ _("cms.platform.pricing.products_limit", count=tier.products_limit) }}{% else %}{{ _("cms.platform.pricing.unlimited_products") }}{% endif %}
-
- {# Team Members #}
-
-
-
-
- {% if tier.team_members %}{{ _("cms.platform.pricing.team_members", count=tier.team_members) }}{% else %}{{ _("cms.platform.pricing.unlimited_team") }}{% endif %}
-
- {# Letzshop Sync - always included #}
-
-
-
-
- {{ _("cms.platform.pricing.letzshop_sync") }}
-
- {# EU VAT Invoicing #}
-
- {% if 'invoice_eu_vat' in tier.features %}
-
-
-
- {% else %}
-
-
-
- {% endif %}
- {{ _("cms.platform.pricing.eu_vat_invoicing") }}
-
- {# Analytics Dashboard #}
-
- {% if 'analytics_dashboard' in tier.features %}
-
-
-
- {% else %}
-
-
-
- {% endif %}
- {{ _("cms.platform.pricing.analytics_dashboard") }}
-
- {# API Access #}
-
- {% if 'api_access' in tier.features %}
-
-
-
- {% else %}
-
-
-
- {% endif %}
- {{ _("cms.platform.pricing.api_access") }}
-
- {# Multi-channel Integration - Enterprise only #}
-
- {% if tier.is_enterprise %}
-
-
-
- {% else %}
-
-
-
- {% endif %}
- {{ _("cms.platform.pricing.multi_channel") }}
-
-
-
- {# CTA Button #}
- {% if tier.is_enterprise %}
-
- {{ _("cms.platform.pricing.contact_sales") }}
-
- {% else %}
-
- {{ _("cms.platform.pricing.start_trial") }}
-
- {% endif %}
-
- {% endfor %}
-
-
-
-
- {# =========================================================================
- ADD-ONS SECTION
- ========================================================================= #}
-
-
- {# Section Header #}
-
-
- {{ _("cms.platform.addons.title") }}
-
-
- {{ _("cms.platform.addons.subtitle") }}
-
-
-
- {# Add-ons Grid #}
-
- {% for addon in addons %}
-
- {# Icon #}
-
- {% if addon.icon == 'globe' %}
-
-
-
- {% elif addon.icon == 'shield-check' %}
-
-
-
- {% elif addon.icon == 'mail' %}
-
-
-
- {% endif %}
-
-
- {# Name & Description #}
-
{{ addon.name }}
-
{{ addon.description }}
-
- {# Price #}
-
- {{ addon.price }}€
- /{{ addon.billing_period }}
-
-
- {# Options for email packages #}
- {% if addon.options %}
-
- {% for opt in addon.options %}
-
- {{ opt.quantity }} addresses: {{ opt.price }}€/month
-
- {% endfor %}
-
- {% endif %}
-
- {% endfor %}
-
-
-
-
- {# =========================================================================
- LETZSHOP STORE FINDER
- ========================================================================= #}
-
-
- {# Section Header #}
-
-
- {{ _("cms.platform.find_shop.title") }}
-
-
- {{ _("cms.platform.find_shop.subtitle") }}
-
-
-
- {# Search Form #}
-
-
-
-
-
-
-
-
-
-
- {{ _("cms.platform.find_shop.button") }}
-
-
-
- {# Result #}
-
-
-
-
- {# Help Text #}
-
- {{ _("cms.platform.find_shop.no_account") }} {{ _("cms.platform.find_shop.signup_letzshop") }} {{ _("cms.platform.find_shop.then_connect") }}
-
-
-
-
-
- {# =========================================================================
- FINAL CTA SECTION
- ========================================================================= #}
-
-
-{% endblock %}
-
-{% block extra_scripts %}
-
-{% endblock %}
diff --git a/app/modules/cms/templates/cms/platform/sections/_products.html b/app/modules/cms/templates/cms/platform/sections/_products.html
new file mode 100644
index 00000000..266e8f04
--- /dev/null
+++ b/app/modules/cms/templates/cms/platform/sections/_products.html
@@ -0,0 +1,91 @@
+{# app/templates/platform/sections/_products.html #}
+{# Products/offerings section for multi-product platforms (e.g. wizard.lu) #}
+{#
+ Parameters:
+ - products: ProductsSection object (or dict)
+ - lang: Current language code
+ - default_lang: Fallback language
+#}
+
+{% macro render_products(products, lang, default_lang) %}
+{% if products and products.enabled %}
+
+
+ {# Section header #}
+
+ {% set title = products.title.translations.get(lang) or products.title.translations.get(default_lang) or '' %}
+ {% if title %}
+
+ {{ title }}
+
+ {% endif %}
+
+ {% if products.subtitle and products.subtitle.translations %}
+ {% set subtitle = products.subtitle.translations.get(lang) or products.subtitle.translations.get(default_lang) %}
+ {% if subtitle %}
+
+ {{ subtitle }}
+
+ {% endif %}
+ {% endif %}
+
+
+ {# Product cards #}
+ {% if products.products %}
+
+ {% for product in products.products %}
+
+ {# Badge #}
+ {% if product.badge and product.badge.translations %}
+ {% set badge_text = product.badge.translations.get(lang) or product.badge.translations.get(default_lang) %}
+ {% if badge_text %}
+
+
+ {{ badge_text }}
+
+
+ {% endif %}
+ {% endif %}
+
+ {# Icon #}
+ {% if product.icon %}
+
+
+
+ {% endif %}
+
+ {# Title #}
+ {% set product_title = product.title.translations.get(lang) or product.title.translations.get(default_lang) or '' %}
+ {% if product_title %}
+
+ {{ product_title }}
+
+ {% endif %}
+
+ {# Description #}
+ {% set product_desc = product.description.translations.get(lang) or product.description.translations.get(default_lang) or '' %}
+ {% if product_desc %}
+
+ {{ product_desc }}
+
+ {% endif %}
+
+ {# CTA Link #}
+ {% if product.url %}
+
+ {% set link_text = product_title or 'Learn More' %}
+ Learn More
+
+
+
+
+ {% endif %}
+
+ {% endfor %}
+
+ {% endif %}
+
+
+{% endif %}
+{% endmacro %}
diff --git a/app/modules/cms/templates/cms/storefront/content-page.html b/app/modules/cms/templates/cms/storefront/content-page.html
index 1df5795e..45a99664 100644
--- a/app/modules/cms/templates/cms/storefront/content-page.html
+++ b/app/modules/cms/templates/cms/storefront/content-page.html
@@ -42,17 +42,16 @@
{% endif %}
- {# Content #}
+ {# Content — use page_content (with resolved placeholders) when available #}
+ {% set content = page_content if page_content is defined and page_content else page.content %}
{% if page.content_format == 'markdown' %}
- {# Markdown content - future enhancement: render with markdown library #}
- {{ page.content | safe }}{# sanitized: CMS content #}
+ {{ content | safe }}{# sanitized: CMS content #}
{% else %}
- {# HTML content (default) #}
- {{ page.content | safe }}{# sanitized: CMS content #}
+ {{ content | safe }}{# sanitized: CMS content #}
{% endif %}
diff --git a/scripts/seed/create_default_content_pages.py b/scripts/seed/create_default_content_pages.py
index b2353b05..a5ae86f5 100755
--- a/scripts/seed/create_default_content_pages.py
+++ b/scripts/seed/create_default_content_pages.py
@@ -1,20 +1,20 @@
#!/usr/bin/env python3
"""
-Create Default Platform Content Pages (CMS)
+Create Default Content Pages for All Platforms (CMS)
-This script creates platform-level default content pages for ALL platforms.
-Pages include:
-- Platform Homepage (platform marketing page)
-- About Us
-- Contact
-- FAQ
-- Shipping Policy
-- Return Policy
-- Privacy Policy
-- Terms of Service
+This script creates:
+1. Platform Homepages (slug="home", is_platform_page=True, sections JSON)
+ - wizard.lu: multi-product landing (hero + products + cta)
+ - omsflow.lu: OMS-focused (hero + features + pricing + cta)
+ - rewardflow.lu: loyalty-focused (hero + features + pricing + cta)
-Pages are created per-platform (unique constraint: platform_id + store_id + slug).
-Stores can override any of these pages with their own custom content.
+2. Platform Marketing Pages (is_platform_page=True)
+ - about, contact, faq, privacy, terms per platform
+
+3. Store Default Pages (is_platform_page=False, store_id=NULL)
+ - Generic templates with {{store_name}} placeholders
+ - OMS: about, contact, faq, shipping, returns, privacy, terms
+ - Loyalty: about, contact, faq, privacy, terms
Prerequisites:
- Database migrations must be applied
@@ -23,9 +23,6 @@ Prerequisites:
Usage:
python scripts/seed/create_default_content_pages.py
-
- # Or with make:
- make create-cms-defaults
"""
import sys
@@ -61,66 +58,761 @@ from app.core.database import SessionLocal
from app.modules.cms.models import ContentPage
from app.modules.tenancy.models import Platform
+
# ============================================================================
-# PLATFORM HOMEPAGE (is_platform_page=True)
+# HELPER: TranslatableText dict builder
# ============================================================================
-PLATFORM_HOMEPAGE = {
- "slug": "platform_homepage",
- "title": "Welcome to Our Multi-Store Marketplace",
- "content": """
-
- Connect stores with customers worldwide. Build your online store and reach millions of shoppers.
-
-
- Our platform empowers entrepreneurs to launch their own branded e-commerce stores
- with minimal effort and maximum impact.
-
- """,
- "meta_description": "Leading multi-store marketplace platform. Connect with thousands of stores and discover millions of products.",
- "meta_keywords": "marketplace, multi-store, e-commerce, online shopping, platform",
- "show_in_footer": False,
- "show_in_header": False,
- "is_platform_page": True,
- "template": "modern",
- "display_order": 0,
+
+def t(fr: str, en: str, de: str) -> dict:
+ """Build a TranslatableText-compatible dict."""
+ return {"translations": {"fr": fr, "en": en, "de": de}}
+
+
+# ============================================================================
+# PLATFORM HOMEPAGE SECTIONS (slug="home", is_platform_page=True)
+# ============================================================================
+
+
+def _wizard_homepage_sections() -> dict:
+ """Wizard.lu (main) — multi-product landing page."""
+ return {
+ "hero": {
+ "enabled": True,
+ "title": t(
+ "Votre boîte à outils numérique pour votre entreprise",
+ "Your Business Digital Toolkit",
+ "Ihr digitales Business-Toolkit",
+ ),
+ "subtitle": t(
+ "Gestion des commandes, programmes de fidélité et création de sites web — tout ce dont votre entreprise luxembourgeoise a besoin.",
+ "Order management, loyalty programs, and website building — everything your Luxembourg business needs.",
+ "Bestellverwaltung, Treueprogramme und Website-Erstellung — alles, was Ihr luxemburgisches Unternehmen braucht.",
+ ),
+ "background_type": "gradient",
+ "buttons": [
+ {
+ "text": t(
+ "Découvrir nos solutions",
+ "Discover Our Solutions",
+ "Unsere Lösungen entdecken",
+ ),
+ "url": "#products",
+ "style": "primary",
+ },
+ ],
+ },
+ "products": {
+ "enabled": True,
+ "title": t(
+ "Nos solutions",
+ "Our Solutions",
+ "Unsere Lösungen",
+ ),
+ "subtitle": t(
+ "Des outils conçus pour le commerce luxembourgeois",
+ "Tools built for Luxembourg commerce",
+ "Werkzeuge für den luxemburgischen Handel",
+ ),
+ "products": [
+ {
+ "icon": "clipboard-list",
+ "title": t(
+ "Gestion des Commandes (OMS)",
+ "Order Management (OMS)",
+ "Bestellverwaltung (OMS)",
+ ),
+ "description": t(
+ "Synchronisez vos commandes Letzshop, gérez les stocks et générez des factures conformes à la TVA.",
+ "Sync your Letzshop orders, manage inventory, and generate VAT-compliant invoices.",
+ "Synchronisieren Sie Ihre Letzshop-Bestellungen, verwalten Sie Lagerbestände und erstellen Sie MwSt-konforme Rechnungen.",
+ ),
+ "url": "/platforms/oms/",
+ },
+ {
+ "icon": "heart",
+ "title": t(
+ "Programme de Fidélité",
+ "Loyalty Program",
+ "Treueprogramm",
+ ),
+ "description": t(
+ "Créez des programmes de fidélité avec points, récompenses et niveaux pour fidéliser vos clients.",
+ "Create loyalty programs with points, rewards, and tiers to retain your customers.",
+ "Erstellen Sie Treueprogramme mit Punkten, Prämien und Stufen, um Ihre Kunden zu binden.",
+ ),
+ "url": "/platforms/loyalty/",
+ },
+ {
+ "icon": "globe-alt",
+ "title": t(
+ "Création de Sites Web",
+ "Website Builder",
+ "Website-Erstellung",
+ ),
+ "description": t(
+ "Créez votre présence en ligne avec notre outil de création de sites web intuitif.",
+ "Build your online presence with our intuitive website builder.",
+ "Erstellen Sie Ihre Online-Präsenz mit unserem intuitiven Website-Baukasten.",
+ ),
+ "url": "",
+ "badge": t("Bientôt", "Coming Soon", "Demnächst"),
+ },
+ ],
+ },
+ "cta": {
+ "enabled": True,
+ "title": t(
+ "Prêt à digitaliser votre entreprise ?",
+ "Ready to Digitalize Your Business?",
+ "Bereit, Ihr Unternehmen zu digitalisieren?",
+ ),
+ "subtitle": t(
+ "Rejoignez les entreprises luxembourgeoises qui font confiance à nos solutions.",
+ "Join Luxembourg businesses that trust our solutions.",
+ "Schließen Sie sich luxemburgischen Unternehmen an, die unseren Lösungen vertrauen.",
+ ),
+ "background_type": "gradient",
+ "buttons": [
+ {
+ "text": t(
+ "Commencer maintenant",
+ "Get Started Now",
+ "Jetzt starten",
+ ),
+ "url": "/signup",
+ "style": "primary",
+ },
+ {
+ "text": t(
+ "Nous contacter",
+ "Contact Us",
+ "Kontaktieren Sie uns",
+ ),
+ "url": "/contact",
+ "style": "secondary",
+ },
+ ],
+ },
+ }
+
+
+def _oms_homepage_sections() -> dict:
+ """OMS (omsflow.lu) — order management focused homepage."""
+ return {
+ "hero": {
+ "enabled": True,
+ "badge_text": t(
+ "Essai gratuit — Aucune carte de crédit requise",
+ "Free Trial — No Credit Card Required",
+ "Kostenlose Testversion — Keine Kreditkarte erforderlich",
+ ),
+ "title": t(
+ "OMS léger pour les vendeurs Letzshop",
+ "Lightweight OMS for Letzshop Sellers",
+ "Leichtes OMS für Letzshop-Verkäufer",
+ ),
+ "subtitle": t(
+ "Gestion des commandes, stocks et facturation conçue pour le e-commerce luxembourgeois. Arrêtez de jongler avec les tableurs. Gérez votre entreprise.",
+ "Order management, inventory, and invoicing built for Luxembourg e-commerce. Stop juggling spreadsheets. Start running your business.",
+ "Bestellverwaltung, Lager und Rechnungsstellung für den luxemburgischen E-Commerce. Schluss mit Tabellenkalkulationen. Führen Sie Ihr Geschäft.",
+ ),
+ "background_type": "gradient",
+ "buttons": [
+ {
+ "text": t(
+ "Essai gratuit",
+ "Start Free Trial",
+ "Kostenlos testen",
+ ),
+ "url": "/signup",
+ "style": "primary",
+ },
+ {
+ "text": t(
+ "Trouvez votre boutique Letzshop",
+ "Find Your Letzshop Shop",
+ "Finden Sie Ihren Letzshop",
+ ),
+ "url": "#find-shop",
+ "style": "secondary",
+ },
+ ],
+ },
+ "features": {
+ "enabled": True,
+ "title": t(
+ "Tout ce dont un vendeur Letzshop a besoin",
+ "Everything a Letzshop Seller Needs",
+ "Alles, was ein Letzshop-Verkäufer braucht",
+ ),
+ "subtitle": t(
+ "Les outils opérationnels que Letzshop ne fournit pas",
+ "The operational tools Letzshop doesn't provide",
+ "Die operativen Tools, die Letzshop nicht bietet",
+ ),
+ "layout": "grid",
+ "features": [
+ {
+ "icon": "refresh",
+ "title": t(
+ "Synchronisation Letzshop",
+ "Letzshop Order Sync",
+ "Letzshop-Synchronisierung",
+ ),
+ "description": t(
+ "Les commandes se synchronisent automatiquement. Confirmez et ajoutez le suivi depuis Orion.",
+ "Orders sync automatically. Confirm and add tracking directly from Orion.",
+ "Bestellungen werden automatisch synchronisiert. Bestätigen und Tracking direkt von Orion hinzufügen.",
+ ),
+ },
+ {
+ "icon": "cube",
+ "title": t(
+ "Gestion des stocks",
+ "Inventory Management",
+ "Lagerverwaltung",
+ ),
+ "description": t(
+ "Suivez vos stocks en temps réel avec emplacements d'entrepôt et bons de commande.",
+ "Track inventory in real-time with warehouse locations and purchase orders.",
+ "Verfolgen Sie Lagerbestände in Echtzeit mit Lagerstandorten und Bestellungen.",
+ ),
+ },
+ {
+ "icon": "document-text",
+ "title": t(
+ "Facturation TVA UE",
+ "EU VAT Invoicing",
+ "EU-MwSt-Rechnungen",
+ ),
+ "description": t(
+ "Générez des factures PDF conformes avec la TVA correcte pour tout pays UE.",
+ "Generate compliant PDF invoices with correct VAT for any EU country.",
+ "Erstellen Sie konforme PDF-Rechnungen mit korrekter MwSt für jedes EU-Land.",
+ ),
+ },
+ {
+ "icon": "users",
+ "title": t(
+ "Données clients",
+ "Customer Data",
+ "Kundendaten",
+ ),
+ "description": t(
+ "Possédez vos données clients. Exportez pour le marketing et la fidélisation.",
+ "Own your customer data. Export for marketing and loyalty building.",
+ "Besitzen Sie Ihre Kundendaten. Exportieren Sie für Marketing und Kundenbindung.",
+ ),
+ },
+ ],
+ },
+ "pricing": {
+ "enabled": True,
+ "title": t(
+ "Tarification simple et transparente",
+ "Simple, Transparent Pricing",
+ "Einfache, transparente Preise",
+ ),
+ "subtitle": t(
+ "Choisissez le plan adapté à votre entreprise. Tous les plans incluent un essai gratuit.",
+ "Choose the plan that fits your business. All plans include a free trial.",
+ "Wählen Sie den Plan, der zu Ihrem Unternehmen passt. Alle Pläne beinhalten eine kostenlose Testversion.",
+ ),
+ "use_subscription_tiers": True,
+ },
+ "cta": {
+ "enabled": True,
+ "title": t(
+ "Prêt à optimiser vos commandes ?",
+ "Ready to Streamline Your Orders?",
+ "Bereit, Ihre Bestellungen zu optimieren?",
+ ),
+ "subtitle": t(
+ "Rejoignez les vendeurs Letzshop qui font confiance à Orion pour leur gestion de commandes.",
+ "Join Letzshop stores who trust Orion for their order management.",
+ "Schließen Sie sich Letzshop-Händlern an, die Orion für ihre Bestellverwaltung vertrauen.",
+ ),
+ "background_type": "gradient",
+ "buttons": [
+ {
+ "text": t(
+ "Essai gratuit",
+ "Start Free Trial",
+ "Kostenlos testen",
+ ),
+ "url": "/signup",
+ "style": "primary",
+ },
+ ],
+ },
+ }
+
+
+def _loyalty_homepage_sections() -> dict:
+ """Loyalty (rewardflow.lu) — loyalty program focused homepage."""
+ return {
+ "hero": {
+ "enabled": True,
+ "badge_text": t(
+ "Essai gratuit — Aucune carte de crédit requise",
+ "Free Trial — No Credit Card Required",
+ "Kostenlose Testversion — Keine Kreditkarte erforderlich",
+ ),
+ "title": t(
+ "Fidélisation client simplifiée",
+ "Customer Loyalty Made Simple",
+ "Kundentreue leicht gemacht",
+ ),
+ "subtitle": t(
+ "Créez des programmes de fidélité engageants avec points, récompenses et niveaux. Conçu pour les commerces luxembourgeois.",
+ "Create engaging loyalty programs with points, rewards, and tiers. Built for Luxembourg businesses.",
+ "Erstellen Sie ansprechende Treueprogramme mit Punkten, Prämien und Stufen. Für luxemburgische Unternehmen.",
+ ),
+ "background_type": "gradient",
+ "buttons": [
+ {
+ "text": t(
+ "Essai gratuit",
+ "Start Free Trial",
+ "Kostenlos testen",
+ ),
+ "url": "/signup",
+ "style": "primary",
+ },
+ {
+ "text": t(
+ "Voir les fonctionnalités",
+ "See Features",
+ "Funktionen ansehen",
+ ),
+ "url": "#features",
+ "style": "secondary",
+ },
+ ],
+ },
+ "features": {
+ "enabled": True,
+ "title": t(
+ "Tout pour fidéliser vos clients",
+ "Everything to Build Customer Loyalty",
+ "Alles für die Kundenbindung",
+ ),
+ "subtitle": t(
+ "Des outils puissants pour créer et gérer vos programmes de fidélité",
+ "Powerful tools to create and manage your loyalty programs",
+ "Leistungsstarke Tools zum Erstellen und Verwalten Ihrer Treueprogramme",
+ ),
+ "layout": "grid",
+ "features": [
+ {
+ "icon": "star",
+ "title": t(
+ "Système de points",
+ "Points System",
+ "Punktesystem",
+ ),
+ "description": t(
+ "Attribuez des points pour chaque achat. Règles flexibles et multiplicateurs personnalisables.",
+ "Award points for every purchase. Flexible rules and customizable multipliers.",
+ "Vergeben Sie Punkte für jeden Einkauf. Flexible Regeln und anpassbare Multiplikatoren.",
+ ),
+ },
+ {
+ "icon": "gift",
+ "title": t(
+ "Catalogue de récompenses",
+ "Rewards Catalog",
+ "Prämienkatalog",
+ ),
+ "description": t(
+ "Créez un catalogue de récompenses attrayant. Produits, remises ou expériences exclusives.",
+ "Create an attractive rewards catalog. Products, discounts, or exclusive experiences.",
+ "Erstellen Sie einen attraktiven Prämienkatalog. Produkte, Rabatte oder exklusive Erlebnisse.",
+ ),
+ },
+ {
+ "icon": "trending-up",
+ "title": t(
+ "Gestion des niveaux",
+ "Tier Management",
+ "Stufenverwaltung",
+ ),
+ "description": t(
+ "Bronze, Argent, Or — motivez vos clients avec des niveaux de fidélité progressifs.",
+ "Bronze, Silver, Gold — motivate customers with progressive loyalty tiers.",
+ "Bronze, Silber, Gold — motivieren Sie Kunden mit progressiven Treuestufen.",
+ ),
+ },
+ {
+ "icon": "chart-bar",
+ "title": t(
+ "Analytique avancée",
+ "Advanced Analytics",
+ "Erweiterte Analytik",
+ ),
+ "description": t(
+ "Suivez l'engagement, les taux de rétention et le ROI de vos programmes.",
+ "Track engagement, retention rates, and program ROI.",
+ "Verfolgen Sie Engagement, Bindungsraten und Programm-ROI.",
+ ),
+ },
+ ],
+ },
+ "pricing": {
+ "enabled": True,
+ "title": t(
+ "Tarification simple et transparente",
+ "Simple, Transparent Pricing",
+ "Einfache, transparente Preise",
+ ),
+ "subtitle": t(
+ "Choisissez le plan adapté à votre programme de fidélité.",
+ "Choose the plan that fits your loyalty program.",
+ "Wählen Sie den Plan, der zu Ihrem Treueprogramm passt.",
+ ),
+ "use_subscription_tiers": True,
+ },
+ "cta": {
+ "enabled": True,
+ "title": t(
+ "Prêt à fidéliser vos clients ?",
+ "Ready to Build Customer Loyalty?",
+ "Bereit, Kundentreue aufzubauen?",
+ ),
+ "subtitle": t(
+ "Commencez votre programme de fidélité dès aujourd'hui. Essai gratuit, sans engagement.",
+ "Start your loyalty program today. Free trial, no commitment.",
+ "Starten Sie Ihr Treueprogramm noch heute. Kostenlose Testversion, ohne Verpflichtung.",
+ ),
+ "background_type": "gradient",
+ "buttons": [
+ {
+ "text": t(
+ "Essai gratuit",
+ "Start Free Trial",
+ "Kostenlos testen",
+ ),
+ "url": "/signup",
+ "style": "primary",
+ },
+ ],
+ },
+ }
+
+
+HOMEPAGE_SECTIONS = {
+ "main": _wizard_homepage_sections,
+ "oms": _oms_homepage_sections,
+ "loyalty": _loyalty_homepage_sections,
}
+
# ============================================================================
-# DEFAULT STORE CONTENT PAGES (is_platform_page=False)
+# PLATFORM MARKETING PAGES (is_platform_page=True)
# ============================================================================
-DEFAULT_PAGES = [
+
+def _get_platform_pages(platform_code: str) -> list[dict]:
+ """Get platform marketing pages for a given platform code."""
+
+ if platform_code == "main":
+ return [
+ {
+ "slug": "about",
+ "title": "About Wizard",
+ "content": """
+
About Wizard
+
Wizard is the digital toolkit for Luxembourg businesses. We provide integrated solutions for order management, customer loyalty, and online presence.
+
Our Mission
+
To empower Luxembourg businesses with modern, easy-to-use digital tools that help them grow and thrive in the digital age.
+
Our Solutions
+
+ OMS (omsflow.lu): Order management, inventory, and invoicing for Letzshop sellers
+ Loyalty (rewardflow.lu): Customer loyalty programs with points, rewards, and tiers
+ Website Builder: Coming soon — build your online presence
+
+
Built in Luxembourg
+
We understand the unique needs of Luxembourg commerce — from multilingual support (FR/DE/EN) to EU VAT compliance.
+
""",
+ "meta_description": "Wizard — the digital toolkit for Luxembourg businesses. Order management, loyalty programs, and more.",
+ "show_in_footer": True,
+ "show_in_header": True,
+ "display_order": 1,
+ },
+ {
+ "slug": "contact",
+ "title": "Contact Us",
+ "content": """
+
Contact Wizard
+
We'd love to hear from you. Get in touch with our team.
+
General Inquiries
+
+ Email: info@wizard.lu
+
+
Sales
+
Interested in our solutions for your business?
+
+ Email: sales@wizard.lu
+
+
Support
+
Already a customer? Our support team is here to help.
+
+ Email: support@wizard.lu
+
+
Office
+
Luxembourg
+
""",
+ "meta_description": "Contact the Wizard team for inquiries about our business solutions.",
+ "show_in_footer": True,
+ "show_in_header": True,
+ "display_order": 2,
+ },
+ {
+ "slug": "faq",
+ "title": "FAQ",
+ "content": """
+
Frequently Asked Questions
+
General
+
What is Wizard?
+
Wizard is a suite of digital tools for Luxembourg businesses, including order management (OMS), customer loyalty programs, and website building.
+
Who is Wizard for?
+
Wizard is designed for Luxembourg businesses of all sizes — from individual Letzshop sellers to multi-store retailers.
+
Pricing & Billing
+
Is there a free trial?
+
Yes! All our solutions offer a free trial period. No credit card required to start.
+
Can I switch plans?
+
Yes, you can upgrade or downgrade your plan at any time.
+
Technical
+
What languages are supported?
+
Our platform supports French, German, and English — the three official languages of Luxembourg.
+
Is my data secure?
+
Yes. We use industry-standard encryption and follow GDPR regulations for data protection.
+
""",
+ "meta_description": "Frequently asked questions about Wizard solutions for Luxembourg businesses.",
+ "show_in_footer": True,
+ "show_in_header": False,
+ "display_order": 3,
+ },
+ ]
+
+ if platform_code == "oms":
+ return [
+ {
+ "slug": "about",
+ "title": "About OMS",
+ "content": """
+
About OMS by Wizard
+
OMS (omsflow.lu) is a lightweight order management system built specifically for Letzshop sellers in Luxembourg.
+
Why OMS?
+
Letzshop is a great marketplace, but it doesn't give sellers the back-office tools they need. OMS fills that gap with:
+
+ Automatic order sync from Letzshop
+ Inventory management with warehouse locations
+ EU VAT invoicing with correct rates per country
+ Customer data ownership for marketing and retention
+
+
Built for Luxembourg
+
From multilingual support to Luxembourg VAT compliance, OMS is designed for the local market.
+
""",
+ "meta_description": "OMS — lightweight order management for Letzshop sellers. Manage orders, inventory, and invoicing.",
+ "show_in_footer": True,
+ "show_in_header": True,
+ "display_order": 1,
+ },
+ {
+ "slug": "contact",
+ "title": "Contact OMS Support",
+ "content": """
+
Contact OMS Support
+
Need help with your order management? We're here for you.
+
Support
+
+ Email: support@omsflow.lu
+
+
Sales
+
Interested in OMS for your Letzshop store?
+
+ Email: sales@omsflow.lu
+
+
""",
+ "meta_description": "Contact the OMS support team for help with order management.",
+ "show_in_footer": True,
+ "show_in_header": True,
+ "display_order": 2,
+ },
+ {
+ "slug": "faq",
+ "title": "OMS FAQ",
+ "content": """
+
Frequently Asked Questions
+
Do I need a Letzshop account?
+
While OMS works best with Letzshop integration, you can also use it as a standalone order management tool.
+
How does Letzshop sync work?
+
Enter your Letzshop API credentials and orders sync automatically. Setup takes about 2 minutes.
+
Is EU VAT invoicing included?
+
Yes, EU VAT invoicing is available on Professional and Enterprise plans. Luxembourg VAT is available on all plans.
+
Can I export my data?
+
Yes, you can export customers, orders, and invoices at any time.
+
Is there a free trial?
+
Yes! Start with a free trial, no credit card required.
+
""",
+ "meta_description": "Frequently asked questions about OMS order management for Letzshop sellers.",
+ "show_in_footer": True,
+ "show_in_header": False,
+ "display_order": 3,
+ },
+ ]
+
+ if platform_code == "loyalty":
+ return [
+ {
+ "slug": "about",
+ "title": "About Loyalty",
+ "content": """
+
About Loyalty by Wizard
+
Loyalty (rewardflow.lu) helps Luxembourg businesses create engaging customer loyalty programs.
+
What We Offer
+
+ Points system: Award points for purchases with flexible rules
+ Rewards catalog: Products, discounts, or exclusive experiences
+ Tier management: Bronze, Silver, Gold — progressive loyalty levels
+ Analytics: Track engagement, retention, and program ROI
+
+
Built for Luxembourg
+
Multilingual support (FR/DE/EN) and local business features out of the box.
+
""",
+ "meta_description": "Loyalty — customer loyalty programs for Luxembourg businesses. Points, rewards, and tiers.",
+ "show_in_footer": True,
+ "show_in_header": True,
+ "display_order": 1,
+ },
+ {
+ "slug": "contact",
+ "title": "Contact Loyalty Support",
+ "content": """
+
Contact Loyalty Support
+
Need help with your loyalty program? We're here for you.
+
Support
+
+ Email: support@rewardflow.lu
+
+
Sales
+
Interested in a loyalty program for your business?
+
+ Email: sales@rewardflow.lu
+
+
""",
+ "meta_description": "Contact the Loyalty support team for help with your loyalty program.",
+ "show_in_footer": True,
+ "show_in_header": True,
+ "display_order": 2,
+ },
+ {
+ "slug": "faq",
+ "title": "Loyalty FAQ",
+ "content": """
+
Frequently Asked Questions
+
How do loyalty points work?
+
You define the rules — for example, 1 point per euro spent. Points can be redeemed for rewards from your catalog.
+
Can I customize the loyalty tiers?
+
Yes! You can create custom tiers with different names, thresholds, and benefits.
+
How do customers check their points?
+
Customers can check their balance through a personalized loyalty page or at your point of sale.
+
Is there a free trial?
+
Yes! Start with a free trial, no credit card required.
+
""",
+ "meta_description": "Frequently asked questions about Loyalty customer loyalty programs.",
+ "show_in_footer": True,
+ "show_in_header": False,
+ "display_order": 3,
+ },
+ ]
+
+ return []
+
+
+# Shared legal pages (same content for all platforms)
+SHARED_PLATFORM_PAGES = [
+ {
+ "slug": "privacy",
+ "title": "Privacy Policy",
+ "content": """
+
Privacy Policy
+
Last Updated: February 2026
+
Information We Collect
+
We collect information you provide directly:
+
+ Name, email address, and contact information
+ Business information (for merchants)
+ Payment information (processed securely by our payment processor)
+
+
How We Use Your Information
+
We use your information to provide and improve our services, process payments, communicate with you, and comply with legal obligations.
+
Data Protection (GDPR)
+
We comply with the EU General Data Protection Regulation. You have the right to access, correct, delete, or export your personal data at any time.
+
Contact
+
For privacy-related questions, contact privacy@wizard.lu
+
""",
+ "meta_description": "Privacy policy — how we collect, use, and protect your personal information.",
+ "show_in_footer": False,
+ "show_in_header": False,
+ "show_in_legal": True,
+ "display_order": 10,
+ },
+ {
+ "slug": "terms",
+ "title": "Terms of Service",
+ "content": """
+
Terms of Service
+
Last Updated: February 2026
+
1. Acceptance of Terms
+
By accessing and using this platform, you accept and agree to be bound by these Terms of Service.
+
2. Services
+
We provide digital business tools including order management, loyalty programs, and website building services.
+
3. Account
+
You must provide accurate information and maintain the security of your account.
+
4. Payments
+
Subscription fees are billed monthly or annually. You may cancel at any time.
+
5. Data & Privacy
+
Your use of our services is also governed by our Privacy Policy.
+
6. Governing Law
+
These terms are governed by the laws of Luxembourg.
+
Contact
+
For questions about these terms, contact legal@wizard.lu
+
""",
+ "meta_description": "Terms of service governing the use of our platform.",
+ "show_in_footer": False,
+ "show_in_header": False,
+ "show_in_legal": True,
+ "display_order": 11,
+ },
+]
+
+
+# ============================================================================
+# STORE DEFAULT PAGES (is_platform_page=False, store_id=NULL)
+# Uses {{store_name}}, {{store_email}}, {{store_phone}} placeholders
+# ============================================================================
+
+
+STORE_DEFAULTS_COMMON = [
{
"slug": "about",
"title": "About Us",
- "content": """
-
-
Welcome to Our Platform
-
We are a multi-store e-commerce platform connecting quality sellers with customers worldwide.
-
-
Our Mission
-
To empower independent businesses and artisans by providing them with the tools and platform they need to reach customers globally.
-
-
What We Offer
+ "content": """
+
About {{store_name}}
+
Welcome to {{store_name}}. We are committed to providing you with quality products and excellent service.
+
Our Story
+
{{store_name}} was founded with a simple mission: to deliver exceptional value to our customers.
+
Contact Us
+
Have questions? We'd love to hear from you.
- Curated selection of quality products from verified stores
- Secure payment processing and buyer protection
- Fast and reliable shipping options
- Dedicated customer support
+ Email: {{store_email}}
+ Phone: {{store_phone}}
-
-
Our Values
-
- Quality: We work only with stores who meet our quality standards
- Transparency: Clear pricing, policies, and communication
- Customer First: Your satisfaction is our priority
- Innovation: Continuously improving our platform and services
-
-
- """,
- "meta_description": "Learn about our mission to connect quality stores with customers worldwide",
- "meta_keywords": "about us, mission, values, platform",
+
""",
+ "meta_description": "Learn about {{store_name}} — our story, values, and commitment to quality.",
"show_in_footer": True,
"show_in_header": True,
"display_order": 1,
@@ -128,140 +820,95 @@ DEFAULT_PAGES = [
{
"slug": "contact",
"title": "Contact Us",
- "content": """
-
-
Get in Touch
-
We're here to help! Reach out to us through any of the following channels:
-
-
Customer Support
-
For questions about orders, products, or general inquiries:
+ "content": """
+
Contact {{store_name}}
+
We're here to help! Reach out to us.
+
Get in Touch
- Email: support@example.com
- Phone: 1-800-EXAMPLE (Mon-Fri, 9am-6pm EST)
- Live Chat: Available on our website during business hours
+ Email: {{store_email}}
+ Phone: {{store_phone}}
-
-
Business Inquiries
-
Interested in becoming a store or partnering with us?
-
- Email: stores@example.com
-
-
-
Office Address
-
- 123 Commerce Street
- Suite 456
- City, State 12345
- United States
-
-
-
Response Time
-
We typically respond to all inquiries within 24 hours during business days.
-
- """,
- "meta_description": "Contact our customer support team for assistance with orders, products, or inquiries",
- "meta_keywords": "contact, support, customer service, help",
+
We typically respond within 24 hours during business days.
+
""",
+ "meta_description": "Contact {{store_name}} for questions, support, or inquiries.",
"show_in_footer": True,
"show_in_header": True,
"display_order": 2,
},
{
"slug": "faq",
- "title": "Frequently Asked Questions",
- "content": """
-
+ "title": "FAQ",
+ "content": """
Frequently Asked Questions
-
-
Orders & Shipping
-
-
How do I track my order?
-
Once your order ships, you'll receive a tracking number via email. You can also view your order status in your account dashboard.
-
-
How long does shipping take?
-
Shipping times vary by store and destination. Most orders arrive within 3-7 business days. See our Shipping Policy for details.
-
-
Do you ship internationally?
-
Yes! We ship to most countries worldwide. International shipping times and costs vary by destination.
-
-
Returns & Refunds
-
-
What is your return policy?
-
Most items can be returned within 30 days of delivery. See our Return Policy for complete details.
-
-
How do I request a refund?
-
Contact the store directly through your order page or reach out to our support team for assistance.
-
-
Payments
-
-
What payment methods do you accept?
-
We accept all major credit cards, PayPal, and other secure payment methods.
-
-
Is my payment information secure?
-
Yes! We use industry-standard encryption and never store your full payment details.
-
-
Account
-
-
Do I need an account to make a purchase?
-
No, you can checkout as a guest. However, creating an account lets you track orders and save your preferences.
-
-
How do I reset my password?
-
Click "Forgot Password" on the login page and follow the instructions sent to your email.
-
- """,
- "meta_description": "Find answers to common questions about orders, shipping, returns, and more",
- "meta_keywords": "faq, questions, help, support",
+
How can I contact you?
+
You can reach us at {{store_email}} or call {{store_phone}}.
+
What are your business hours?
+
We are available during regular business hours, Monday through Friday.
+
Do you have a physical location?
+
Please contact us for information about our location and visiting hours.
+
""",
+ "meta_description": "Frequently asked questions about {{store_name}}.",
"show_in_footer": True,
"show_in_header": False,
"display_order": 3,
},
+ {
+ "slug": "privacy",
+ "title": "Privacy Policy",
+ "content": """
+
Privacy Policy
+
{{store_name}} is committed to protecting your personal information.
+
Information We Collect
+
We collect information necessary to process your orders and provide customer service.
+
Your Rights
+
Under GDPR, you have the right to access, correct, or delete your personal data. Contact us at {{store_email}}.
+
""",
+ "meta_description": "Privacy policy for {{store_name}}.",
+ "show_in_footer": False,
+ "show_in_header": False,
+ "show_in_legal": True,
+ "display_order": 10,
+ },
+ {
+ "slug": "terms",
+ "title": "Terms of Service",
+ "content": """
+
Terms of Service
+
By using the services provided by {{store_name}}, you agree to these terms.
+
Orders
+
All orders are subject to availability and confirmation.
+
Contact
+
For questions about these terms, contact us at {{store_email}}.
+
""",
+ "meta_description": "Terms of service for {{store_name}}.",
+ "show_in_footer": False,
+ "show_in_header": False,
+ "show_in_legal": True,
+ "display_order": 11,
+ },
+]
+
+# OMS-specific store defaults (shipping/returns)
+STORE_DEFAULTS_OMS_EXTRA = [
{
"slug": "shipping",
"title": "Shipping Policy",
- "content": """
-
+ "content": """
Shipping Policy
-
Shipping Methods
-
We offer multiple shipping options to meet your needs:
+
{{store_name}} offers multiple shipping options:
- Standard Shipping: 5-7 business days
- Express Shipping: 2-3 business days
- Overnight Shipping: Next business day
+ Standard Shipping: 3-7 business days
+ Express Shipping: 1-3 business days
-
Shipping Costs
-
Shipping costs are calculated based on:
-
- Package weight and dimensions
- Destination address
- Selected shipping method
-
-
Free standard shipping on orders over $50!
-
-
Processing Time
-
Orders are typically processed within 1-2 business days. You'll receive a confirmation email when your order ships with tracking information.
-
-
International Shipping
-
We ship to most countries worldwide. International shipping times vary by destination (typically 7-21 business days).
-
- Customs fees and import duties are the responsibility of the recipient
- International orders may be subject to customs inspection
-
-
+
Shipping costs are calculated based on weight and destination at checkout.
Tracking
-
All orders include tracking. You can monitor your package status through:
-
- Your account dashboard
- Email notifications
- Carrier tracking website
-
-
-
Delivery Issues
-
If you experience any delivery issues, please contact us within 7 days of the expected delivery date.
-
- """,
- "meta_description": "Learn about our shipping methods, costs, and delivery times",
- "meta_keywords": "shipping, delivery, tracking, international",
+
You will receive a tracking number by email once your order ships.
+
Questions?
+
Contact us at {{store_email}} for shipping inquiries.
+
""",
+ "meta_description": "Shipping policy for {{store_name}} — methods, costs, and delivery times.",
"show_in_footer": True,
"show_in_header": False,
"display_order": 4,
@@ -269,236 +916,26 @@ DEFAULT_PAGES = [
{
"slug": "returns",
"title": "Return & Refund Policy",
- "content": """
-
+ "content": """
Return & Refund Policy
-
-
30-Day Return Window
-
Most items can be returned within 30 days of delivery for a full refund or exchange.
-
-
Return Requirements
-
To be eligible for a return, items must:
-
- Be in original condition with tags attached
- Include original packaging and accessories
- Not be used or damaged
- Include proof of purchase
-
-
-
Non-Returnable Items
-
The following items cannot be returned:
-
- Personalized or custom-made items
- Perishable goods
- Health and personal care items
- Digital downloads
- Sale or clearance items (marked as final sale)
-
-
+
Returns
+
{{store_name}} accepts returns within 14 days of delivery, in accordance with Luxembourg consumer protection law.
How to Return
- Log into your account and go to "Order History"
- Select the order and click "Request Return"
- Choose items and provide reason for return
- Print the prepaid return label
- Pack items securely and attach label
- Drop off at any authorized carrier location
+ Contact us at {{store_email}} to initiate a return
+ Pack the item in its original condition
+ Ship using the provided return instructions
-
-
Refund Process
-
Once we receive your return:
-
- We'll inspect the items (2-3 business days)
- Approved refunds are processed within 5-7 business days
- Refunds go to original payment method
- You'll receive email confirmation
-
-
-
Return Shipping
-
Return shipping is free for defective or incorrect items. For other returns, a $5.99 return shipping fee will be deducted from your refund.
-
-
Exchanges
-
We offer free exchanges for different sizes or colors of the same item. Contact support to arrange an exchange.
-
-
Damaged or Defective Items
-
If you receive a damaged or defective item, please contact us within 7 days of delivery with photos. We'll send a replacement or full refund immediately.
-
- """,
- "meta_description": "Our 30-day return policy ensures your satisfaction with every purchase",
- "meta_keywords": "returns, refunds, exchange, policy",
+
Refunds
+
Refunds are processed within 14 days of receiving the returned item, back to the original payment method.
+
Damaged Items
+
If you receive a damaged item, contact us immediately at {{store_email}} with photos.
+
""",
+ "meta_description": "Return and refund policy for {{store_name}}.",
"show_in_footer": True,
"show_in_header": False,
"display_order": 5,
},
- {
- "slug": "privacy",
- "title": "Privacy Policy",
- "content": """
-
-
Privacy Policy
-
Last Updated: [Date]
-
-
Information We Collect
-
We collect information you provide directly:
-
- Name, email address, and contact information
- Shipping and billing addresses
- Payment information (processed securely by our payment processor)
- Order history and preferences
-
-
-
We automatically collect:
-
- Browser type and device information
- IP address and location data
- Cookies and similar tracking technologies
- Usage data and analytics
-
-
-
How We Use Your Information
-
We use your information to:
-
- Process and fulfill your orders
- Communicate about orders and account
- Provide customer support
- Send marketing communications (with your consent)
- Improve our platform and services
- Prevent fraud and enhance security
-
-
-
Information Sharing
-
We share your information only when necessary:
-
- Stores: To fulfill your orders
- Service Providers: Payment processors, shipping carriers, analytics
- Legal Requirements: When required by law
-
-
We never sell your personal information to third parties.
-
-
Your Rights
-
You have the right to:
-
- Access your personal data
- Correct inaccurate information
- Request deletion of your data
- Opt-out of marketing communications
- Export your data
-
-
-
Data Security
-
We implement industry-standard security measures including:
-
- SSL/TLS encryption for data transmission
- Secure password hashing
- Regular security audits
- Limited employee access to personal data
-
-
-
Cookies
-
We use cookies to enhance your experience. You can control cookies through your browser settings.
-
-
Children's Privacy
-
Our platform is not intended for children under 13. We do not knowingly collect information from children.
-
-
Changes to This Policy
-
We may update this policy periodically. Significant changes will be communicated via email.
-
-
Contact Us
-
For privacy-related questions, contact us at privacy@example.com
-
- """,
- "meta_description": "Learn how we collect, use, and protect your personal information",
- "meta_keywords": "privacy, data protection, security, policy",
- "show_in_footer": False,
- "show_in_header": False,
- "show_in_legal": True,
- "display_order": 6,
- },
- {
- "slug": "terms",
- "title": "Terms of Service",
- "content": """
-
-
Terms of Service
-
Last Updated: [Date]
-
-
1. Acceptance of Terms
-
By accessing and using this platform, you accept and agree to be bound by these Terms of Service.
-
-
2. Account Registration
-
You must:
-
- Be at least 18 years old
- Provide accurate and complete information
- Maintain the security of your account
- Notify us of unauthorized access
-
-
-
3. User Conduct
-
You agree not to:
-
- Violate any laws or regulations
- Infringe on intellectual property rights
- Transmit harmful code or viruses
- Harass or harm other users
- Engage in fraudulent activities
- Scrape or data mine our platform
-
-
-
4. Product Listings
-
Product information is provided by stores. While we strive for accuracy:
-
- Descriptions may contain errors
- Prices are subject to change
- Availability is not guaranteed
- Images are representative
-
-
-
5. Orders and Payment
-
- All orders are subject to acceptance
- We reserve the right to refuse or cancel orders
- Prices include applicable taxes unless stated otherwise
- Payment must be received before order processing
-
-
-
6. Intellectual Property
-
All content on this platform is protected by copyright, trademark, and other laws. You may not:
-
- Reproduce or distribute our content
- Create derivative works
- Use our trademarks without permission
-
-
-
7. Disclaimer of Warranties
-
This platform is provided "as is" without warranties of any kind, express or implied.
-
-
8. Limitation of Liability
-
We are not liable for indirect, incidental, or consequential damages arising from your use of the platform.
-
-
9. Indemnification
-
You agree to indemnify and hold us harmless from claims arising from your use of the platform or violation of these terms.
-
-
10. Dispute Resolution
-
Disputes will be resolved through binding arbitration in accordance with the rules of the American Arbitration Association.
-
-
11. Governing Law
-
These terms are governed by the laws of [State/Country], without regard to conflict of law principles.
-
-
12. Changes to Terms
-
We may modify these terms at any time. Continued use constitutes acceptance of modified terms.
-
-
13. Contact
-
For questions about these terms, contact legal@example.com
-
- """,
- "meta_description": "Read our terms of service governing the use of our platform",
- "meta_keywords": "terms, conditions, legal, agreement",
- "show_in_footer": False,
- "show_in_header": False,
- "show_in_legal": True,
- "display_order": 7,
- },
]
@@ -507,26 +944,32 @@ DEFAULT_PAGES = [
# ============================================================================
-def _page_exists(db: Session, platform_id: int, slug: str) -> bool:
- """Check if a page already exists for the given platform and slug."""
+def _page_exists(db: Session, platform_id: int, slug: str, *, is_platform_page: bool) -> bool:
+ """Check if a page already exists for the given platform, slug, and type."""
existing = db.execute(
select(ContentPage).where(
ContentPage.platform_id == platform_id,
ContentPage.store_id.is_(None),
ContentPage.slug == slug,
+ ContentPage.is_platform_page == is_platform_page,
)
).scalar_one_or_none()
return existing is not None
def _create_page(
- db: Session, platform_id: int, page_data: dict, *, is_platform_page: bool = False
+ db: Session,
+ platform_id: int,
+ page_data: dict,
+ *,
+ is_platform_page: bool = False,
+ sections: dict | None = None,
) -> bool:
"""Create a single content page. Returns True if created, False if skipped."""
slug = page_data["slug"]
title = page_data["title"]
- if _page_exists(db, platform_id, slug):
+ if _page_exists(db, platform_id, slug, is_platform_page=is_platform_page):
print(f" Skipped: {title} (/{slug}) - already exists")
return False
@@ -535,35 +978,37 @@ def _create_page(
store_id=None,
slug=slug,
title=title,
- content=page_data["content"],
+ content=page_data.get("content", ""),
content_format="html",
template=page_data.get("template", "default"),
- meta_description=page_data["meta_description"],
- meta_keywords=page_data["meta_keywords"],
+ sections=sections,
+ meta_description=page_data.get("meta_description", ""),
+ meta_keywords=page_data.get("meta_keywords", ""),
is_platform_page=is_platform_page,
is_published=True,
published_at=datetime.now(UTC),
- show_in_footer=page_data.get("show_in_footer", True),
+ show_in_footer=page_data.get("show_in_footer", False),
show_in_header=page_data.get("show_in_header", False),
show_in_legal=page_data.get("show_in_legal", False),
- display_order=page_data["display_order"],
+ display_order=page_data.get("display_order", 0),
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
db.add(page)
- print(f" Created: {title} (/{slug})")
+ tier_label = "platform" if is_platform_page else "store-default"
+ print(f" Created [{tier_label}]: {title} (/{slug})")
return True
def create_default_pages(db: Session) -> None:
"""
- Create default platform content pages for ALL platforms.
+ Create all default content pages for all platforms.
- This function is idempotent - it will skip pages that already exist.
+ This function is idempotent — it skips pages that already exist.
"""
print("\n" + "=" * 70)
- print("Creating Default Platform Content Pages (CMS)")
+ print("Creating Default Content Pages (CMS)")
print("=" * 70 + "\n")
# Load all platforms
@@ -576,33 +1021,79 @@ def create_default_pages(db: Session) -> None:
total_skipped = 0
for platform in platforms:
- print(f" Platform: {platform.name} (code={platform.code})")
-
+ print(f"\n Platform: {platform.name} (code={platform.code})")
created_count = 0
skipped_count = 0
- # Create platform homepage
- if _create_page(db, platform.id, PLATFORM_HOMEPAGE, is_platform_page=True):
- created_count += 1
+ # ------------------------------------------------------------------
+ # 1. Platform Homepage (slug="home", is_platform_page=True, sections)
+ # ------------------------------------------------------------------
+ sections_fn = HOMEPAGE_SECTIONS.get(platform.code)
+ if sections_fn:
+ sections_data = sections_fn()
+ homepage_data = {
+ "slug": "home",
+ "title": f"{platform.name} - Home",
+ "content": "",
+ "meta_description": f"{platform.name} — digital tools for Luxembourg businesses.",
+ "show_in_footer": False,
+ "show_in_header": False,
+ "display_order": 0,
+ }
+ if _create_page(
+ db, platform.id, homepage_data,
+ is_platform_page=True, sections=sections_data,
+ ):
+ created_count += 1
+ else:
+ skipped_count += 1
else:
- skipped_count += 1
+ print(f" Warning: No homepage sections defined for platform '{platform.code}'")
- # Create default store content pages
- for page_data in DEFAULT_PAGES:
- if _create_page(db, platform.id, page_data):
+ # ------------------------------------------------------------------
+ # 2. Platform Marketing Pages (is_platform_page=True)
+ # ------------------------------------------------------------------
+ platform_pages = _get_platform_pages(platform.code)
+ for page_data in platform_pages:
+ if _create_page(db, platform.id, page_data, is_platform_page=True):
created_count += 1
else:
skipped_count += 1
+ # Shared legal pages (privacy, terms) for all platforms
+ for page_data in SHARED_PLATFORM_PAGES:
+ if _create_page(db, platform.id, page_data, is_platform_page=True):
+ created_count += 1
+ else:
+ skipped_count += 1
+
+ # ------------------------------------------------------------------
+ # 3. Store Default Pages (is_platform_page=False, store_id=NULL)
+ # Only for platforms that host stores (not wizard.lu main)
+ # ------------------------------------------------------------------
+ if platform.code != "main":
+ for page_data in STORE_DEFAULTS_COMMON:
+ if _create_page(db, platform.id, page_data, is_platform_page=False):
+ created_count += 1
+ else:
+ skipped_count += 1
+
+ # OMS-specific: shipping + returns
+ if platform.code == "oms":
+ for page_data in STORE_DEFAULTS_OMS_EXTRA:
+ if _create_page(db, platform.id, page_data, is_platform_page=False):
+ created_count += 1
+ else:
+ skipped_count += 1
+
print(f" --- {created_count} created, {skipped_count} skipped")
- print()
total_created += created_count
total_skipped += skipped_count
db.commit()
- print("=" * 70)
+ print("\n" + "=" * 70)
print("Summary:")
print(f" Platforms: {len(platforms)}")
print(f" Created: {total_created} pages")
@@ -611,13 +1102,7 @@ def create_default_pages(db: Session) -> None:
print("=" * 70 + "\n")
if total_created > 0:
- print("Default platform content pages created successfully!\n")
- print("Next steps:")
- print(
- " 1. View pages at: /about, /contact, /faq, /shipping, /returns, /privacy, /terms"
- )
- print(" 2. Stores can override these pages through the store dashboard")
- print(" 3. Edit platform defaults through the admin panel\n")
+ print("Default content pages created successfully!\n")
else:
print("All default pages already exist. No changes made.\n")