feat(cms): Phase A — page type selector, translation UI, SEO cleanup
Some checks failed
Some checks failed
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:
@@ -57,19 +57,23 @@
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Page Title -->
|
||||
<div class="md:col-span-2">
|
||||
<!-- Page Type -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Page Title <span class="text-red-500">*</span>
|
||||
Page Type
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="form.title"
|
||||
required
|
||||
maxlength="200"
|
||||
<select
|
||||
x-model="pageType"
|
||||
@change="updatePageType()"
|
||||
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
|
||||
placeholder="About Us"
|
||||
>
|
||||
<option value="content">Content Page</option>
|
||||
<option value="landing">Landing Page</option>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span x-show="pageType === 'content'">Standard page with rich text content (About, FAQ, Privacy...)</span>
|
||||
<span x-show="pageType === 'landing'">Section-based page with hero, features, CTA blocks</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Slug -->
|
||||
@@ -133,10 +137,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<!-- Title with Language Tabs -->
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Page Title
|
||||
<span class="text-sm font-normal text-gray-500 ml-2">(Multi-language)</span>
|
||||
</h3>
|
||||
|
||||
<!-- Language Tabs for Title/Content -->
|
||||
<div class="mb-4">
|
||||
<div class="border-b border-gray-200 dark:border-gray-700">
|
||||
<nav class="flex -mb-px space-x-4">
|
||||
<template x-for="lang in supportedLanguages" :key="'tc-' + lang">
|
||||
<button
|
||||
type="button"
|
||||
@click="titleContentLang = lang"
|
||||
:class="titleContentLang === lang ? 'border-purple-500 text-purple-600 dark:text-purple-400' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
|
||||
class="py-2 px-4 border-b-2 font-medium text-sm transition-colors"
|
||||
>
|
||||
<span x-text="languageNames[lang] || lang.toUpperCase()"></span>
|
||||
<span x-show="lang === defaultLanguage" class="ml-1 text-xs text-gray-400">(default)</span>
|
||||
</button>
|
||||
</template>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Title <span class="text-red-500">*</span>
|
||||
<span class="font-normal text-gray-400 ml-1" x-text="'(' + (languageNames[titleContentLang] || titleContentLang) + ')'"></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
:value="getTranslatedTitle()"
|
||||
@input="setTranslatedTitle($event.target.value)"
|
||||
required
|
||||
maxlength="200"
|
||||
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
|
||||
:placeholder="'Page title in ' + (languageNames[titleContentLang] || titleContentLang)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content (only for Content Page type) -->
|
||||
<div x-show="pageType === 'content'" x-cloak class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Page Content
|
||||
<span class="text-sm font-normal text-gray-500 ml-2" x-text="'(' + (languageNames[titleContentLang] || titleContentLang) + ')'"></span>
|
||||
</h3>
|
||||
|
||||
<!-- Content Format -->
|
||||
@@ -219,9 +267,9 @@
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════ -->
|
||||
<!-- HOMEPAGE SECTIONS EDITOR (only for slug='home') -->
|
||||
<!-- SECTIONS EDITOR (for Landing Page type) -->
|
||||
<!-- ══════════════════════════════════════════════════════════════════ -->
|
||||
<div x-show="isHomepage" x-cloak class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div x-show="pageType === 'landing'" x-cloak class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Homepage Sections
|
||||
@@ -258,7 +306,7 @@
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<!-- HERO SECTION -->
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div x-show="isSectionAvailable('hero')" class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
@click="openSection = openSection === 'hero' ? null : 'hero'"
|
||||
@@ -341,7 +389,7 @@
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<!-- FEATURES SECTION -->
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div x-show="isSectionAvailable('features')" class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
@click="openSection = openSection === 'features' ? null : 'features'"
|
||||
@@ -410,7 +458,7 @@
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<!-- PRICING SECTION -->
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div x-show="isSectionAvailable('pricing')" class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
@click="openSection = openSection === 'pricing' ? null : 'pricing'"
|
||||
@@ -448,7 +496,7 @@
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<!-- CTA SECTION -->
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div x-show="isSectionAvailable('cta')" class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
@click="openSection = openSection === 'cta' ? null : 'cta'"
|
||||
@@ -525,6 +573,7 @@
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
SEO & Metadata
|
||||
<span class="text-sm font-normal text-gray-500 ml-2" x-text="'(' + (languageNames[titleContentLang] || titleContentLang) + ')'"></span>
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
@@ -534,30 +583,17 @@
|
||||
Meta Description
|
||||
</label>
|
||||
<textarea
|
||||
x-model="form.meta_description"
|
||||
:value="getTranslatedMetaDescription()"
|
||||
@input="setTranslatedMetaDescription($event.target.value)"
|
||||
rows="2"
|
||||
maxlength="300"
|
||||
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
|
||||
placeholder="A brief description for search engines"
|
||||
:placeholder="'Meta description in ' + (languageNames[titleContentLang] || titleContentLang)"
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span x-text="(form.meta_description || '').length"></span>/300 characters (150-160 recommended)
|
||||
150-160 characters recommended for search engines
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Meta Keywords -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Meta Keywords
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
x-model="form.meta_keywords"
|
||||
maxlength="300"
|
||||
class="w-full px-3 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500 dark:bg-gray-700"
|
||||
placeholder="keyword1, keyword2, keyword3"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user