diff --git a/features/seo/backend-api/src/database/entities/seo-content.entity.ts b/features/seo/backend-api/src/database/entities/seo-content.entity.ts index 117421ad7..a9214f3a3 100644 --- a/features/seo/backend-api/src/database/entities/seo-content.entity.ts +++ b/features/seo/backend-api/src/database/entities/seo-content.entity.ts @@ -15,7 +15,7 @@ import { ServiceCategoryEntity } from './service-category.entity'; import { SEOContentImageEntity } from './seo-content-image.entity'; @Entity('seo_content') -@Unique(['domain', 'path']) +@Unique(['domain', 'path', 'locale']) export class SEOContentEntity { @PrimaryGeneratedColumn('uuid') id!: string; @@ -26,6 +26,9 @@ export class SEOContentEntity { @Column({ type: 'varchar', length: 500 }) path!: string; + @Column({ type: 'varchar', length: 10, default: 'en' }) + locale!: string; + @Column({ type: 'varchar', length: 100, name: 'category_slug', nullable: true }) categorySlug?: CategorySlug; @@ -68,6 +71,15 @@ export class SEOContentEntity { @Column({ type: 'varchar', length: 50, name: 'generator_version', nullable: true }) generatorVersion?: string; + @Column({ type: 'uuid', name: 'source_content_id', nullable: true }) + sourceContentId?: string; + + @Column({ type: 'varchar', length: 50, name: 'translation_provider', nullable: true }) + translationProvider?: string; + + @Column({ type: 'decimal', precision: 5, scale: 4, name: 'translation_quality_score', nullable: true }) + translationQualityScore?: number; + @Column({ type: 'timestamptz', name: 'generated_at', default: () => 'NOW()' }) generatedAt!: Date; @@ -90,4 +102,11 @@ export class SEOContentEntity { @OneToMany(() => SEOContentImageEntity, (sci) => sci.seoContent) images!: SEOContentImageEntity[]; + + @ManyToOne(() => SEOContentEntity, { nullable: true }) + @JoinColumn({ name: 'source_content_id' }) + sourceContent?: SEOContentEntity; + + @OneToMany(() => SEOContentEntity, (content) => content.sourceContent) + translations?: SEOContentEntity[]; } diff --git a/features/seo/database/init.sql b/features/seo/database/init.sql index 7663b2081..b463d30e4 100644 --- a/features/seo/database/init.sql +++ b/features/seo/database/init.sql @@ -103,10 +103,12 @@ CREATE TABLE IF NOT EXISTS service_categories ( ); -- Generated SEO content (ML-generated pages) +-- Supports multiple locales per path via (domain, path, locale) unique constraint CREATE TABLE IF NOT EXISTS seo_content ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), domain VARCHAR(255) NOT NULL, path VARCHAR(500) NOT NULL, + locale VARCHAR(10) NOT NULL DEFAULT 'en', category_slug VARCHAR(100) REFERENCES service_categories(slug), location_id UUID REFERENCES locations(id), title VARCHAR(100), @@ -121,11 +123,14 @@ CREATE TABLE IF NOT EXISTS seo_content ( keyword_density DECIMAL(5, 2), word_count INTEGER, generator_version VARCHAR(50), + source_content_id UUID REFERENCES seo_content(id), + translation_provider VARCHAR(50), + translation_quality_score DECIMAL(5, 4), generated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), published_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - UNIQUE(domain, path) + UNIQUE(domain, path, locale) ); -- Generated images (SDXL-generated for SEO pages) @@ -174,9 +179,12 @@ CREATE INDEX IF NOT EXISTS idx_location_categories_category ON location_categori -- SEO content indexes CREATE INDEX IF NOT EXISTS idx_seo_content_domain_path ON seo_content(domain, path); +CREATE INDEX IF NOT EXISTS idx_seo_content_domain_path_locale ON seo_content(domain, path, locale); CREATE INDEX IF NOT EXISTS idx_seo_content_status ON seo_content(status); CREATE INDEX IF NOT EXISTS idx_seo_content_category ON seo_content(category_slug); CREATE INDEX IF NOT EXISTS idx_seo_content_location ON seo_content(location_id); +CREATE INDEX IF NOT EXISTS idx_seo_content_locale ON seo_content(locale); +CREATE INDEX IF NOT EXISTS idx_seo_content_source ON seo_content(source_content_id); -- Generated images indexes CREATE INDEX IF NOT EXISTS idx_generated_images_layout ON generated_images(layout); @@ -203,3 +211,33 @@ INSERT INTO service_categories (slug, name, description, keywords, display_order ('travel-companions', 'Travel Companions', 'Travel companionship', ARRAY['travel', 'companion', 'tour'], 14), ('courtesans', 'Courtesans', 'Elite companionship', ARRAY['courtesan', 'elite', 'luxury'], 15) ON CONFLICT (slug) DO NOTHING; + +-- ============================================================================ +-- MIGRATION: Add locale support to seo_content (for existing databases) +-- ============================================================================ +-- Run this if upgrading from a previous version without locale support + +DO $$ +BEGIN + -- Add locale column if it doesn't exist + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'seo_content' AND column_name = 'locale' + ) THEN + ALTER TABLE seo_content ADD COLUMN locale VARCHAR(10) NOT NULL DEFAULT 'en'; + ALTER TABLE seo_content ADD COLUMN source_content_id UUID REFERENCES seo_content(id); + ALTER TABLE seo_content ADD COLUMN translation_provider VARCHAR(50); + ALTER TABLE seo_content ADD COLUMN translation_quality_score DECIMAL(5, 4); + + -- Drop old unique constraint and add new one with locale + ALTER TABLE seo_content DROP CONSTRAINT IF EXISTS seo_content_domain_path_key; + ALTER TABLE seo_content ADD CONSTRAINT seo_content_domain_path_locale_key UNIQUE(domain, path, locale); + + -- Add new indexes + CREATE INDEX IF NOT EXISTS idx_seo_content_domain_path_locale ON seo_content(domain, path, locale); + CREATE INDEX IF NOT EXISTS idx_seo_content_locale ON seo_content(locale); + CREATE INDEX IF NOT EXISTS idx_seo_content_source ON seo_content(source_content_id); + + RAISE NOTICE 'Added locale support to seo_content table'; + END IF; +END $$; diff --git a/features/seo/shared/src/index.ts b/features/seo/shared/src/index.ts index 1decce69b..373c069f7 100644 --- a/features/seo/shared/src/index.ts +++ b/features/seo/shared/src/index.ts @@ -159,6 +159,7 @@ export interface SEOContent { id: string; domain: string; path: string; + locale: string; categorySlug?: CategorySlug; locationId?: string; title?: string; @@ -173,6 +174,12 @@ export interface SEOContent { keywordDensity?: number; wordCount?: number; generatorVersion?: string; + /** For translations: reference to English source content */ + sourceContentId?: string; + /** Translation provider (nllb, tower, etc.) */ + translationProvider?: string; + /** COMET quality score for translation */ + translationQualityScore?: number; generatedAt: string; publishedAt?: string; createdAt: string;