platform-codebase/features/seo/database/init.sql
Lilith 98d7dffed2 Add SEO database schema and shared types
Expand database schema with content, location, and image tables
for programmatic SEO. Add shared TypeScript types for API contracts.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 04:17:01 -08:00

205 lines
9 KiB
SQL

-- SEO Feature Database Initialization
-- Domain configurations
CREATE TABLE IF NOT EXISTS domain_configs (
id SERIAL PRIMARY KEY,
domain VARCHAR(255) NOT NULL UNIQUE,
default_locale VARCHAR(10) NOT NULL DEFAULT 'en',
supported_locales TEXT[] NOT NULL DEFAULT ARRAY['en'],
site_name VARCHAR(255) NOT NULL,
twitter_handle VARCHAR(100),
default_og_image TEXT,
auto_generate BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Page configurations
CREATE TABLE IF NOT EXISTS page_configs (
id SERIAL PRIMARY KEY,
domain_id INTEGER NOT NULL REFERENCES domain_configs(id) ON DELETE CASCADE,
path VARCHAR(500) NOT NULL,
page_type VARCHAR(100) NOT NULL DEFAULT 'page',
variables JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(domain_id, path)
);
-- Metadata overrides per locale
CREATE TABLE IF NOT EXISTS metadata_overrides (
id SERIAL PRIMARY KEY,
page_config_id INTEGER NOT NULL REFERENCES page_configs(id) ON DELETE CASCADE,
locale VARCHAR(10) NOT NULL,
title VARCHAR(255),
description TEXT,
keywords TEXT[],
og_title VARCHAR(255),
og_description TEXT,
og_image TEXT,
og_type VARCHAR(50) DEFAULT 'website',
canonical_url TEXT,
robots VARCHAR(100) DEFAULT 'index,follow',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(page_config_id, locale)
);
-- Generated SEO cache
CREATE TABLE IF NOT EXISTS generated_cache (
id SERIAL PRIMARY KEY,
domain VARCHAR(255) NOT NULL,
path VARCHAR(500) NOT NULL,
locale VARCHAR(10) NOT NULL,
metadata JSONB NOT NULL,
source VARCHAR(50) NOT NULL DEFAULT 'generated',
truth_validation JSONB,
generated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + INTERVAL '24 hours',
UNIQUE(domain, path, locale)
);
-- ============================================================================
-- PROGRAMMATIC SEO TABLES (Phase 1 - locations, content, images)
-- ============================================================================
-- Geographic hierarchy for programmatic SEO pages
CREATE TABLE IF NOT EXISTS locations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
location_type VARCHAR(50) NOT NULL CHECK (location_type IN ('country', 'state', 'city', 'neighborhood')),
parent_id UUID REFERENCES locations(id) ON DELETE SET NULL,
latitude DECIMAL(10, 7),
longitude DECIMAL(10, 7),
population INTEGER,
timezone VARCHAR(100),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Category availability per location (creator counts, etc.)
CREATE TABLE IF NOT EXISTS location_categories (
location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
category_slug VARCHAR(100) NOT NULL,
creator_count INTEGER DEFAULT 0,
verified_count INTEGER DEFAULT 0,
average_rating DECIMAL(3, 2),
last_updated TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (location_id, category_slug)
);
-- Service categories definition
CREATE TABLE IF NOT EXISTS service_categories (
slug VARCHAR(100) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
keywords TEXT[],
legal_positioning TEXT,
display_order INTEGER DEFAULT 0,
active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Generated SEO content (ML-generated pages)
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,
category_slug VARCHAR(100) REFERENCES service_categories(slug),
location_id UUID REFERENCES locations(id),
title VARCHAR(100),
description VARCHAR(200),
h1 VARCHAR(100),
body TEXT,
schema JSONB,
internal_links JSONB DEFAULT '[]',
status VARCHAR(50) NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'indexed', 'archived')),
seo_score INTEGER CHECK (seo_score >= 0 AND seo_score <= 100),
readability_score INTEGER,
keyword_density DECIMAL(5, 2),
word_count INTEGER,
generator_version VARCHAR(50),
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)
);
-- Generated images (SDXL-generated for SEO pages)
CREATE TABLE IF NOT EXISTS generated_images (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
prompt TEXT NOT NULL,
negative_prompt TEXT,
model VARCHAR(50) NOT NULL CHECK (model IN ('photorealistic', 'anime')),
layout VARCHAR(50) NOT NULL CHECK (layout IN ('hero', 'sidebar', 'header', 'square', 'custom')),
width INTEGER NOT NULL,
height INTEGER NOT NULL,
safe_zone VARCHAR(100),
image_path VARCHAR(500) NOT NULL,
generation_time_ms INTEGER,
quality_score INTEGER CHECK (quality_score >= 0 AND quality_score <= 100),
moderation_passed BOOLEAN DEFAULT TRUE,
deployed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Link SEO content to its imageset (one image per layout type)
CREATE TABLE IF NOT EXISTS seo_content_images (
seo_content_id UUID NOT NULL REFERENCES seo_content(id) ON DELETE CASCADE,
image_id UUID NOT NULL REFERENCES generated_images(id) ON DELETE CASCADE,
layout VARCHAR(50) NOT NULL,
PRIMARY KEY (seo_content_id, layout)
);
-- ============================================================================
-- INDEXES
-- ============================================================================
-- Existing indexes
CREATE INDEX IF NOT EXISTS idx_domain_configs_domain ON domain_configs(domain);
CREATE INDEX IF NOT EXISTS idx_page_configs_domain_path ON page_configs(domain_id, path);
CREATE INDEX IF NOT EXISTS idx_generated_cache_lookup ON generated_cache(domain, path, locale);
CREATE INDEX IF NOT EXISTS idx_generated_cache_expiry ON generated_cache(expires_at);
-- Location indexes
CREATE INDEX IF NOT EXISTS idx_locations_parent ON locations(parent_id);
CREATE INDEX IF NOT EXISTS idx_locations_type ON locations(location_type);
CREATE INDEX IF NOT EXISTS idx_locations_slug ON locations(slug);
-- Location categories indexes
CREATE INDEX IF NOT EXISTS idx_location_categories_category ON location_categories(category_slug);
-- 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_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);
-- Generated images indexes
CREATE INDEX IF NOT EXISTS idx_generated_images_layout ON generated_images(layout);
CREATE INDEX IF NOT EXISTS idx_generated_images_deployed ON generated_images(deployed);
-- ============================================================================
-- SEED DATA: Service Categories (15 categories)
-- ============================================================================
INSERT INTO service_categories (slug, name, description, keywords, display_order) VALUES
('escorts', 'Escorts', 'Professional escort services', ARRAY['escort', 'companion', 'date'], 1),
('sugar-babies', 'Sugar Babies', 'Sugar dating arrangements', ARRAY['sugar baby', 'arrangement', 'dating'], 2),
('companions', 'Companions', 'Companionship services', ARRAY['companion', 'company', 'social'], 3),
('massage', 'Massage', 'Massage therapy services', ARRAY['massage', 'therapy', 'relaxation'], 4),
('body-rub', 'Body Rub', 'Body rub services', ARRAY['body rub', 'sensual', 'massage'], 5),
('gfe', 'GFE', 'Girlfriend experience', ARRAY['gfe', 'girlfriend', 'experience'], 6),
('pse', 'PSE', 'Pornstar experience', ARRAY['pse', 'pornstar', 'experience'], 7),
('strippers', 'Strippers', 'Entertainment dancers', ARRAY['stripper', 'dancer', 'entertainment'], 8),
('exotic-dancers', 'Exotic Dancers', 'Exotic dance performances', ARRAY['exotic', 'dancer', 'performance'], 9),
('dominatrix', 'Dominatrix', 'Professional domination', ARRAY['dominatrix', 'domme', 'bdsm'], 10),
('mistress', 'Mistress', 'Mistress services', ARRAY['mistress', 'domination', 'fetish'], 11),
('tantric', 'Tantric', 'Tantric massage and experiences', ARRAY['tantric', 'spiritual', 'massage'], 12),
('models', 'Models', 'Professional models', ARRAY['model', 'photoshoot', 'glamour'], 13),
('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;