feat(cms): Phase A — page type selector, translation UI, SEO cleanup
Some checks failed
CI / ruff (push) Successful in 16s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

Content page editor improvements:
- Page type selector: Content Page / Landing Page dropdown (sets template)
- Title language tabs: translate page titles per language (same pattern as sections)
- Content language tabs: translate page content per language
- Meta description language tabs: translatable SEO descriptions
- Template-driven section palette: template defines which sections are available
  (store landing pages hide Pricing, platform homepages show all)
- Hide content editor when Landing Page selected, hide sections when Content Page

Schema changes (migration cms_003):
- Add meta_description_translations column (JSON) to content_pages
- Drop meta_keywords column (obsolete, ignored by all search engines since 2009)
- Remove meta keywords tag from storefront and platform base templates

API + service updates:
- title_translations, content_translations, meta_description_translations
  added to create/update schemas, route handlers, and service methods

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 22:30:55 +02:00
parent bb3d6f0012
commit b5bb9415f6
11 changed files with 293 additions and 91 deletions

View File

@@ -20,11 +20,13 @@ function contentPageEditor(pageId) {
form: {
slug: '',
title: '',
title_translations: {},
content: '',
content_translations: {},
content_format: 'html',
template: 'default',
meta_description: '',
meta_keywords: '',
meta_description_translations: {},
is_published: false,
show_in_header: false,
show_in_footer: true,
@@ -42,6 +44,12 @@ function contentPageEditor(pageId) {
error: null,
successMessage: null,
// Page type: 'content' or 'landing'
pageType: 'content',
// Translation language for title/content
titleContentLang: 'fr',
// ========================================
// HOMEPAGE SECTIONS STATE
// ========================================
@@ -56,6 +64,13 @@ function contentPageEditor(pageId) {
de: 'Deutsch',
lb: 'Lëtzebuergesch'
},
// Template-driven section palette
sectionPalette: {
'default': ['hero', 'features', 'products', 'pricing', 'testimonials', 'gallery', 'contact_info', 'cta'],
'full': ['hero', 'features', 'testimonials', 'gallery', 'contact_info', 'cta'],
},
sections: {
hero: {
enabled: true,
@@ -108,8 +123,8 @@ function contentPageEditor(pageId) {
await this.loadPage();
contentPageEditLog.groupEnd();
// Load sections if this is a homepage
if (this.form.slug === 'home') {
// Load sections if this is a landing page
if (this.pageType === 'landing') {
await this.loadSections();
}
} else {
@@ -120,14 +135,86 @@ function contentPageEditor(pageId) {
contentPageEditLog.info('=== CONTENT PAGE EDITOR INITIALIZATION COMPLETE ===');
},
// Check if we should show section editor (property, not getter for Alpine compatibility)
// Check if we should show section editor
isHomepage: false,
// Update isHomepage when slug changes
// Is a section available for the current template?
isSectionAvailable(sectionName) {
const palette = this.sectionPalette[this.form.template] || this.sectionPalette['full'];
return palette.includes(sectionName);
},
// Update homepage state
updateIsHomepage() {
this.isHomepage = this.form.slug === 'home';
},
// Update template when page type changes
updatePageType() {
if (this.pageType === 'landing') {
this.form.template = 'full';
// Load sections if editing and not yet loaded
if (this.pageId && !this.sectionsLoaded) {
this.loadSections();
}
} else {
this.form.template = 'default';
}
this.updateIsHomepage();
},
// ========================================
// TITLE/CONTENT TRANSLATION HELPERS
// ========================================
getTranslatedTitle() {
if (this.titleContentLang === this.defaultLanguage) {
return this.form.title;
}
return (this.form.title_translations || {})[this.titleContentLang] || '';
},
setTranslatedTitle(value) {
if (this.titleContentLang === this.defaultLanguage) {
this.form.title = value;
} else {
if (!this.form.title_translations) this.form.title_translations = {};
this.form.title_translations[this.titleContentLang] = value;
}
},
getTranslatedContent() {
if (this.titleContentLang === this.defaultLanguage) {
return this.form.content;
}
return (this.form.content_translations || {})[this.titleContentLang] || '';
},
setTranslatedContent(value) {
if (this.titleContentLang === this.defaultLanguage) {
this.form.content = value;
} else {
if (!this.form.content_translations) this.form.content_translations = {};
this.form.content_translations[this.titleContentLang] = value;
}
},
getTranslatedMetaDescription() {
if (this.titleContentLang === this.defaultLanguage) {
return this.form.meta_description;
}
return (this.form.meta_description_translations || {})[this.titleContentLang] || '';
},
setTranslatedMetaDescription(value) {
if (this.titleContentLang === this.defaultLanguage) {
this.form.meta_description = value;
} else {
if (!this.form.meta_description_translations) this.form.meta_description_translations = {};
this.form.meta_description_translations[this.titleContentLang] = value;
}
},
// Load platforms for dropdown
async loadPlatforms() {
this.loadingPlatforms = true;
@@ -188,11 +275,13 @@ function contentPageEditor(pageId) {
this.form = {
slug: page.slug || '',
title: page.title || '',
title_translations: page.title_translations || {},
content: page.content || '',
content_translations: page.content_translations || {},
content_format: page.content_format || 'html',
template: page.template || 'default',
meta_description: page.meta_description || '',
meta_keywords: page.meta_keywords || '',
meta_description_translations: page.meta_description_translations || {},
is_published: page.is_published || false,
show_in_header: page.show_in_header || false,
show_in_footer: page.show_in_footer !== undefined ? page.show_in_footer : true,
@@ -202,6 +291,9 @@ function contentPageEditor(pageId) {
store_id: page.store_id
};
// Set page type from template
this.pageType = (this.form.template === 'full') ? 'landing' : 'content';
contentPageEditLog.info('Page loaded successfully');
// Update computed properties after loading
@@ -240,24 +332,25 @@ function contentPageEditor(pageId) {
},
// ========================================
// HOMEPAGE SECTIONS METHODS
// SECTIONS METHODS
// ========================================
// Load sections for homepage
// Load sections for landing pages
async loadSections() {
if (!this.pageId || this.form.slug !== 'home') {
contentPageEditLog.debug('Skipping section load - not a homepage');
if (!this.pageId || this.pageType !== 'landing') {
contentPageEditLog.debug('Skipping section load - not a landing page');
return;
}
try {
contentPageEditLog.info('Loading homepage sections...');
contentPageEditLog.info('Loading 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;
this.titleContentLang = this.defaultLanguage;
if (data.sections) {
this.sections = this.mergeWithDefaults(data.sections);
@@ -277,12 +370,18 @@ function contentPageEditor(pageId) {
mergeWithDefaults(loadedSections) {
const defaults = this.getDefaultSectionStructure();
// Deep merge each section
for (const key of ['hero', 'features', 'pricing', 'cta']) {
// Deep merge each section that exists in defaults
for (const key of Object.keys(defaults)) {
if (loadedSections[key]) {
defaults[key] = { ...defaults[key], ...loadedSections[key] };
}
}
// Also preserve any extra sections from loaded data
for (const key of Object.keys(loadedSections)) {
if (!defaults[key]) {
defaults[key] = loadedSections[key];
}
}
return defaults;
},
@@ -375,7 +474,7 @@ function contentPageEditor(pageId) {
// Save sections
async saveSections() {
if (!this.pageId || !this.isHomepage) return;
if (!this.pageId || this.pageType !== 'landing') return;
try {
contentPageEditLog.info('Saving sections...');
@@ -401,11 +500,13 @@ function contentPageEditor(pageId) {
const payload = {
slug: this.form.slug,
title: this.form.title,
title_translations: this.form.title_translations,
content: this.form.content,
content_translations: this.form.content_translations,
content_format: this.form.content_format,
template: this.form.template,
meta_description: this.form.meta_description,
meta_keywords: this.form.meta_keywords,
meta_description_translations: this.form.meta_description_translations,
is_published: this.form.is_published,
show_in_header: this.form.show_in_header,
show_in_footer: this.form.show_in_footer,
@@ -422,8 +523,8 @@ function contentPageEditor(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) {
// Also save sections if this is a landing page
if (this.pageType === 'landing' && this.sectionsLoaded) {
await this.saveSections();
}