diff --git a/alembic/versions/z4e5f6a7b8c9_add_multi_platform_support.py b/alembic/versions/z4e5f6a7b8c9_add_multi_platform_support.py
index 185ed0cf..ea8b53b4 100644
--- a/alembic/versions/z4e5f6a7b8c9_add_multi_platform_support.py
+++ b/alembic/versions/z4e5f6a7b8c9_add_multi_platform_support.py
@@ -178,7 +178,9 @@ def upgrade() -> None:
""")
)
-I dn
+ # Get OMS platform ID for backfilling
+ result = conn.execute(sa.text("SELECT id FROM platforms WHERE code = 'oms'"))
+ oms_platform_id = result.fetchone()[0]
# =========================================================================
# 6. Backfill content_pages with platform_id
diff --git a/alembic/versions/z8i9j0k1l2m3_add_sections_to_content_pages.py b/alembic/versions/z8i9j0k1l2m3_add_sections_to_content_pages.py
new file mode 100644
index 00000000..dbf4da8d
--- /dev/null
+++ b/alembic/versions/z8i9j0k1l2m3_add_sections_to_content_pages.py
@@ -0,0 +1,36 @@
+"""Add sections column to content_pages
+
+Revision ID: z8i9j0k1l2m3
+Revises: z7h8i9j0k1l2
+Create Date: 2026-01-23
+
+Adds sections JSON column for structured homepage editing with multi-language support.
+The sections column stores hero, features, pricing, and cta configurations
+with TranslatableText pattern for i18n.
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "z8i9j0k1l2m3"
+down_revision = "z7h8i9j0k1l2"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ op.add_column(
+ "content_pages",
+ sa.Column(
+ "sections",
+ sa.JSON(),
+ nullable=True,
+ comment="Structured homepage sections (hero, features, pricing, cta) with i18n",
+ ),
+ )
+
+
+def downgrade() -> None:
+ op.drop_column("content_pages", "sections")
diff --git a/app/api/v1/admin/content_pages.py b/app/api/v1/admin/content_pages.py
index f7fbd057..e5243205 100644
--- a/app/api/v1/admin/content_pages.py
+++ b/app/api/v1/admin/content_pages.py
@@ -284,3 +284,116 @@ def delete_page(
"""Delete a content page."""
content_page_service.delete_page_or_raise(db, page_id)
db.commit()
+
+
+# ============================================================================
+# HOMEPAGE SECTIONS MANAGEMENT
+# ============================================================================
+
+
+class HomepageSectionsResponse(BaseModel):
+ """Response containing homepage sections with platform language info."""
+
+ sections: dict | None = None
+ supported_languages: list[str] = Field(default_factory=lambda: ["fr", "de", "en"])
+ default_language: str = "fr"
+
+
+class SectionUpdateResponse(BaseModel):
+ """Response after updating sections."""
+
+ message: str
+ sections: dict | None = None
+
+
+@router.get("/{page_id}/sections", response_model=HomepageSectionsResponse)
+def get_page_sections(
+ page_id: int,
+ current_user: User = Depends(get_current_admin_api),
+ db: Session = Depends(get_db),
+):
+ """
+ Get homepage sections for a content page.
+
+ Returns sections along with platform language settings for the editor.
+ """
+ page = content_page_service.get_page_by_id_or_raise(db, page_id)
+
+ # Get platform languages
+ platform = page.platform
+ supported_languages = (
+ platform.supported_languages if platform else ["fr", "de", "en"]
+ )
+ default_language = platform.default_language if platform else "fr"
+
+ return {
+ "sections": page.sections,
+ "supported_languages": supported_languages,
+ "default_language": default_language,
+ }
+
+
+@router.put("/{page_id}/sections", response_model=SectionUpdateResponse)
+def update_page_sections(
+ page_id: int,
+ sections: dict,
+ current_user: User = Depends(get_current_admin_api),
+ db: Session = Depends(get_db),
+):
+ """
+ Update all homepage sections at once.
+
+ Expected structure:
+ {
+ "hero": { ... },
+ "features": { ... },
+ "pricing": { ... },
+ "cta": { ... }
+ }
+ """
+ page = content_page_service.update_homepage_sections(
+ db,
+ page_id=page_id,
+ sections=sections,
+ updated_by=current_user.id,
+ )
+ db.commit()
+
+ return {
+ "message": "Sections updated successfully",
+ "sections": page.sections,
+ }
+
+
+@router.put("/{page_id}/sections/{section_name}", response_model=SectionUpdateResponse)
+def update_single_section(
+ page_id: int,
+ section_name: str,
+ section_data: dict,
+ current_user: User = Depends(get_current_admin_api),
+ db: Session = Depends(get_db),
+):
+ """
+ Update a single section (hero, features, pricing, or cta).
+
+ section_name must be one of: hero, features, pricing, cta
+ """
+ if section_name not in ["hero", "features", "pricing", "cta"]:
+ raise ValidationException(
+ message=f"Invalid section name: {section_name}. Must be one of: hero, features, pricing, cta",
+ field="section_name",
+ )
+
+ page = content_page_service.update_single_section(
+ db,
+ page_id=page_id,
+ section_name=section_name,
+ section_data=section_data,
+ updated_by=current_user.id,
+ )
+ db.commit()
+
+ return {
+ "message": f"Section '{section_name}' updated successfully",
+ "sections": page.sections,
+ }
diff --git a/app/services/content_page_service.py b/app/services/content_page_service.py
index c94edbb4..b930e2ab 100644
--- a/app/services/content_page_service.py
+++ b/app/services/content_page_service.py
@@ -872,6 +872,130 @@ class ContentPageService:
if not success:
raise ContentPageNotFoundException(identifier=page_id)
+ # =========================================================================
+ # Homepage Sections Management
+ # =========================================================================
+
+ @staticmethod
+ def update_homepage_sections(
+ db: Session,
+ page_id: int,
+ sections: dict,
+ updated_by: int | None = None,
+ ) -> ContentPage:
+ """
+ Update homepage sections with validation.
+
+ Args:
+ db: Database session
+ page_id: Content page ID
+ sections: Homepage sections dict (validated against HomepageSections schema)
+ updated_by: User ID making the update
+
+ Returns:
+ Updated ContentPage
+
+ Raises:
+ ContentPageNotFoundException: If page not found
+ ValidationError: If sections schema invalid
+ """
+ from models.schema.homepage_sections import HomepageSections
+
+ page = ContentPageService.get_page_by_id_or_raise(db, page_id)
+
+ # Validate sections against schema
+ validated = HomepageSections(**sections)
+
+ # Update page
+ page.sections = validated.model_dump()
+ page.updated_by = updated_by
+
+ db.flush()
+ db.refresh(page)
+
+ logger.info(f"[CMS] Updated homepage sections for page_id={page_id}")
+ return page
+
+ @staticmethod
+ def update_single_section(
+ db: Session,
+ page_id: int,
+ section_name: str,
+ section_data: dict,
+ updated_by: int | None = None,
+ ) -> ContentPage:
+ """
+ Update a single section within homepage sections.
+
+ Args:
+ db: Database session
+ page_id: Content page ID
+ section_name: Section to update (hero, features, pricing, cta)
+ section_data: Section configuration dict
+ updated_by: User ID making the update
+
+ Returns:
+ Updated ContentPage
+
+ Raises:
+ ContentPageNotFoundException: If page not found
+ ValueError: If section name is invalid
+ """
+ from models.schema.homepage_sections import (
+ HeroSection,
+ FeaturesSection,
+ PricingSection,
+ CTASection,
+ )
+
+ SECTION_SCHEMAS = {
+ "hero": HeroSection,
+ "features": FeaturesSection,
+ "pricing": PricingSection,
+ "cta": CTASection,
+ }
+
+ if section_name not in SECTION_SCHEMAS:
+ raise ValueError(f"Invalid section name: {section_name}. Must be one of: {list(SECTION_SCHEMAS.keys())}")
+
+ page = ContentPageService.get_page_by_id_or_raise(db, page_id)
+
+ # Validate section data against its schema
+ schema = SECTION_SCHEMAS[section_name]
+ validated_section = schema(**section_data)
+
+ # Initialize sections if needed
+ current_sections = page.sections or {}
+ current_sections[section_name] = validated_section.model_dump()
+
+ page.sections = current_sections
+ page.updated_by = updated_by
+
+ db.flush()
+ db.refresh(page)
+
+ logger.info(f"[CMS] Updated section '{section_name}' for page_id={page_id}")
+ return page
+
+ @staticmethod
+ def get_default_sections(languages: list[str] | None = None) -> dict:
+ """
+ Get empty sections structure for new homepage.
+
+ Args:
+ languages: List of language codes from platform.supported_languages.
+ Defaults to ['fr', 'de', 'en'] if not provided.
+
+ Returns:
+ Empty sections dict with language placeholders
+ """
+ from models.schema.homepage_sections import HomepageSections
+
+ if languages is None:
+ languages = ["fr", "de", "en"]
+
+ return HomepageSections.get_empty_structure(languages).model_dump()
+
# Singleton instance
content_page_service = ContentPageService()
diff --git a/app/templates/admin/content-page-edit.html b/app/templates/admin/content-page-edit.html
index 72f9d604..8502bf6b 100644
--- a/app/templates/admin/content-page-edit.html
+++ b/app/templates/admin/content-page-edit.html
@@ -170,6 +170,317 @@
+
+
+
+
+
+
+ Homepage Sections
+ (Multi-language content)
+
+
+
+ Loading sections...
+
+
+
+
+
+
+
+
+
+
+ (default)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hero Section
+
+
+
+
+
+ Badge Text
+
+
+
+
+ Title *
+
+
+
+
+ Subtitle
+
+
+
+
+
+
+
+
+
+
+
+
+ Features Section
+
+
+
+
+
+ Section Title
+
+
+
+
+
Feature Cards
+
+
+
+
+ + Add Feature Card
+
+
+
+
+
+
+
+
+
+
+ Pricing Section
+
+
+
+
+
+ Section Title
+
+
+
+
+
+ Use subscription tiers from database
+
+
When enabled, pricing cards are dynamically pulled from your subscription tier configuration.
+
+
+
+
+
+
+
+
+ Call to Action Section
+
+
+
+
+
+ Title
+
+
+
+
+ Subtitle
+
+
+
+
+
+
+
+
+
+
diff --git a/app/templates/platform/homepage-default.html b/app/templates/platform/homepage-default.html
index efd93210..fbab764a 100644
--- a/app/templates/platform/homepage-default.html
+++ b/app/templates/platform/homepage-default.html
@@ -1,9 +1,15 @@
{# app/templates/platform/homepage-default.html #}
-{# Default platform homepage template #}
+{# Default platform homepage template with section-based rendering #}
{% extends "platform/base.html" %}
+{# Import section partials #}
+{% from 'platform/sections/_hero.html' import render_hero %}
+{% from 'platform/sections/_features.html' import render_features %}
+{% from 'platform/sections/_pricing.html' import render_pricing %}
+{% from 'platform/sections/_cta.html' import render_cta %}
+
{% block title %}
- {% if page %}{{ page.title }}{% else %}Home{% endif %} - Multi-Vendor Marketplace
+ {% if page %}{{ page.title }}{% else %}Home{% endif %} - {{ platform.name if platform else 'Multi-Vendor Marketplace' }}
{% endblock %}
{% block meta_description %}
@@ -15,214 +21,114 @@
{% endblock %}
{% block content %}
-
-
-
+{# Set up language context #}
+{% set lang = request.state.language or (platform.default_language if platform else 'fr') %}
+{% set default_lang = platform.default_language if platform else 'fr' %}
+
+{# ═══════════════════════════════════════════════════════════════════════════ #}
+{# SECTION-BASED RENDERING (when page.sections is configured) #}
+{# ═══════════════════════════════════════════════════════════════════════════ #}
+{% if page and page.sections %}
+
+ {# Hero Section #}
+ {% if page.sections.hero %}
+ {{ render_hero(page.sections.hero, lang, default_lang) }}
+ {% endif %}
+
+ {# Features Section #}
+ {% if page.sections.features %}
+ {{ render_features(page.sections.features, lang, default_lang) }}
+ {% endif %}
+
+ {# Pricing Section #}
+ {% if page.sections.pricing %}
+ {{ render_pricing(page.sections.pricing, lang, default_lang, tiers) }}
+ {% endif %}
+
+ {# CTA Section #}
+ {% if page.sections.cta %}
+ {{ render_cta(page.sections.cta, lang, default_lang) }}
+ {% endif %}
+
+{% else %}
+{# ═══════════════════════════════════════════════════════════════════════════ #}
+{# PLACEHOLDER CONTENT (when sections not configured) #}
+{# ═══════════════════════════════════════════════════════════════════════════ #}
+
+
- {% if page %}
- {# CMS-driven content #}
-
- {{ page.title }}
-
-
- {{ page.content | safe }}{# sanitized: CMS content #}
-
- {% else %}
- {# Default fallback content #}
-
- Welcome to Our Marketplace
-
-
- Connect vendors with customers worldwide. Build your online store and reach millions of shoppers.
-
- {% endif %}
-
+
+ {{ _('homepage.placeholder.title') or 'Configure Your Homepage' }}
+
+
+ {{ _('homepage.placeholder.subtitle') or 'Use the admin panel to configure homepage sections with multi-language content.' }}
+
-
-
-
+
- Why Choose Our Platform?
+ {{ _('homepage.placeholder.features_title') or 'Features Section' }}
- Everything you need to launch and grow your online business
+ {{ _('homepage.placeholder.features_subtitle') or 'Configure feature cards in the admin panel' }}
-
-
-
-
-
+ {% for i in range(3) %}
+
+
-
- Lightning Fast
+
+ Feature {{ i + 1 }}
-
- Optimized for speed and performance. Your store loads in milliseconds.
-
-
-
-
-
-
-
- Secure & Reliable
-
-
- Enterprise-grade security with 99.9% uptime guarantee.
-
-
-
-
-
-
-
- Fully Customizable
-
-
- Brand your store with custom themes, colors, and layouts.
+
+ Configure this feature card
+ {% endfor %}
-
-
-
-
-
-
-
- Featured Vendors
-
-
- Discover amazing shops from around the world
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
- Ready to Get Started?
+
+ {{ _('homepage.placeholder.cta_title') or 'Call to Action' }}
-
- Join thousands of vendors already selling on our platform
+
+ {{ _('homepage.placeholder.cta_subtitle') or 'Configure CTA section in the admin panel' }}
+
+{% endif %}
{% endblock %}
diff --git a/app/templates/platform/sections/_cta.html b/app/templates/platform/sections/_cta.html
new file mode 100644
index 00000000..afa69978
--- /dev/null
+++ b/app/templates/platform/sections/_cta.html
@@ -0,0 +1,54 @@
+{# app/templates/platform/sections/_cta.html #}
+{# Call-to-action section partial with multi-language support #}
+{#
+ Parameters:
+ - cta: CTASection object (or dict)
+ - lang: Current language code
+ - default_lang: Fallback language
+#}
+
+{% macro render_cta(cta, lang, default_lang) %}
+{% if cta and cta.enabled %}
+
+
+ {# Title #}
+ {% set title = cta.title.translations.get(lang) or cta.title.translations.get(default_lang) or '' %}
+ {% if title %}
+
+ {{ title }}
+
+ {% endif %}
+
+ {# Subtitle #}
+ {% if cta.subtitle and cta.subtitle.translations %}
+ {% set subtitle = cta.subtitle.translations.get(lang) or cta.subtitle.translations.get(default_lang) %}
+ {% if subtitle %}
+
+ {{ subtitle }}
+
+ {% endif %}
+ {% endif %}
+
+ {# Buttons #}
+ {% if cta.buttons %}
+
+ {% endif %}
+
+
+{% endif %}
+{% endmacro %}
diff --git a/app/templates/platform/sections/_features.html b/app/templates/platform/sections/_features.html
new file mode 100644
index 00000000..9df04dc7
--- /dev/null
+++ b/app/templates/platform/sections/_features.html
@@ -0,0 +1,72 @@
+{# app/templates/platform/sections/_features.html #}
+{# Features section partial with multi-language support #}
+{#
+ Parameters:
+ - features: FeaturesSection object (or dict)
+ - lang: Current language code
+ - default_lang: Fallback language
+#}
+
+{% macro render_features(features, lang, default_lang) %}
+{% if features and features.enabled %}
+
+
+ {# Section header #}
+
+ {% set title = features.title.translations.get(lang) or features.title.translations.get(default_lang) or '' %}
+ {% if title %}
+
+ {{ title }}
+
+ {% endif %}
+
+ {% if features.subtitle and features.subtitle.translations %}
+ {% set subtitle = features.subtitle.translations.get(lang) or features.subtitle.translations.get(default_lang) %}
+ {% if subtitle %}
+
+ {{ subtitle }}
+
+ {% endif %}
+ {% endif %}
+
+
+ {# Feature cards #}
+ {% if features.features %}
+
+ {% for feature in features.features %}
+
+ {# Icon #}
+ {% if feature.icon %}
+
+ {# Support for icon names - rendered via Alpine $icon helper or direct SVG #}
+ {% if feature.icon.startswith('
+ {% endif %}
+
+ {% endif %}
+
+ {# Title #}
+ {% set feature_title = feature.title.translations.get(lang) or feature.title.translations.get(default_lang) or '' %}
+ {% if feature_title %}
+
+ {{ feature_title }}
+
+ {% endif %}
+
+ {# Description #}
+ {% set feature_desc = feature.description.translations.get(lang) or feature.description.translations.get(default_lang) or '' %}
+ {% if feature_desc %}
+
+ {{ feature_desc }}
+
+ {% endif %}
+
+ {% endfor %}
+
+ {% endif %}
+
+
+{% endif %}
+{% endmacro %}
diff --git a/app/templates/platform/sections/_hero.html b/app/templates/platform/sections/_hero.html
new file mode 100644
index 00000000..589f1adc
--- /dev/null
+++ b/app/templates/platform/sections/_hero.html
@@ -0,0 +1,71 @@
+{# app/templates/platform/sections/_hero.html #}
+{# Hero section partial with multi-language support #}
+{#
+ Parameters:
+ - hero: HeroSection object (or dict)
+ - lang: Current language code (from request.state.language)
+ - default_lang: Fallback language (from platform.default_language)
+#}
+
+{% macro render_hero(hero, lang, default_lang) %}
+{% if hero and hero.enabled %}
+
+
+
+ {# Badge #}
+ {% if hero.badge_text and hero.badge_text.translations %}
+ {% set badge = hero.badge_text.translations.get(lang) or hero.badge_text.translations.get(default_lang) %}
+ {% if badge %}
+
+ {{ badge }}
+
+ {% endif %}
+ {% endif %}
+
+ {# Title #}
+ {% set title = hero.title.translations.get(lang) or hero.title.translations.get(default_lang) or '' %}
+ {% if title %}
+
+ {% endif %}
+
+ {# Subtitle #}
+ {% set subtitle = hero.subtitle.translations.get(lang) or hero.subtitle.translations.get(default_lang) or '' %}
+ {% if subtitle %}
+
+ {{ subtitle }}
+
+ {% endif %}
+
+ {# Buttons #}
+ {% if hero.buttons %}
+
+ {% endif %}
+
+
+
+ {# Background decorations #}
+
+
+
+
+
+
+{% endif %}
+{% endmacro %}
diff --git a/app/templates/platform/sections/_pricing.html b/app/templates/platform/sections/_pricing.html
new file mode 100644
index 00000000..75e0c4ca
--- /dev/null
+++ b/app/templates/platform/sections/_pricing.html
@@ -0,0 +1,116 @@
+{# app/templates/platform/sections/_pricing.html #}
+{# Pricing section partial with multi-language support #}
+{#
+ Parameters:
+ - pricing: PricingSection object (or dict)
+ - lang: Current language code
+ - default_lang: Fallback language
+ - tiers: List of subscription tiers from DB (passed via context)
+#}
+
+{% macro render_pricing(pricing, lang, default_lang, tiers) %}
+{% if pricing and pricing.enabled %}
+
+
+ {# Section header #}
+
+ {% set title = pricing.title.translations.get(lang) or pricing.title.translations.get(default_lang) or '' %}
+ {% if title %}
+
+ {{ title }}
+
+ {% endif %}
+
+ {% if pricing.subtitle and pricing.subtitle.translations %}
+ {% set subtitle = pricing.subtitle.translations.get(lang) or pricing.subtitle.translations.get(default_lang) %}
+ {% if subtitle %}
+
+ {{ subtitle }}
+
+ {% endif %}
+ {% endif %}
+
+
+ {# Pricing toggle (monthly/annual) #}
+ {% if pricing.use_subscription_tiers and tiers %}
+
+ {# Billing toggle #}
+
+
+ {{ _('pricing.monthly') or 'Monthly' }}
+
+
+
+
+
+ {{ _('pricing.annual') or 'Annual' }}
+ {{ _('pricing.save_20') or 'Save 20%' }}
+
+
+
+ {# Pricing cards #}
+
+ {% for tier in tiers %}
+
+ {% if tier.is_popular %}
+
+
+ {{ _('pricing.most_popular') or 'Most Popular' }}
+
+
+ {% endif %}
+
+
+
+ {{ tier.name }}
+
+
+ {{ tier.description or '' }}
+
+
+ {# Price #}
+
+
+ /{{ _('pricing.month') or 'mo' }}
+
+
+ {# CTA button #}
+
+ {{ _('pricing.get_started') or 'Get Started' }}
+
+
+
+ {# Features list #}
+ {% if tier.features %}
+
+ {% for feature in tier.features %}
+
+
+
+
+ {{ feature }}
+
+ {% endfor %}
+
+ {% endif %}
+
+ {% endfor %}
+
+
+ {% else %}
+ {# Placeholder when no tiers available #}
+
+ {{ _('pricing.coming_soon') or 'Pricing plans coming soon' }}
+
+ {% endif %}
+
+
+{% endif %}
+{% endmacro %}
diff --git a/docs/proposals/section-based-homepage-plan.md b/docs/proposals/section-based-homepage-plan.md
index 95d88809..5d7de616 100644
--- a/docs/proposals/section-based-homepage-plan.md
+++ b/docs/proposals/section-based-homepage-plan.md
@@ -1,8 +1,9 @@
# Section-Based Homepage Management System
-**Status:** Planning
+**Status:** COMPLETE
**Created:** 2026-01-20
-**Resume:** Ready to implement
+**Updated:** 2026-01-23
+**Completed:** All 7 phases implemented
## Problem Statement
diff --git a/models/database/content_page.py b/models/database/content_page.py
index 9f61f46e..93aa9bca 100644
--- a/models/database/content_page.py
+++ b/models/database/content_page.py
@@ -29,6 +29,7 @@ Features:
from datetime import UTC, datetime
from sqlalchemy import (
+ JSON,
Boolean,
Column,
DateTime,
@@ -107,6 +108,16 @@ class ContentPage(Base):
# Only used for landing pages (slug='landing' or 'home')
template = Column(String(50), default="default", nullable=False)
+ # Homepage sections (structured JSON for section-based editing)
+ # Only used for homepage (slug='home'). Contains hero, features, pricing, cta sections
+ # with multi-language support via TranslatableText pattern
+ sections = Column(
+ JSON,
+ nullable=True,
+ default=None,
+ comment="Structured homepage sections (hero, features, pricing, cta) with i18n",
+ )
+
# SEO
meta_description = Column(String(300), nullable=True)
meta_keywords = Column(String(300), nullable=True)
@@ -199,6 +210,7 @@ class ContentPage(Base):
"content": self.content,
"content_format": self.content_format,
"template": self.template,
+ "sections": self.sections,
"meta_description": self.meta_description,
"meta_keywords": self.meta_keywords,
"is_published": self.is_published,
diff --git a/models/schema/homepage_sections.py b/models/schema/homepage_sections.py
new file mode 100644
index 00000000..283c3be7
--- /dev/null
+++ b/models/schema/homepage_sections.py
@@ -0,0 +1,174 @@
+"""
+Homepage Section Schemas with Dynamic Multi-Language Support.
+
+Language codes are NOT hardcoded - they come from platform.supported_languages.
+The TranslatableText class stores translations as a dict where keys are language codes.
+
+Example JSON structure:
+{
+ "hero": {
+ "enabled": true,
+ "title": {"translations": {"fr": "Bienvenue", "en": "Welcome"}},
+ "subtitle": {"translations": {...}},
+ "buttons": [...]
+ },
+ "features": {...},
+ "pricing": {...},
+ "cta": {...}
+}
+"""
+
+from pydantic import BaseModel, Field
+from typing import Optional
+
+
+class TranslatableText(BaseModel):
+ """
+ Text field with translations stored as language-keyed dict.
+
+ Languages come from platform.supported_languages (not hardcoded).
+ Use .get(lang, default_lang) to retrieve translation with fallback.
+ """
+
+ translations: dict[str, str] = Field(
+ default_factory=dict, description="Language code -> translated text mapping"
+ )
+
+ def get(self, lang: str, default_lang: str = "fr") -> str:
+ """Get translation with fallback to default language."""
+ return self.translations.get(lang) or self.translations.get(default_lang) or ""
+
+ def set(self, lang: str, text: str) -> None:
+ """Set translation for a language."""
+ self.translations[lang] = text
+
+ def has_translation(self, lang: str) -> bool:
+ """Check if translation exists for language."""
+ return bool(self.translations.get(lang))
+
+
+class HeroButton(BaseModel):
+ """Button in hero or CTA section."""
+
+ text: TranslatableText = Field(default_factory=TranslatableText)
+ url: str = ""
+ style: str = Field(default="primary", description="primary, secondary, outline")
+
+
+class HeroSection(BaseModel):
+ """Hero section configuration."""
+
+ enabled: bool = True
+ badge_text: Optional[TranslatableText] = None
+ title: TranslatableText = Field(default_factory=TranslatableText)
+ subtitle: TranslatableText = Field(default_factory=TranslatableText)
+ background_type: str = Field(
+ default="gradient", description="gradient, image, solid"
+ )
+ background_image: Optional[str] = None
+ buttons: list[HeroButton] = Field(default_factory=list)
+
+
+class FeatureCard(BaseModel):
+ """Single feature in features section."""
+
+ icon: str = ""
+ title: TranslatableText = Field(default_factory=TranslatableText)
+ description: TranslatableText = Field(default_factory=TranslatableText)
+
+
+class FeaturesSection(BaseModel):
+ """Features section configuration."""
+
+ enabled: bool = True
+ title: TranslatableText = Field(default_factory=TranslatableText)
+ subtitle: Optional[TranslatableText] = None
+ features: list[FeatureCard] = Field(default_factory=list)
+ layout: str = Field(default="grid", description="grid, list, cards")
+
+
+class PricingSection(BaseModel):
+ """Pricing section configuration."""
+
+ enabled: bool = True
+ title: TranslatableText = Field(default_factory=TranslatableText)
+ subtitle: Optional[TranslatableText] = None
+ use_subscription_tiers: bool = Field(
+ default=True, description="Pull pricing from subscription_tiers table dynamically"
+ )
+
+
+class CTASection(BaseModel):
+ """Call-to-action section configuration."""
+
+ enabled: bool = True
+ title: TranslatableText = Field(default_factory=TranslatableText)
+ subtitle: Optional[TranslatableText] = None
+ buttons: list[HeroButton] = Field(default_factory=list)
+ background_type: str = Field(
+ default="gradient", description="gradient, image, solid"
+ )
+
+
+class HomepageSections(BaseModel):
+ """Complete homepage sections structure."""
+
+ hero: Optional[HeroSection] = None
+ features: Optional[FeaturesSection] = None
+ pricing: Optional[PricingSection] = None
+ cta: Optional[CTASection] = None
+
+ @classmethod
+ def get_empty_structure(cls, languages: list[str]) -> "HomepageSections":
+ """
+ Create empty section structure with language placeholders.
+
+ Args:
+ languages: List of language codes from platform.supported_languages
+
+ Returns:
+ HomepageSections with empty translations for all languages
+ """
+
+ def make_translatable(langs: list[str]) -> TranslatableText:
+ return TranslatableText(translations={lang: "" for lang in langs})
+
+ return cls(
+ hero=HeroSection(
+ title=make_translatable(languages),
+ subtitle=make_translatable(languages),
+ buttons=[],
+ ),
+ features=FeaturesSection(
+ title=make_translatable(languages),
+ features=[],
+ ),
+ pricing=PricingSection(
+ title=make_translatable(languages),
+ use_subscription_tiers=True,
+ ),
+ cta=CTASection(
+ title=make_translatable(languages),
+ buttons=[],
+ ),
+ )
+
+
+# =============================================================================
+# API Request/Response Schemas
+# =============================================================================
+
+
+class SectionUpdateRequest(BaseModel):
+ """Request to update a single section."""
+
+ section_name: str = Field(..., description="hero, features, pricing, or cta")
+ section_data: dict = Field(..., description="Section configuration")
+
+
+class HomepageSectionsResponse(BaseModel):
+ """Response containing all homepage sections with platform language info."""
+
+ sections: Optional[HomepageSections] = None
+ supported_languages: list[str] = Field(default_factory=lambda: ["fr", "de", "en"])
+ default_language: str = "fr"
diff --git a/static/admin/js/content-page-edit.js b/static/admin/js/content-page-edit.js
index 34ae3898..7056eea4 100644
--- a/static/admin/js/content-page-edit.js
+++ b/static/admin/js/content-page-edit.js
@@ -39,6 +39,51 @@ function contentPageEditor(pageId) {
error: null,
successMessage: null,
+ // ========================================
+ // HOMEPAGE SECTIONS STATE
+ // ========================================
+ supportedLanguages: ['fr', 'de', 'en'],
+ defaultLanguage: 'fr',
+ currentLang: 'fr',
+ openSection: null,
+ sectionsLoaded: false,
+ languageNames: {
+ en: 'English',
+ fr: 'Français',
+ de: 'Deutsch',
+ lb: 'Lëtzebuergesch'
+ },
+ sections: {
+ hero: {
+ enabled: true,
+ badge_text: { translations: {} },
+ title: { translations: {} },
+ subtitle: { translations: {} },
+ background_type: 'gradient',
+ buttons: []
+ },
+ features: {
+ enabled: true,
+ title: { translations: {} },
+ subtitle: { translations: {} },
+ features: [],
+ layout: 'grid'
+ },
+ pricing: {
+ enabled: true,
+ title: { translations: {} },
+ subtitle: { translations: {} },
+ use_subscription_tiers: true
+ },
+ cta: {
+ enabled: true,
+ title: { translations: {} },
+ subtitle: { translations: {} },
+ buttons: [],
+ background_type: 'gradient'
+ }
+ },
+
// Initialize
async init() {
contentPageEditLog.info('=== CONTENT PAGE EDITOR INITIALIZING ===');
@@ -59,6 +104,11 @@ function contentPageEditor(pageId) {
contentPageEditLog.group('Loading page for editing');
await this.loadPage();
contentPageEditLog.groupEnd();
+
+ // Load sections if this is a homepage
+ if (this.form.slug === 'home') {
+ await this.loadSections();
+ }
} else {
// Create mode - use default values
contentPageEditLog.info('Create mode - using default form values');
@@ -67,6 +117,11 @@ function contentPageEditor(pageId) {
contentPageEditLog.info('=== CONTENT PAGE EDITOR INITIALIZATION COMPLETE ===');
},
+ // Check if we should show section editor
+ get isHomepage() {
+ return this.form.slug === 'home';
+ },
+
// Load vendors for dropdown
async loadVendors() {
this.loadingVendors = true;
@@ -128,6 +183,154 @@ function contentPageEditor(pageId) {
}
},
+ // ========================================
+ // HOMEPAGE SECTIONS METHODS
+ // ========================================
+
+ // Load sections for homepage
+ async loadSections() {
+ if (!this.pageId || this.form.slug !== 'home') {
+ contentPageEditLog.debug('Skipping section load - not a homepage');
+ return;
+ }
+
+ try {
+ contentPageEditLog.info('Loading homepage sections...');
+ const response = await apiClient.get(`/admin/content-pages/${this.pageId}/sections`);
+ const data = response.data || response;
+
+ this.supportedLanguages = data.supported_languages || ['fr', 'de', 'en'];
+ this.defaultLanguage = data.default_language || 'fr';
+ this.currentLang = this.defaultLanguage;
+
+ if (data.sections) {
+ this.sections = this.mergeWithDefaults(data.sections);
+ contentPageEditLog.info('Sections loaded:', Object.keys(data.sections));
+ } else {
+ this.initializeEmptySections();
+ contentPageEditLog.info('No sections found - initialized empty structure');
+ }
+
+ this.sectionsLoaded = true;
+ } catch (err) {
+ contentPageEditLog.error('Error loading sections:', err);
+ }
+ },
+
+ // Merge loaded sections with default structure
+ mergeWithDefaults(loadedSections) {
+ const defaults = this.getDefaultSectionStructure();
+
+ // Deep merge each section
+ for (const key of ['hero', 'features', 'pricing', 'cta']) {
+ if (loadedSections[key]) {
+ defaults[key] = { ...defaults[key], ...loadedSections[key] };
+ }
+ }
+
+ return defaults;
+ },
+
+ // Get default section structure
+ getDefaultSectionStructure() {
+ const emptyTranslations = () => {
+ const t = {};
+ this.supportedLanguages.forEach(lang => t[lang] = '');
+ return { translations: t };
+ };
+
+ return {
+ hero: {
+ enabled: true,
+ badge_text: emptyTranslations(),
+ title: emptyTranslations(),
+ subtitle: emptyTranslations(),
+ background_type: 'gradient',
+ buttons: []
+ },
+ features: {
+ enabled: true,
+ title: emptyTranslations(),
+ subtitle: emptyTranslations(),
+ features: [],
+ layout: 'grid'
+ },
+ pricing: {
+ enabled: true,
+ title: emptyTranslations(),
+ subtitle: emptyTranslations(),
+ use_subscription_tiers: true
+ },
+ cta: {
+ enabled: true,
+ title: emptyTranslations(),
+ subtitle: emptyTranslations(),
+ buttons: [],
+ background_type: 'gradient'
+ }
+ };
+ },
+
+ // Initialize empty sections for all languages
+ initializeEmptySections() {
+ this.sections = this.getDefaultSectionStructure();
+ },
+
+ // Add a button to hero or cta section
+ addButton(sectionName) {
+ const newButton = {
+ text: { translations: {} },
+ url: '',
+ style: 'primary'
+ };
+ this.supportedLanguages.forEach(lang => {
+ newButton.text.translations[lang] = '';
+ });
+ this.sections[sectionName].buttons.push(newButton);
+ contentPageEditLog.debug(`Added button to ${sectionName}`);
+ },
+
+ // Remove a button from hero or cta section
+ removeButton(sectionName, index) {
+ this.sections[sectionName].buttons.splice(index, 1);
+ contentPageEditLog.debug(`Removed button ${index} from ${sectionName}`);
+ },
+
+ // Add a feature card
+ addFeature() {
+ const newFeature = {
+ icon: '',
+ title: { translations: {} },
+ description: { translations: {} }
+ };
+ this.supportedLanguages.forEach(lang => {
+ newFeature.title.translations[lang] = '';
+ newFeature.description.translations[lang] = '';
+ });
+ this.sections.features.features.push(newFeature);
+ contentPageEditLog.debug('Added feature card');
+ },
+
+ // Remove a feature card
+ removeFeature(index) {
+ this.sections.features.features.splice(index, 1);
+ contentPageEditLog.debug(`Removed feature ${index}`);
+ },
+
+ // Save sections
+ async saveSections() {
+ if (!this.pageId || !this.isHomepage) return;
+
+ try {
+ contentPageEditLog.info('Saving sections...');
+ await apiClient.put(`/admin/content-pages/${this.pageId}/sections`, this.sections);
+ contentPageEditLog.info('Sections saved successfully');
+ } catch (err) {
+ contentPageEditLog.error('Error saving sections:', err);
+ throw err;
+ }
+ },
+
// Save page (create or update)
async savePage() {
if (this.saving) return;
@@ -161,6 +364,12 @@ function contentPageEditor(pageId) {
if (this.pageId) {
// Update existing page
response = await apiClient.put(`/admin/content-pages/${this.pageId}`, payload);
+
+ // Also save sections if this is a homepage
+ if (this.isHomepage && this.sectionsLoaded) {
+ await this.saveSections();
+ }
+
this.successMessage = 'Page updated successfully!';
contentPageEditLog.info('Page updated');
} else {