Files
orion/docs/proposals/section-based-homepage-plan.md
Samir Boulahtit dca52d004e feat: implement section-based homepage management system
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>
2026-01-23 14:31:23 +01:00

394 lines
11 KiB
Markdown

# Section-Based Homepage Management System
**Status:** COMPLETE
**Created:** 2026-01-20
**Updated:** 2026-01-23
**Completed:** All 7 phases implemented
## Problem Statement
Current homepage implementation has critical issues:
1. **Hardcoded platform content** - Migrations contain OMS/Loyalty/Main-specific HTML
2. **Monolithic content storage** - Entire page stored as HTML blob, can't edit sections individually
3. **No admin control** - Hero, features, pricing sections are hardcoded in templates
## Solution: JSON-Based Section Architecture
### Approach: Add `sections` JSON field to ContentPage
**Why JSON field vs separate PageSection model:**
- Simpler - no new tables, no joins, no N+1 queries
- Flexible - schema can evolve without migrations
- Atomic - save entire homepage in one transaction
- Follows existing pattern - VendorTheme already uses JSON for `colors`
---
## Multi-Language Support
### Dynamic Language Support
Languages are NOT hardcoded. The system uses the platform's `supported_languages` setting:
```python
# Platform model already has:
supported_languages = Column(JSON) # e.g., ["fr", "de", "en"]
default_language = Column(String) # e.g., "fr"
```
### Schema with Dynamic i18n
```python
class TranslatableText(BaseModel):
"""
Text field with translations stored as dict.
Keys are language codes from platform.supported_languages.
"""
translations: dict[str, str] = {} # {"fr": "...", "de": "...", "en": "..."}
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 ""
class HeroButton(BaseModel):
text: TranslatableText
url: str
style: str = "primary"
class HeroSection(BaseModel):
enabled: bool = True
badge_text: Optional[TranslatableText] = None
title: TranslatableText
subtitle: TranslatableText
background_type: str = "gradient"
buttons: list[HeroButton] = []
```
### Template Usage with Platform Languages
```html
{# Language comes from platform settings #}
{% set lang = request.state.language or platform.default_language %}
{% set default_lang = platform.default_language %}
<h1>{{ hero.title.get(lang, default_lang) }}</h1>
<p>{{ hero.subtitle.get(lang, default_lang) }}</p>
```
### Admin UI Language Tabs
The admin editor dynamically generates language tabs from `platform.supported_languages`:
```javascript
// Fetch platform languages
const platform = await apiClient.get(`/admin/platforms/${platformCode}`);
const languages = platform.supported_languages; // ["fr", "de", "en"]
// Render language tabs dynamically
languages.forEach(lang => {
addLanguageTab(lang);
});
```
---
## Implementation Plan
### Phase 1: Database Changes
**1.1 Add `sections` column to ContentPage**
File: `models/database/content_page.py`
```python
sections = Column(JSON, nullable=True, default=None)
```
**1.2 Create migration**
File: `alembic/versions/xxx_add_sections_to_content_pages.py`
- Add `sections` JSON column (nullable)
### Phase 2: Schema Validation
**2.1 Create Pydantic schemas with dynamic i18n**
File: `models/schema/homepage_sections.py` (NEW)
```python
from pydantic import BaseModel
from typing import Optional
class TranslatableText(BaseModel):
"""
Stores translations as dict with language codes as keys.
Language codes come from platform.supported_languages.
"""
translations: dict[str, str] = {}
def get(self, lang: str, default_lang: str = "fr") -> str:
"""Get text for language with fallback."""
return self.translations.get(lang) or self.translations.get(default_lang) or ""
class HeroButton(BaseModel):
text: TranslatableText
url: str
style: str = "primary" # primary, secondary, outline
class HeroSection(BaseModel):
enabled: bool = True
badge_text: Optional[TranslatableText] = None
title: TranslatableText = TranslatableText()
subtitle: TranslatableText = TranslatableText()
background_type: str = "gradient"
buttons: list[HeroButton] = []
class FeatureCard(BaseModel):
icon: str
title: TranslatableText
description: TranslatableText
class FeaturesSection(BaseModel):
enabled: bool = True
title: TranslatableText = TranslatableText()
subtitle: Optional[TranslatableText] = None
features: list[FeatureCard] = []
layout: str = "grid"
class PricingSection(BaseModel):
enabled: bool = True
title: TranslatableText = TranslatableText()
subtitle: Optional[TranslatableText] = None
use_subscription_tiers: bool = True # Pull from DB dynamically
class CTASection(BaseModel):
enabled: bool = True
title: TranslatableText = TranslatableText()
subtitle: Optional[TranslatableText] = None
buttons: list[HeroButton] = []
class HomepageSections(BaseModel):
hero: Optional[HeroSection] = None
features: Optional[FeaturesSection] = None
pricing: Optional[PricingSection] = None
cta: Optional[CTASection] = None
```
### Phase 3: Template Changes
**3.1 Create section partials**
Directory: `app/templates/platform/sections/` (NEW)
- `_hero.html` - Renders hero with language support
- `_features.html` - Renders features grid
- `_pricing.html` - Renders pricing (uses subscription_tiers from DB)
- `_cta.html` - Renders CTA section
**3.2 Update homepage templates**
File: `app/templates/platform/homepage-default.html`
```html
{% set lang = request.state.language or platform.default_language or 'fr' %}
{% if page and page.sections %}
{{ render_hero(page.sections.hero, lang) }}
{{ render_features(page.sections.features, lang) }}
{{ render_pricing(page.sections.pricing, lang, tiers) }}
{{ render_cta(page.sections.cta, lang) }}
{% else %}
{# Placeholder for unconfigured homepage #}
{% endif %}
```
### Phase 4: Service Layer
**4.1 Add section methods to ContentPageService**
File: `app/services/content_page_service.py`
- `update_homepage_sections(db, page_id, sections, updated_by)` - Validates and saves
- `get_default_sections()` - Returns empty section structure
### Phase 5: Admin API
**5.1 Add section endpoints**
File: `app/api/v1/admin/content_pages.py`
- `GET /{page_id}/sections` - Get structured sections
- `PUT /{page_id}/sections` - Update all sections
- `PUT /{page_id}/sections/{section_name}` - Update single section
### Phase 6: Remove Hardcoded Content from Migrations
**6.1 Update OMS migration**
File: `alembic/versions/z4e5f6a7b8c9_add_multi_platform_support.py`
- Remove `oms_homepage_content` variable
- Create homepage with empty `sections` structure instead
- Set `is_published=False` (admin configures before publishing)
**6.2 Migration creates structure only**
- Migrations should ONLY create empty structure
- Content is entered via admin UI in each language
### Phase 7: Admin UI
**7.1 Add section editor to content-page-edit**
File: `app/templates/admin/content-page-edit.html`
- Add "Sections" tab for homepage pages
- Language tabs within each section (dynamically from platform.supported_languages)
- Form fields for each section type
- Enable/disable toggle per section
File: `static/admin/js/content-page-edit.js`
- Section editor logic
- Language tab switching
- Save sections via API
---
## Critical Files to Modify
1. `models/database/content_page.py` - Add `sections` column
2. `models/schema/homepage_sections.py` - NEW: Pydantic schemas with i18n
3. `app/services/content_page_service.py` - Add section methods
4. `app/api/v1/admin/content_pages.py` - Add section endpoints
5. `app/templates/platform/sections/` - NEW: Section partials
6. `app/templates/platform/homepage-default.html` - Use section partials
7. `app/routes/platform_pages.py` - Pass sections + language to context
8. `alembic/versions/z4e5f6a7b8c9_*.py` - Remove hardcoded content
9. `app/templates/admin/content-page-edit.html` - Section editor UI with language tabs
10. `static/admin/js/content-page-edit.js` - Section editor JS
---
## Section JSON Schema Example (with dynamic i18n)
Languages in `translations` dict come from `platform.supported_languages`.
```json
{
"hero": {
"enabled": true,
"badge_text": {
"translations": {
"fr": "Essai gratuit de 30 jours",
"de": "30 Tage kostenlos testen",
"en": "30-Day Free Trial"
}
},
"title": {
"translations": {
"fr": "Votre titre de plateforme ici",
"de": "Ihr Plattform-Titel hier",
"en": "Your Platform Headline Here"
}
},
"subtitle": {
"translations": {
"fr": "Une description convaincante de votre plateforme.",
"de": "Eine überzeugende Beschreibung Ihrer Plattform.",
"en": "A compelling description of your platform."
}
},
"background_type": "gradient",
"buttons": [
{
"text": {
"translations": {"fr": "Commencer", "de": "Loslegen", "en": "Get Started"}
},
"url": "/signup",
"style": "primary"
}
]
},
"features": {
"enabled": true,
"title": {
"translations": {
"fr": "Pourquoi nous choisir",
"de": "Warum uns wählen",
"en": "Why Choose Us"
}
},
"features": [
{
"icon": "lightning-bolt",
"title": {"translations": {"fr": "Rapide", "de": "Schnell", "en": "Fast"}},
"description": {"translations": {"fr": "Rapide et efficace.", "de": "Schnell und effizient.", "en": "Quick and efficient."}}
}
]
},
"pricing": {
"enabled": true,
"title": {
"translations": {
"fr": "Tarification simple",
"de": "Einfache Preise",
"en": "Simple Pricing"
}
},
"use_subscription_tiers": true
},
"cta": {
"enabled": true,
"title": {
"translations": {
"fr": "Prêt à commencer?",
"de": "Bereit anzufangen?",
"en": "Ready to Start?"
}
},
"buttons": [
{
"text": {
"translations": {"fr": "S'inscrire gratuitement", "de": "Kostenlos registrieren", "en": "Sign Up Free"}
},
"url": "/signup",
"style": "primary"
}
]
}
}
```
---
## Migration Strategy (No Hardcoded Content)
When creating a platform homepage:
```python
homepage = ContentPage(
platform_id=platform_id,
slug="home",
title="Homepage", # Generic
content="", # Empty - sections used instead
sections=get_default_sections(), # Empty structure with all languages
is_published=False, # Admin configures first
)
```
---
## Verification Steps
1. Run migration to add `sections` column
2. Create a test homepage with sections via API (all languages)
3. Verify homepage renders correct language based on request
4. Test admin UI section editor with language tabs
5. Verify pricing section pulls from subscription_tiers
6. Test enable/disable toggle for each section
7. Test language fallback when translation is missing
---
## Notes
- Languages are dynamic from `platform.supported_languages` (not hardcoded)
- Fallback uses `platform.default_language`
- Admin UI should allow partial translations (show warning indicator for missing)
## Pre-Implementation Cleanup
Before implementing, fix the broken OMS migration file:
- `alembic/versions/z4e5f6a7b8c9_add_multi_platform_support.py` has "I dn" on line 181