Extend SEO database schema and shared types

Add translation and job tracking fields to database schema.
Extend shared types for i18n integration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Lilith 2026-01-01 05:57:38 -08:00
parent c6de604dbb
commit 68b1f74ff8
3 changed files with 66 additions and 2 deletions

View file

@ -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[];
}

View file

@ -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 $$;

View file

@ -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;