Add structured JSON sections to ContentPage for multi-language homepage editing:
Database:
- Add `sections` JSON column to content_pages table
- Migration z8i9j0k1l2m3 adds the column
Schema:
- New models/schema/homepage_sections.py with Pydantic schemas
- TranslatableText for language-keyed translations
- HeroSection, FeaturesSection, PricingSection, CTASection
Templates:
- New section partials in app/templates/platform/sections/
- Updated homepage-default.html to render sections dynamically
- Fallback to placeholder content when sections not configured
Service:
- update_homepage_sections() - validate and save all sections
- update_single_section() - update individual section
- get_default_sections() - empty structure for new homepages
API:
- GET /{page_id}/sections - get sections with platform languages
- PUT /{page_id}/sections - update all sections
- PUT /{page_id}/sections/{section_name} - update single section
Admin UI:
- Section editor appears when editing homepage (slug='home')
- Language tabs from platform.supported_languages
- Accordion sections for Hero, Features, Pricing, CTA
- Button/feature card repeaters with add/remove
Also fixes broken line 181 in z4e5f6a7b8c9 migration.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
175 lines
5.6 KiB
Python
175 lines
5.6 KiB
Python
"""
|
|
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"
|