feat(storefront): section-based homepages, header action partials, fixes

Phase 1 — Section-based store homepages:
- Store defaults use template="full" with per-platform sections JSON
- OMS: shop hero + features + CTA; Loyalty: rewards hero + features + CTA
- Hosting: services hero + features + CTA
- Deep placeholder resolution for {{store_name}} inside sections JSON
- landing-full.html uses resolved page_sections from context

Phase 2 — Module-contributed header actions:
- header_template field on MenuItemDefinition + DiscoveredMenuItem
- Catalog provides header-search.html partial
- Cart provides header-cart.html partial with badge
- Base template iterates storefront_nav.actions with {% include %}
- Generic icon fallback for actions without a template

Fixes:
- Store theme API: get_store_by_code → get_store_by_code_or_subdomain

Docs:
- CMS redesign proposal: menu restructure, page types, translations UI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 23:33:06 +02:00
parent adc36246b8
commit 3044490a3e
13 changed files with 641 additions and 73 deletions

View File

@@ -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 = {

View File

@@ -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
# =========================================================================

View File

@@ -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}")

View File

@@ -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' %}
<div class="min-h-screen">
{% 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 %}
</div>
{% else %}