diff --git a/app/modules/base.py b/app/modules/base.py
index b1552532..e92b952e 100644
--- a/app/modules/base.py
+++ b/app/modules/base.py
@@ -95,6 +95,7 @@ class MenuItemDefinition:
requires_permission: str | None = None
badge_source: str | None = None
is_super_admin_only: bool = False
+ header_template: str | None = None # Optional partial for custom header rendering
@dataclass
diff --git a/app/modules/cart/definition.py b/app/modules/cart/definition.py
index 795f3a37..92097c61 100644
--- a/app/modules/cart/definition.py
+++ b/app/modules/cart/definition.py
@@ -72,6 +72,7 @@ cart_module = ModuleDefinition(
icon="shopping-cart",
route="cart",
order=20,
+ header_template="cart/storefront/partials/header-cart.html",
),
],
),
diff --git a/app/modules/cart/templates/cart/storefront/partials/header-cart.html b/app/modules/cart/templates/cart/storefront/partials/header-cart.html
new file mode 100644
index 00000000..1d026b19
--- /dev/null
+++ b/app/modules/cart/templates/cart/storefront/partials/header-cart.html
@@ -0,0 +1,10 @@
+{# cart/storefront/partials/header-cart.html #}
+{# Cart icon with badge for storefront header — provided by cart module #}
+
+
+
+
+
diff --git a/app/modules/catalog/definition.py b/app/modules/catalog/definition.py
index 77f23529..c95d3c38 100644
--- a/app/modules/catalog/definition.py
+++ b/app/modules/catalog/definition.py
@@ -152,6 +152,7 @@ catalog_module = ModuleDefinition(
icon="search",
route="",
order=10,
+ header_template="catalog/storefront/partials/header-search.html",
),
],
),
diff --git a/app/modules/catalog/templates/catalog/storefront/partials/header-search.html b/app/modules/catalog/templates/catalog/storefront/partials/header-search.html
new file mode 100644
index 00000000..8818e0c0
--- /dev/null
+++ b/app/modules/catalog/templates/catalog/storefront/partials/header-search.html
@@ -0,0 +1,5 @@
+{# catalog/storefront/partials/header-search.html #}
+{# Search button for storefront header — provided by catalog module #}
+
diff --git a/app/modules/cms/routes/pages/storefront.py b/app/modules/cms/routes/pages/storefront.py
index fd8c734e..1808a289 100644
--- a/app/modules/cms/routes/pages/storefront.py
+++ b/app/modules/cms/routes/pages/storefront.py
@@ -60,12 +60,14 @@ async def storefront_homepage(
include_unpublished=False,
)
- # Resolve placeholders for store default pages (title + content)
+ # Resolve placeholders for store default pages (title, content, sections)
page_content = None
page_title = None
+ page_sections = None
if page:
page_content = page.content
page_title = page.title
+ page_sections = page.sections
if page.is_store_default and store:
page_content = content_page_service.resolve_placeholders(
page.content, store
@@ -73,12 +75,18 @@ async def storefront_homepage(
page_title = content_page_service.resolve_placeholders(
page.title, store
)
+ if page_sections:
+ page_sections = content_page_service.resolve_placeholders_deep(
+ page_sections, store
+ )
context = get_storefront_context(request, db=db, page=page)
if page_content:
context["page_content"] = page_content
if page_title:
context["page_title"] = page_title
+ if page_sections:
+ context["page_sections"] = page_sections
# Select template based on page.template field (or default)
template_map = {
diff --git a/app/modules/cms/services/content_page_service.py b/app/modules/cms/services/content_page_service.py
index 4757f73b..ac6c4f1d 100644
--- a/app/modules/cms/services/content_page_service.py
+++ b/app/modules/cms/services/content_page_service.py
@@ -24,6 +24,7 @@ Lookup Strategy for Store Storefronts:
import logging
from datetime import UTC, datetime
+from typing import Any
from sqlalchemy import and_
from sqlalchemy.orm import Session
@@ -991,6 +992,28 @@ class ContentPageService:
content = content.replace(placeholder, value)
return content
+ @staticmethod
+ def resolve_placeholders_deep(data, store) -> Any:
+ """
+ Recursively resolve {{store_name}} etc. in a nested data structure
+ (dicts, lists, strings). Used for sections JSON in store default pages.
+ """
+ if not data or not store:
+ return data
+ if isinstance(data, str):
+ return ContentPageService.resolve_placeholders(data, store)
+ if isinstance(data, dict):
+ return {
+ k: ContentPageService.resolve_placeholders_deep(v, store)
+ for k, v in data.items()
+ }
+ if isinstance(data, list):
+ return [
+ ContentPageService.resolve_placeholders_deep(item, store)
+ for item in data
+ ]
+ return data
+
# =========================================================================
# Homepage Sections Management
# =========================================================================
diff --git a/app/modules/cms/services/store_theme_service.py b/app/modules/cms/services/store_theme_service.py
index 2e34fc85..cbcf759f 100644
--- a/app/modules/cms/services/store_theme_service.py
+++ b/app/modules/cms/services/store_theme_service.py
@@ -70,7 +70,7 @@ class StoreThemeService:
"""
from app.modules.tenancy.services.store_service import store_service
- store = store_service.get_store_by_code(db, store_code)
+ store = store_service.get_store_by_code_or_subdomain(db, store_code)
if not store:
self.logger.warning(f"Store not found: {store_code}")
diff --git a/app/modules/cms/templates/cms/storefront/landing-full.html b/app/modules/cms/templates/cms/storefront/landing-full.html
index 8b1fff14..dbea2f23 100644
--- a/app/modules/cms/templates/cms/storefront/landing-full.html
+++ b/app/modules/cms/templates/cms/storefront/landing-full.html
@@ -14,7 +14,8 @@
{# SECTION-BASED RENDERING (when page.sections is configured) #}
{# Used by POC builder templates — takes priority over hardcoded HTML #}
{# ═══════════════════════════════════════════════════════════════════ #}
-{% if page and page.sections %}
+{% set sections = page_sections if page_sections is defined and page_sections else (page.sections if page else none) %}
+{% if sections %}
{% from 'cms/platform/sections/_hero.html' import render_hero %}
{% from 'cms/platform/sections/_features.html' import render_features %}
{% from 'cms/platform/sections/_testimonials.html' import render_testimonials %}
@@ -26,12 +27,12 @@
{% set default_lang = 'fr' %}
- {% if page.sections.hero %}{{ render_hero(page.sections.hero, lang, default_lang) }}{% endif %}
- {% if page.sections.features %}{{ render_features(page.sections.features, lang, default_lang) }}{% endif %}
- {% if page.sections.testimonials %}{{ render_testimonials(page.sections.testimonials, lang, default_lang) }}{% endif %}
- {% if page.sections.gallery %}{{ render_gallery(page.sections.gallery, lang, default_lang) }}{% endif %}
- {% if page.sections.contact_info %}{{ render_contact_info(page.sections.contact_info, lang, default_lang) }}{% endif %}
- {% if page.sections.cta %}{{ render_cta(page.sections.cta, lang, default_lang) }}{% endif %}
+ {% if sections.hero %}{{ render_hero(sections.hero, lang, default_lang) }}{% endif %}
+ {% if sections.features %}{{ render_features(sections.features, lang, default_lang) }}{% endif %}
+ {% if sections.testimonials %}{{ render_testimonials(sections.testimonials, lang, default_lang) }}{% endif %}
+ {% if sections.gallery %}{{ render_gallery(sections.gallery, lang, default_lang) }}{% endif %}
+ {% if sections.contact_info %}{{ render_contact_info(sections.contact_info, lang, default_lang) }}{% endif %}
+ {% if sections.cta %}{{ render_cta(sections.cta, lang, default_lang) }}{% endif %}
{% else %}
diff --git a/app/modules/core/services/menu_discovery_service.py b/app/modules/core/services/menu_discovery_service.py
index afd02a4c..fe775bc5 100644
--- a/app/modules/core/services/menu_discovery_service.py
+++ b/app/modules/core/services/menu_discovery_service.py
@@ -67,6 +67,7 @@ class DiscoveredMenuItem:
section_order: int
is_visible: bool = True
is_module_enabled: bool = True
+ header_template: str | None = None
@dataclass
@@ -191,6 +192,7 @@ class MenuDiscoveryService:
section_label_key=section.label_key,
section_order=section.order,
is_module_enabled=is_module_enabled,
+ header_template=item.header_template,
)
sections_map[section.id].items.append(discovered_item)
diff --git a/app/templates/storefront/base.html b/app/templates/storefront/base.html
index 6d98ed52..9b3ed4d7 100644
--- a/app/templates/storefront/base.html
+++ b/app/templates/storefront/base.html
@@ -105,36 +105,17 @@
{% endfor %}
- {# Right side actions #}
+ {# Right side actions — module-provided via header_template #}
-
- {# Action items from enabled modules (search, cart, etc.) #}
- {% set action_ids = storefront_nav.get('actions', [])|map(attribute='id')|list %}
-
- {% if 'search' in action_ids %}
- {# Search #}
-
- {% endif %}
-
- {% if 'cart' in action_ids %}
- {# Cart #}
-
-
-
-
-
- {% endif %}{# cart #}
+ {% for item in storefront_nav.get('actions', []) %}
+ {% if item.header_template %}
+ {% include item.header_template %}
+ {% else %}
+
+
+
+ {% endif %}
+ {% endfor %}
{# Theme toggle #}
""",
- "content_translations": tt(
- # English
- """
-
Welcome
-
{{store_name}} is here to serve you. Browse our offerings and discover what we have for you.
-
""",
- # French
- """
-
Bienvenue
-
{{store_name}} est là pour vous servir. Découvrez nos offres et ce que nous avons pour vous.
-
""",
- # German
- """
-
Willkommen
-
{{store_name}} ist für Sie da. Entdecken Sie unser Angebot.
-
""",
- # Luxembourgish
- """
-
Wëllkomm
-
{{store_name}} ass fir Iech do. Entdeckt eist Angebot.
-
""",
- ),
- "template": "default",
- "meta_description": "Welcome to {{store_name}}",
+ "content": "",
+ "template": "full",
+ "meta_description": "{{store_name}}",
"show_in_header": False,
"show_in_footer": False,
"display_order": 0,
}
+def _store_homepage_sections_oms() -> dict:
+ """Store homepage sections for OMS platform stores."""
+ return {
+ "hero": {
+ "enabled": True,
+ "title": t(
+ "Bienvenue chez {{store_name}}",
+ "Welcome to {{store_name}}",
+ "Willkommen bei {{store_name}}",
+ ),
+ "subtitle": t(
+ "Découvrez notre sélection de produits et profitez d'une expérience d'achat exceptionnelle.",
+ "Discover our product selection and enjoy an exceptional shopping experience.",
+ "Entdecken Sie unsere Produktauswahl und genießen Sie ein außergewöhnliches Einkaufserlebnis.",
+ ),
+ "background_type": "gradient",
+ "buttons": [
+ {
+ "text": t("Voir nos produits", "Browse Products", "Produkte ansehen"),
+ "url": "products",
+ "style": "primary",
+ },
+ {
+ "text": t("À propos", "About Us", "Über uns"),
+ "url": "about",
+ "style": "secondary",
+ },
+ ],
+ },
+ "features": {
+ "enabled": True,
+ "title": t(
+ "Pourquoi nous choisir",
+ "Why Choose Us",
+ "Warum uns wählen",
+ ),
+ "subtitle": t(
+ "Ce qui nous distingue",
+ "What sets us apart",
+ "Was uns auszeichnet",
+ ),
+ "layout": "grid",
+ "features": [
+ {
+ "icon": "check",
+ "title": t("Qualité premium", "Premium Quality", "Premium-Qualität"),
+ "description": t(
+ "Des produits soigneusement sélectionnés pour vous.",
+ "Carefully selected products just for you.",
+ "Sorgfältig ausgewählte Produkte für Sie.",
+ ),
+ },
+ {
+ "icon": "truck",
+ "title": t("Livraison rapide", "Fast Shipping", "Schnelle Lieferung"),
+ "description": t(
+ "Livraison rapide directement chez vous.",
+ "Quick delivery right to your door.",
+ "Schnelle Lieferung direkt zu Ihnen.",
+ ),
+ },
+ {
+ "icon": "shield-check",
+ "title": t("Paiement sécurisé", "Secure Payment", "Sichere Zahlung"),
+ "description": t(
+ "Vos transactions sont protégées à 100%.",
+ "Your transactions are 100% protected.",
+ "Ihre Transaktionen sind 100% geschützt.",
+ ),
+ },
+ {
+ "icon": "chat-bubble-left",
+ "title": t("Support client", "Customer Support", "Kundensupport"),
+ "description": t(
+ "Une équipe à votre écoute pour vous accompagner.",
+ "A dedicated team ready to assist you.",
+ "Ein engagiertes Team, das Ihnen zur Seite steht.",
+ ),
+ },
+ ],
+ },
+ "cta": {
+ "enabled": True,
+ "title": t(
+ "Prêt à découvrir nos produits ?",
+ "Ready to Explore Our Products?",
+ "Bereit, unsere Produkte zu entdecken?",
+ ),
+ "subtitle": t(
+ "Parcourez notre catalogue et trouvez ce qui vous convient.",
+ "Browse our catalog and find what suits you.",
+ "Durchstöbern Sie unseren Katalog und finden Sie, was zu Ihnen passt.",
+ ),
+ "background_type": "gradient",
+ "buttons": [
+ {
+ "text": t("Voir les produits", "View Products", "Produkte ansehen"),
+ "url": "products",
+ "style": "primary",
+ },
+ ],
+ },
+ }
+
+
+def _store_homepage_sections_loyalty() -> dict:
+ """Store homepage sections for Loyalty platform stores."""
+ return {
+ "hero": {
+ "enabled": True,
+ "title": t(
+ "Bienvenue chez {{store_name}}",
+ "Welcome to {{store_name}}",
+ "Willkommen bei {{store_name}}",
+ ),
+ "subtitle": t(
+ "Rejoignez notre programme de fidélité et profitez de récompenses exclusives à chaque visite.",
+ "Join our loyalty program and enjoy exclusive rewards with every visit.",
+ "Treten Sie unserem Treueprogramm bei und genießen Sie exklusive Prämien bei jedem Besuch.",
+ ),
+ "background_type": "gradient",
+ "buttons": [
+ {
+ "text": t("Rejoindre le programme", "Join Rewards", "Programm beitreten"),
+ "url": "account/loyalty",
+ "style": "primary",
+ },
+ {
+ "text": t("En savoir plus", "Learn More", "Mehr erfahren"),
+ "url": "about",
+ "style": "secondary",
+ },
+ ],
+ },
+ "features": {
+ "enabled": True,
+ "title": t(
+ "Votre fidélité récompensée",
+ "Your Loyalty Rewarded",
+ "Ihre Treue wird belohnt",
+ ),
+ "subtitle": t(
+ "Découvrez les avantages de notre programme",
+ "Discover the benefits of our program",
+ "Entdecken Sie die Vorteile unseres Programms",
+ ),
+ "layout": "grid",
+ "features": [
+ {
+ "icon": "star",
+ "title": t("Gagnez des points", "Earn Points", "Punkte sammeln"),
+ "description": t(
+ "Cumulez des points à chaque achat et échangez-les contre des récompenses.",
+ "Accumulate points with every purchase and redeem them for rewards.",
+ "Sammeln Sie bei jedem Einkauf Punkte und lösen Sie sie gegen Prämien ein.",
+ ),
+ },
+ {
+ "icon": "gift",
+ "title": t("Récompenses exclusives", "Exclusive Rewards", "Exklusive Prämien"),
+ "description": t(
+ "Accédez à des offres et récompenses réservées aux membres.",
+ "Access offers and rewards reserved for members.",
+ "Zugang zu Angeboten und Prämien nur für Mitglieder.",
+ ),
+ },
+ {
+ "icon": "heart",
+ "title": t("Avantages membres", "Member Benefits", "Mitgliedervorteile"),
+ "description": t(
+ "Profitez d'avantages exclusifs en tant que membre fidèle.",
+ "Enjoy exclusive benefits as a loyal member.",
+ "Genießen Sie exklusive Vorteile als treues Mitglied.",
+ ),
+ },
+ ],
+ },
+ "cta": {
+ "enabled": True,
+ "title": t(
+ "Rejoignez-nous aujourd'hui",
+ "Join Us Today",
+ "Treten Sie uns noch heute bei",
+ ),
+ "subtitle": t(
+ "Inscrivez-vous à notre programme de fidélité et commencez à gagner des récompenses.",
+ "Sign up for our loyalty program and start earning rewards.",
+ "Melden Sie sich für unser Treueprogramm an und beginnen Sie, Prämien zu verdienen.",
+ ),
+ "background_type": "gradient",
+ "buttons": [
+ {
+ "text": t("S'inscrire", "Sign Up", "Anmelden"),
+ "url": "account/loyalty",
+ "style": "primary",
+ },
+ ],
+ },
+ }
+
+
+def _store_homepage_sections_hosting() -> dict:
+ """Store homepage sections for Hosting platform stores (client sites)."""
+ return {
+ "hero": {
+ "enabled": True,
+ "title": t(
+ "Bienvenue chez {{store_name}}",
+ "Welcome to {{store_name}}",
+ "Willkommen bei {{store_name}}",
+ ),
+ "subtitle": t(
+ "Votre partenaire de confiance pour des solutions numériques sur mesure.",
+ "Your trusted partner for tailored digital solutions.",
+ "Ihr vertrauenswürdiger Partner für maßgeschneiderte digitale Lösungen.",
+ ),
+ "background_type": "gradient",
+ "buttons": [
+ {
+ "text": t("Nous contacter", "Contact Us", "Kontaktieren Sie uns"),
+ "url": "contact",
+ "style": "primary",
+ },
+ {
+ "text": t("À propos", "About Us", "Über uns"),
+ "url": "about",
+ "style": "secondary",
+ },
+ ],
+ },
+ "features": {
+ "enabled": True,
+ "title": t(
+ "Nos services",
+ "Our Services",
+ "Unsere Dienstleistungen",
+ ),
+ "subtitle": t(
+ "Ce que nous pouvons faire pour vous",
+ "What we can do for you",
+ "Was wir für Sie tun können",
+ ),
+ "layout": "grid",
+ "features": [
+ {
+ "icon": "globe-alt",
+ "title": t("Site web", "Website", "Webseite"),
+ "description": t(
+ "Un site web professionnel qui reflète votre activité.",
+ "A professional website that reflects your business.",
+ "Eine professionelle Website, die Ihr Unternehmen widerspiegelt.",
+ ),
+ },
+ {
+ "icon": "shield-check",
+ "title": t("Hébergement sécurisé", "Secure Hosting", "Sicheres Hosting"),
+ "description": t(
+ "Hébergement fiable avec certificat SSL inclus.",
+ "Reliable hosting with SSL certificate included.",
+ "Zuverlässiges Hosting mit SSL-Zertifikat inklusive.",
+ ),
+ },
+ {
+ "icon": "mail",
+ "title": t("Email professionnel", "Professional Email", "Professionelle E-Mail"),
+ "description": t(
+ "Adresses email personnalisées pour votre entreprise.",
+ "Custom email addresses for your business.",
+ "Individuelle E-Mail-Adressen für Ihr Unternehmen.",
+ ),
+ },
+ ],
+ },
+ "cta": {
+ "enabled": True,
+ "title": t(
+ "Besoin d'aide ?",
+ "Need Help?",
+ "Brauchen Sie Hilfe?",
+ ),
+ "subtitle": t(
+ "Contactez-nous pour discuter de votre projet.",
+ "Contact us to discuss your project.",
+ "Kontaktieren Sie uns, um Ihr Projekt zu besprechen.",
+ ),
+ "background_type": "gradient",
+ "buttons": [
+ {
+ "text": t("Nous contacter", "Contact Us", "Kontaktieren Sie uns"),
+ "url": "contact",
+ "style": "primary",
+ },
+ ],
+ },
+ }
+
+
STORE_DEFAULTS_COMMON = [
{
"slug": "about",
@@ -2796,8 +3068,18 @@ def create_default_pages(db: Session) -> None:
# Only for platforms that host stores (not wizard.lu main)
# ------------------------------------------------------------------
if platform.code != "main":
- # Store homepage (slug="home")
- if _create_page(db, platform.id, STORE_DEFAULT_HOME, is_platform_page=False):
+ # Store homepage (slug="home") with platform-specific sections
+ store_sections_map = {
+ "oms": _store_homepage_sections_oms,
+ "loyalty": _store_homepage_sections_loyalty,
+ "hosting": _store_homepage_sections_hosting,
+ }
+ store_sections_fn = store_sections_map.get(platform.code)
+ store_sections = store_sections_fn() if store_sections_fn else None
+ if _create_page(
+ db, platform.id, STORE_DEFAULT_HOME,
+ is_platform_page=False, sections=store_sections,
+ ):
created_count += 1
else:
skipped_count += 1