# 35 — Website Template Engine

**Status:** Phase 1.5 (LP Feature Parity) | **Completion:** 0% (New instruction) | **Priority:** High (SaaS revenue driver) | **Dependencies:** Instructions 15 (Next.js), 25 (Valuation), 28 (One Tap SSO), 32 (Blog), 34 (IDX), 22 (Vendor Marketplace) | **Estimated Build Time:** 8 weeks | **Team:** Kean (architecture), Claude (implementation)

---

## Overview

The Website Template Engine is a **Squarespace-for-real-estate builder** that allows agents and brokerages to quickly publish luxury-designed websites without coding. Agents browse a curated library of templates, select one, customize it with their brand identity and content, and deploy a fully functional public-facing website with integrated lead capture, IDX search, blog, valuation tools, and analytics.

This module is the **missing piece of the Fogbreak SaaS platform**. Luxury Presence (the incumbent) runs 50,000+ agent websites—that is their entire business model. Fogbreak currently has no public website builder, which means:
- Agents using Fogbreak still need a separate tool (Squarespace, WordPress, Weebly)
- Lead capture happens outside the system
- No integration between website leads and CRM
- Brokerage-level deployment is harder
- No SaaS revenue per website

This instruction builds that revenue stream. **One website per agent per SaaS subscription** becomes possible.

### What It Replaces
**Luxury Presence** — Agent/brokerage websites, template library, drag-and-drop editor, hosting, SSL, analytics

### What It Adds (Over Luxury Presence)
- **Tight CRM integration** — Form submissions → CRM leads. Website analytics feed into lead scoring
- **AI-powered content** — Blog posts auto-generate from market data. Neighborhood guides auto-write. Testimonials pull from transaction data
- **Unified branding** — One brand config per tenant → applied to ALL websites, emails, documents, ads, social, AI output. No duplicate work
- **Built-in premium features** — Valuation widget, IDX search, Google One Tap lead capture, market reports. No third-party integrations needed
- **Self-hosted AI** — Photo enhancement, virtual staging, video generation available at-cost
- **Real-time team sites** — Agent sites auto-sync when deal closes, photo updates, stats change
- **Marketplace integration** — Vendor directory embeddable on sites
- **Multi-site per agent** — Solo agent site + single-property microsites + team collaboration

### Target Users
1. **Solo agents** (primary) — Build a professional website for $30/month (included with PRO tier)
2. **Team leads** — Publish a team site with searchable agent directory
3. **Brokerages** — Full brokerage website with office locator, agent directory, career page, market reports
4. **Agencies** — White-label builder for their own clients
5. **Teams with single-property focus** — Dedicated microsite per high-value listing

---

## Database Schema

All tables follow Fogbreak patterns: `id` (PK auto-increment), `tenant_id` (NULL = system, multi-tenant safe), `created_at`, `updated_at`. All table creation via auto-migration in api/website_templates.php.

### DDL — Core Tables

```sql
-- Template library (system-level + tenant-specific)
CREATE TABLE IF NOT EXISTS website_templates (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tenant_id INT NULL,
  name VARCHAR(200) NOT NULL,
  slug VARCHAR(200) NOT NULL,
  description TEXT,
  category ENUM('agent','team','brokerage','single_property','landing_page') NOT NULL,
  layout_schema LONGTEXT NOT NULL COMMENT 'JSON: pages[], sections[], components[]',
  default_styles LONGTEXT COMMENT 'JSON: colors, fonts, animations',
  thumbnail_url VARCHAR(500),
  preview_url VARCHAR(500),
  is_active TINYINT DEFAULT 1,
  is_premium TINYINT DEFAULT 0,
  version VARCHAR(20) DEFAULT '1.0',
  author VARCHAR(200),
  downloads INT DEFAULT 0,
  rating_avg DECIMAL(3,2) DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY idx_tenant_active (tenant_id, is_active),
  KEY idx_category (category),
  KEY idx_slug (slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Published websites (agent/team/brokerage sites)
CREATE TABLE IF NOT EXISTS website_sites (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tenant_id INT NOT NULL,
  template_id INT NOT NULL,
  agent_id INT NULL COMMENT 'null = brokerage-level site',
  site_type ENUM('agent','team','brokerage','single_property','landing_page') NOT NULL,
  title VARCHAR(300) NOT NULL,
  domain VARCHAR(300) NULL COMMENT 'custom domain or NULL for subdomain',
  subdomain VARCHAR(100) NULL COMMENT 'unique per tenant for name.fogbreak.io',
  status ENUM('draft','published','paused','archived') DEFAULT 'draft',
  customizations LONGTEXT COMMENT 'JSON: brand overrides, custom styles',
  custom_css TEXT COMMENT 'User CSS overrides',
  custom_head TEXT COMMENT 'Analytics, tracking tags',
  ssl_status ENUM('pending','active','failed') DEFAULT 'pending',
  published_at TIMESTAMP NULL,
  settings LONGTEXT COMMENT 'JSON: enabled features (blog, idx, valuation, forms, etc)',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY idx_tenant (tenant_id),
  KEY idx_agent (agent_id),
  KEY idx_status (status),
  KEY idx_domain (domain),
  KEY idx_subdomain (subdomain),
  UNIQUE KEY uq_subdomain (tenant_id, subdomain)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Pages within sites (home, about, listings, blog, etc)
CREATE TABLE IF NOT EXISTS website_pages (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tenant_id INT NOT NULL,
  site_id INT NOT NULL,
  page_type ENUM('home','about','team','listings','property_detail','neighborhoods','blog','valuation','testimonials','contact','market_reports','recruitment','vendor_directory','custom') NOT NULL,
  title VARCHAR(300) NOT NULL,
  slug VARCHAR(300) NOT NULL,
  content LONGTEXT COMMENT 'JSON: section references and order',
  seo_title VARCHAR(200),
  seo_description VARCHAR(500),
  seo_keywords VARCHAR(500),
  is_published TINYINT DEFAULT 1,
  position INT DEFAULT 0 COMMENT 'nav order',
  parent_id INT NULL COMMENT 'for nested nav',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY idx_site (site_id),
  KEY idx_tenant (tenant_id),
  KEY idx_slug (slug),
  UNIQUE KEY uq_site_slug (site_id, slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Sections (hero, listings, cta, etc — one per component instance)
CREATE TABLE IF NOT EXISTS website_sections (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tenant_id INT NOT NULL,
  page_id INT NOT NULL,
  section_type VARCHAR(100) NOT NULL COMMENT 'hero, featured_listings, testimonials, cta, text_block, gallery, team_grid, form, etc',
  content LONGTEXT COMMENT 'JSON: section-specific data',
  position INT DEFAULT 0,
  is_visible TINYINT DEFAULT 1,
  settings LONGTEXT COMMENT 'JSON: styling (bg, padding, animation)',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY idx_page (page_id),
  KEY idx_tenant (tenant_id),
  KEY idx_type (section_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Media uploads (images, videos, documents)
CREATE TABLE IF NOT EXISTS website_media (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tenant_id INT NOT NULL,
  site_id INT NOT NULL,
  file_path VARCHAR(500) NOT NULL,
  file_type ENUM('image','video','document','icon') NOT NULL,
  alt_text VARCHAR(300),
  caption TEXT,
  original_filename VARCHAR(300),
  width INT,
  height INT,
  file_size INT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY idx_site (site_id),
  KEY idx_tenant (tenant_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Navigation menu structure
CREATE TABLE IF NOT EXISTS website_navigation (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tenant_id INT NOT NULL,
  site_id INT NOT NULL,
  label VARCHAR(100) NOT NULL,
  url VARCHAR(500) NOT NULL COMMENT 'internal page ID or external URL',
  page_id INT NULL,
  position INT DEFAULT 0,
  parent_id INT NULL COMMENT 'for dropdown menus',
  is_external TINYINT DEFAULT 0,
  is_visible TINYINT DEFAULT 1,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY idx_site (site_id),
  KEY idx_tenant (tenant_id),
  KEY idx_parent (parent_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Lead capture forms (contact, valuation, buyer reg, etc)
CREATE TABLE IF NOT EXISTS website_forms (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tenant_id INT NOT NULL,
  site_id INT NOT NULL,
  form_type ENUM('contact','valuation','buyer_registration','seller_consultation','open_house_rsvp','newsletter','custom') NOT NULL,
  title VARCHAR(200) NOT NULL,
  fields LONGTEXT COMMENT 'JSON: field definitions (name, email, phone, custom fields)',
  success_message TEXT COMMENT 'HTML shown after submission',
  notification_email VARCHAR(200),
  redirect_url VARCHAR(500) COMMENT 'post-submit redirect',
  submissions_count INT DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY idx_site (site_id),
  KEY idx_tenant (tenant_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Form submissions (leads from website)
CREATE TABLE IF NOT EXISTS website_form_submissions (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tenant_id INT NOT NULL,
  form_id INT NOT NULL,
  data LONGTEXT COMMENT 'JSON: form field values',
  source_url VARCHAR(500),
  ip_address VARCHAR(45),
  user_agent TEXT,
  imported_to_crm TINYINT DEFAULT 0,
  client_id INT NULL COMMENT 'CRM client if imported',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY idx_tenant (tenant_id),
  KEY idx_form (form_id),
  KEY idx_created (created_at),
  KEY idx_imported (imported_to_crm)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Analytics (daily aggregates)
CREATE TABLE IF NOT EXISTS website_analytics (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tenant_id INT NOT NULL,
  site_id INT NOT NULL,
  date DATE NOT NULL,
  page_views INT DEFAULT 0,
  unique_visitors INT DEFAULT 0,
  sessions INT DEFAULT 0,
  avg_session_duration INT DEFAULT 0 COMMENT 'seconds',
  bounce_rate DECIMAL(5,2) DEFAULT 0 COMMENT 'percentage 0-100',
  leads_captured INT DEFAULT 0,
  top_pages LONGTEXT COMMENT 'JSON: page slug => views',
  traffic_sources LONGTEXT COMMENT 'JSON: source => count',
  device_breakdown LONGTEXT COMMENT 'JSON: mobile/tablet/desktop => count',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY idx_site_date (site_id, date),
  KEY idx_tenant (tenant_id),
  UNIQUE KEY uq_site_date (site_id, date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Custom domains (support multiple per site)
CREATE TABLE IF NOT EXISTS website_domains (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tenant_id INT NOT NULL,
  site_id INT NOT NULL,
  domain VARCHAR(300) NOT NULL,
  is_primary TINYINT DEFAULT 1 COMMENT 'primary domain for site',
  ssl_cert_path VARCHAR(500),
  ssl_expires_at TIMESTAMP NULL,
  dns_verified TINYINT DEFAULT 0,
  dns_verification_token VARCHAR(100),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY idx_site (site_id),
  KEY idx_domain (domain),
  KEY idx_tenant (tenant_id),
  UNIQUE KEY uq_domain (domain)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Testimonials (client success stories)
CREATE TABLE IF NOT EXISTS website_testimonials (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tenant_id INT NOT NULL,
  site_id INT NULL COMMENT 'null = available on all agent sites in tenant',
  client_name VARCHAR(200) NOT NULL,
  client_photo VARCHAR(500),
  content TEXT NOT NULL,
  rating INT DEFAULT 5 COMMENT '1-5',
  transaction_type ENUM('buyer','seller','both') DEFAULT 'both',
  property_address VARCHAR(300),
  transaction_date DATE,
  is_featured TINYINT DEFAULT 0,
  is_published TINYINT DEFAULT 1,
  position INT DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY idx_site (site_id),
  KEY idx_tenant (tenant_id),
  KEY idx_published (is_published)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Team members on team sites
CREATE TABLE IF NOT EXISTS website_team_members (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tenant_id INT NOT NULL,
  site_id INT NOT NULL,
  admin_id INT NULL COMMENT 'linked to admins table if applicable',
  name VARCHAR(200) NOT NULL,
  title VARCHAR(200),
  bio TEXT,
  photo VARCHAR(500),
  email VARCHAR(200),
  phone VARCHAR(20),
  social_links LONGTEXT COMMENT 'JSON: {instagram, facebook, twitter, linkedin}',
  specialties LONGTEXT COMMENT 'JSON: [investment, luxury, etc]',
  designations LONGTEXT COMMENT 'JSON: [ABR, CRS, GRI, etc]',
  is_featured TINYINT DEFAULT 0,
  position INT DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY idx_site (site_id),
  KEY idx_tenant (tenant_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Page view tracking (raw events for analytics)
CREATE TABLE IF NOT EXISTS website_pageviews (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tenant_id INT NOT NULL,
  site_id INT NOT NULL,
  page_id INT NULL,
  session_id VARCHAR(100),
  referrer VARCHAR(500),
  user_agent TEXT,
  ip_address VARCHAR(45),
  device_type ENUM('mobile','tablet','desktop') DEFAULT 'desktop',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  KEY idx_site_date (site_id, created_at),
  KEY idx_tenant (tenant_id),
  KEY idx_session (session_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## API Endpoints (api/website_templates.php)

All endpoints follow Fogbreak pattern: `?action=method` via GET or POST. Auth: session token (humans) or API key (cron). All responses JSON.

### Template Browsing (Public)

**`browse_templates` (GET, NO auth)**
- Returns template gallery with filtering
- Params: `category` (agent|team|brokerage|single_property|landing_page), `search`, `is_premium`, `limit=20`, `offset=0`
- Response: `{success: true, data: [{id, name, slug, category, thumbnail_url, description, is_premium}, ...], total: 47}`

**`preview_template` (GET, NO auth)**
- Full template preview with demo data
- Params: `template_id`
- Response: `{success: true, data: {template_id, name, preview_url, layout_schema: {...}, default_styles: {...}}}`

**`get_template` (GET, auth required)**
- Template details + schema
- Params: `template_id`
- Response: `{success: true, data: {id, name, slug, category, layout_schema: {pages: [...], sections: [...]}, default_styles: {...}}}`

### Site Management (Auth Required)

**`create_site` (POST)**
- Create website from template
- Params: `template_id` (required), `site_type` (required), `title` (required), `agent_id` (optional), `subdomain` (optional)
- Process: (1) Validate template exists, (2) Create website_sites entry, (3) Clone layout_schema pages, (4) Create default pages + sections, (5) Set up default navigation
- Response: `{success: true, data: {site_id: 123, title: "...", subdomain: "kean.fogbreak.io", status: "draft"}}`

**`get_site` (GET)**
- Full site config with pages, sections, navigation
- Params: `site_id`
- Response: `{success: true, data: {id, title, subdomain, domain, status, template_id, pages: [{id, title, slug, sections: [...]}, ...], navigation: [...], analytics: {...}}}`

**`update_site` (POST)**
- Update site settings, customizations, CSS
- Params: `site_id`, plus any of: `title`, `customizations` (JSON brand overrides), `custom_css`, `settings` (JSON feature flags), `custom_head`
- Response: `{success: true, message: "Site updated"}`

**`list_sites` (GET)**
- List all sites for tenant
- Params: `status` (draft|published|paused|archived), `site_type`, `agent_id`, `limit=20`, `offset=0`
- Response: `{success: true, data: [{id, title, site_type, status, published_at, subdomain}, ...], total: 12}`

**`publish_site` (POST)**
- Make site live (validate required pages exist, set status to published, assign subdomain if needed, trigger SSL provisioning)
- Params: `site_id`
- Response: `{success: true, data: {site_id, status: "published", subdomain: "kean.fogbreak.io", ssl_status: "pending"}}`

**`unpublish_site` (POST)**
- Take site offline
- Params: `site_id`
- Response: `{success: true, message: "Site unpublished"}`

**`delete_site` (POST)**
- Archive site (soft delete)
- Params: `site_id`
- Response: `{success: true, message: "Site archived"}`

**`duplicate_site` (POST)**
- Clone existing site with new title
- Params: `site_id`, `new_title`
- Process: (1) Clone website_sites record, (2) Clone all pages, sections, navigation, (3) New subdomain/domain required
- Response: `{success: true, data: {new_site_id, title: "..."}}`

### Page Editor

**`get_page` (GET)**
- Full page with sections
- Params: `page_id`
- Response: `{success: true, data: {id, title, slug, page_type, seo_title, seo_description, sections: [{id, type, content, settings, position}, ...]}}`

**`create_page` (POST)**
- Add page to site
- Params: `site_id`, `page_type` (required), `title` (required), `slug` (auto-generated if omitted)
- Response: `{success: true, data: {page_id, slug, title, sections: []}}`

**`update_page` (POST)**
- Update page metadata/SEO
- Params: `page_id`, plus any of: `title`, `slug`, `seo_title`, `seo_description`, `seo_keywords`, `is_published`, `position`
- Response: `{success: true, message: "Page updated"}`

**`delete_page` (POST)**
- Remove page (soft delete)
- Params: `page_id`
- Response: `{success: true, message: "Page deleted"}`

**`reorder_pages` (POST)**
- Reorder navigation
- Params: `site_id`, `page_ids` (array in order)
- Response: `{success: true, message: "Pages reordered"}`

### Section Editor

**`add_section` (POST)**
- Add section to page
- Params: `page_id`, `section_type` (hero|featured_listings|testimonials|cta|text_block|image_gallery|team_grid|stats_counter|contact_form|video_embed|neighborhood_map|blog_feed|valuation_widget|idx_search|custom_html), `content` (JSON), `position` (optional)
- Response: `{success: true, data: {section_id, type, content, position}}`

**`update_section` (POST)**
- Update section content/styling
- Params: `section_id`, plus any of: `content`, `settings`, `is_visible`
- Response: `{success: true, message: "Section updated"}`

**`remove_section` (POST)**
- Remove section
- Params: `section_id`
- Response: `{success: true, message: "Section deleted"}`

**`reorder_sections` (POST)**
- Reorder sections on page
- Params: `page_id`, `section_ids` (array in order)
- Response: `{success: true, message: "Sections reordered"}`

**`get_section_types` (GET)**
- List available section types with schema for each
- Returns: `{success: true, data: {hero: {fields: [...], example: {...}}, featured_listings: {...}, ...}}`

### Navigation

**`get_navigation` (GET)**
- Get site navigation tree
- Params: `site_id`
- Response: `{success: true, data: [{id, label, url, position, children: [...]}, ...]}`

**`update_navigation` (POST)**
- Set/reorder navigation items
- Params: `site_id`, `items` (array of {label, page_id or url, position, children: [...]})
- Response: `{success: true, message: "Navigation updated"}`

### Media Management

**`upload_media` (POST)**
- Upload image/video/document
- Multipart form data: `site_id`, `file`, `alt_text` (optional), `caption` (optional)
- Process: (1) Validate file type, (2) Auto-resize images for web (hero: 1920x1080, gallery: 800x600), (3) Convert to webp where possible, (4) Store in /media/{tenant}/{site} folder, (5) Return CDN URL
- Response: `{success: true, data: {media_id, file_path, width, height, url: "https://cdn.fogbreak.io/media/..."}}`

**`list_media` (GET)**
- List media for site
- Params: `site_id`, `file_type` (image|video|document|icon), `limit=50`, `offset=0`
- Response: `{success: true, data: [{id, file_path, width, height, alt_text, created_at}, ...], total: 127}`

**`delete_media` (POST)**
- Remove media
- Params: `media_id`
- Response: `{success: true, message: "Media deleted"}`

### Forms & Lead Capture

**`create_form` (POST)**
- Create lead capture form
- Params: `site_id`, `form_type` (required), `title`, `fields` (JSON array of {name, label, type: text|email|phone|textarea|select, required: true/false}), `notification_email`, `success_message`, `redirect_url` (optional)
- Response: `{success: true, data: {form_id, form_type, title, fields}}`

**`get_form` (GET)**
- Get form config
- Params: `form_id`
- Response: `{success: true, data: {id, form_type, title, fields, notification_email, success_message, submissions_count}}`

**`submit_form` (POST, NO auth — public)**
- Public form submission from website visitor
- Params: `form_id`, `data` (JSON of field values), `g-recaptcha-response` (required), `source_url`, `referrer`
- Process: (1) Validate reCAPTCHA, (2) Rate limit IP (10/minute), (3) Store in website_form_submissions, (4) Send notification email to agent, (5) If form_type=contact/buyer_reg/seller_consultation: auto-create CRM client, (6) Log analytics
- Response: `{success: true, message: "Form submitted", data: {submission_id}}`

**`get_submissions` (GET)**
- List form submissions (agent only)
- Params: `form_id`, `date_from`, `date_to`, `limit=50`, `offset=0`
- Response: `{success: true, data: [{submission_id, data: {...}, created_at, imported_to_crm}, ...], total: 342}`

**`import_submission` (POST)**
- Manually import submission into CRM as lead
- Params: `submission_id`
- Process: (1) Extract name/email/phone from submission data, (2) Create client in CRM, (3) Set imported_to_crm=1, (4) Auto-assign to agent
- Response: `{success: true, data: {client_id, name, email}}`

### Testimonials

**`add_testimonial` (POST)**
- Add testimonial
- Params: `tenant_id` (optional, sys admin), `site_id` (optional), `client_name`, `client_photo` (media_id or upload), `content`, `rating` (1-5), `transaction_type` (buyer|seller|both), `property_address`, `transaction_date`
- Response: `{success: true, data: {testimonial_id, client_name, rating}}`

**`list_testimonials` (GET)**
- List testimonials for site or tenant
- Params: `site_id` (optional), `is_published=1`, `limit=20`, `offset=0`
- Response: `{success: true, data: [{id, client_name, client_photo, content, rating, transaction_type}, ...], total: 18}`

**`update_testimonial` (POST)**
- Edit testimonial
- Params: `testimonial_id`, plus any of: `client_name`, `content`, `rating`, `is_published`, `is_featured`, `position`
- Response: `{success: true, message: "Testimonial updated"}`

**`delete_testimonial` (POST)**
- Remove testimonial
- Params: `testimonial_id`
- Response: `{success: true, message: "Testimonial deleted"}`

### Team Members

**`add_team_member` (POST)**
- Add team member to site
- Params: `site_id`, `name`, `title`, `bio`, `photo` (media_id), `email`, `phone`, `social_links` (JSON: {instagram, facebook, twitter, linkedin}), `specialties` (JSON array), `designations` (JSON array)
- Response: `{success: true, data: {member_id, name, title}}`

**`list_team_members` (GET)**
- List team members
- Params: `site_id`, `limit=50`, `offset=0`
- Response: `{success: true, data: [{id, name, title, photo, email, phone, designations}, ...], total: 7}`

**`update_team_member` (POST)**
- Edit team member
- Params: `member_id`, plus any of: `name`, `title`, `bio`, `photo`, `email`, `phone`, `social_links`, `specialties`, `designations`, `is_featured`, `position`
- Response: `{success: true, message: "Team member updated"}`

**`remove_team_member` (POST)**
- Remove team member
- Params: `member_id`
- Response: `{success: true, message: "Team member removed"}`

### Domain Management

**`add_domain` (POST)**
- Add custom domain
- Params: `site_id`, `domain` (e.g. "keanmatthams.com")
- Process: (1) Validate domain not in use, (2) Create DNS verification token, (3) Store in website_domains, (4) Return DNS verification instructions
- Response: `{success: true, data: {domain, dns_verification_token, instructions: "Add CNAME record: _validation.{domain} -> {token}.verify.fogbreak.io"}}`

**`verify_domain` (GET)**
- Check DNS verification (polling from frontend)
- Params: `domain_id`
- Process: (1) Query DNS for verification record, (2) If found: update dns_verified=1, trigger SSL provisioning, return success
- Response: `{success: true/false, verified: true/false, message: "..."}`

**`provision_ssl` (POST, API key — cron)**
- Auto-provision Let's Encrypt SSL (called from cron_ssl_provision)
- Params: `domain_id` (optional — runs all pending if omitted)
- Process: (1) Query website_domains where ssl_status='pending' AND dns_verified=1, (2) For each: call Let's Encrypt API via certbot, (3) Store cert path + expiry, (4) Update ssl_status='active', (5) Log execution
- Response: `{success: true, data: {provisioned: 5, failed: 0, message: "..."}}`

**`remove_domain` (POST)**
- Remove domain
- Params: `domain_id`
- Response: `{success: true, message: "Domain removed"}`

### Analytics & Tracking

**`record_pageview` (POST, NO auth — public tracking pixel)**
- Record page view (called from template footer on every page load)
- Params: `site_id`, `page_id` (optional), `page_slug`, `session_id`, `referrer`, `user_agent` (auto-detected by browser)
- Process: (1) Store in website_pageviews, (2) Update website_analytics aggregate daily (runs via cron), (3) Update lead_scoring if lead came from own site
- Response: `{success: true}` (minimal response, non-blocking)

**`get_site_analytics` (GET)**
- Site analytics dashboard
- Params: `site_id`, `date_from`, `date_to`, `period` (day|week|month)
- Response: `{success: true, data: {site_id, period_data: [{date, page_views, unique_visitors, leads_captured}, ...], summary: {total_pv, total_visitors, total_leads, top_pages: [{slug, views}, ...], traffic_sources: [{source, count}, ...]}, device_breakdown: {mobile: %, tablet: %, desktop: %}}}`

### Public Rendering

**`render_site` (GET, NO auth — core public endpoint)**
- Serve published site page
- Params: `domain` or `subdomain` (route resolver), `page_slug`
- Process:
  1. Resolve domain/subdomain → site_id
  2. Query website_pages where site_id AND slug
  3. If published: render full page
  4. Load template definition + site customizations
  5. Inject tenant brand CSS variables from tenants table
  6. Render each section (calls section-specific render function)
  7. Inject Google One Tap script, forms, tracking pixel
  8. Return full HTML (SSR for SEO)
- Response: HTML page with all meta tags, structured data, analytics tracking

---

## Section Component Library (15+ types)

Each section type has: (1) JSON schema (data structure), (2) render function (HTML output), (3) editor interface (form fields).

### Implemented Section Types

1. **hero** — Full-width hero banner with background image, headline, subheadline, CTA button
2. **featured_listings** — Grid of featured properties with photo, price, beds/baths, MLS link
3. **testimonials** — Carousel of client testimonials with photo, quote, name, rating
4. **cta** — Call-to-action section with headline, description, button(s)
5. **text_block** — Rich text content (heading, body, optional image)
6. **image_gallery** — Image gallery with lightbox
7. **team_grid** — Team member grid with photo, name, title, bio, contact
8. **stats_counter** — KPI counters (homes sold, satisfied clients, years in business)
9. **contact_form** — Embedded lead capture form
10. **video_embed** — YouTube/Vimeo embed with optional transcript
11. **neighborhood_map** — Map of neighborhood with boundaries, listed properties, walkability score
12. **blog_feed** — Latest blog posts with excerpt, read more link
13. **valuation_widget** — Home valuation calculator (instruction 25)
14. **idx_search** — Full IDX property search interface (instruction 34)
15. **custom_html** — Developer-level custom HTML/CSS/JS

---

## Template Designs (12 initial templates)

### Agent Templates (6)
1. **Modern Luxury** — Bold hero, glassmorphism cards, dark background, geometric accents
2. **Classic Elegance** — Traditional serif fonts, cream background, gold accents, symmetrical layout
3. **Minimalist Pro** — Clean white space, sans-serif, blue accents, focus on content
4. **Carmel Local** — Beach photography hero, turquoise accents, community focus
5. **Tech Forward** — Gradient backgrounds, smooth animations, modern sans-serif, AI copy emphasis
6. **Personal Brand** — Large headshot hero, narrative-focused, warm colors, story-driven

### Team Templates (2)
1. **Team Hub** — Searchable agent directory, team statistics, leadership bios, recruitment page
2. **Brokerage Power** — Multi-office locations, agent grid with filters, company newsroom, career page

### Single-Property Templates (2)
1. **Luxury Showcase** — Full-bleed gallery, floorplan, neighborhood deep-dive, exclusive listing
2. **Quick Sale** — Compact listing info, quick contact, comparable sales, urgency timer

### Landing Page Templates (2)
1. **Valuation Capture** — Home valuation hero, form, results page
2. **Buyer Registration** — Multi-step form, preferences, saved searches, alerts setup

---

## Implementation Roadmap (8 phases)

### Phase 1: Template Schema & Core API (Weeks 1-2)
- [ ] Create all 12 database tables + auto-migration
- [ ] Build template browsing API (no auth needed for public library)
- [ ] Build site creation from template (clone layout_schema)
- [ ] Build CRUD for pages and sections
- [ ] Test: Create site from template, verify all default pages exist

### Phase 2: Section Component Library (Weeks 2-3)
- [ ] Build each of 15 section types with render functions
- [ ] For each type: JSON schema, editor form, HTML output
- [ ] Create section type selector UI
- [ ] Test: Add each section type to test page, verify HTML output

### Phase 3: Visual Editor (Weeks 3-4)
- [ ] Drag-and-drop section reordering (Vue/React component)
- [ ] Inline text editing (ContentEditable or Tiptap)
- [ ] Image upload with crop/resize
- [ ] Section settings panel (background, padding, visibility)
- [ ] Live preview with brand colors applied
- [ ] Test: Edit all section types, verify live preview

### Phase 4: Branding & Customization (Week 4-5)
- [ ] CSS variable injection from tenant brand config
- [ ] Custom CSS overrides per site
- [ ] Test: Verify same template renders differently per brand

### Phase 5: Domain & Hosting (Weeks 5-6)
- [ ] Subdomain routing (name.fogbreak.io via wildcard DNS)
- [ ] Custom domain with DNS verification workflow
- [ ] SSL auto-provisioning via Let's Encrypt (cron job)
- [ ] CDN setup for static assets
- [ ] Test: Set up 3 sites, verify all accessible via subdomain and custom domain

### Phase 6: Integration (Weeks 6-7)
- [ ] Connect IDX search component to properties API (instruction 34)
- [ ] Connect blog feed component to blog engine (instruction 32)
- [ ] Connect valuation widget (instruction 25)
- [ ] Connect Google One Tap SSO (instruction 28)
- [ ] Connect form submissions → CRM leads (admin.php)
- [ ] Connect testimonials → transaction data
- [ ] Test: Submit forms, verify leads appear in CRM

### Phase 7: Analytics & Optimization (Week 7-8)
- [ ] Page view tracking (pixel on every page)
- [ ] Lead capture attribution (track form source)
- [ ] Traffic source analysis (referrer parsing)
- [ ] SEO metadata management (OG tags, structured data)
- [ ] Performance optimization (image compression, lazy loading, caching)
- [ ] Test: Visit website, verify analytics appear in dashboard within 5 minutes

### Phase 8: Template Marketplace & Polish (Future)
- [ ] Third-party designer submissions
- [ ] Template rating/review system
- [ ] Premium template purchases
- [ ] Version updates with backward compatibility

---

## Integration Points

**Instruction 15 (Next.js Web App)** — Templates are rendered as Next.js page layouts. The public website and admin dashboard share the same Next.js app but on different route groups: `/pwa/*` for admin (instruction 15), `/websites/*` for public sites (instruction 35).

**Instruction 25 (Home Valuation)** — Valuation widget embeds as a section component. One-click integration.

**Instruction 28 (Google One Tap)** — One Tap widget injected into template header and contact forms. Auto-populates lead capture forms.

**Instruction 32 (Blog)** — Blog feed section pulls latest posts. Individual blog posts rendered within site template for branding consistency.

**Instruction 34 (Enhanced IDX)** — Listings page/property detail pages use IDX search component. Property pages rendered as SSR templates for SEO.

**Instruction 22 (Vendor Marketplace)** — Vendor directory page type embeds marketplace. Public vendor listings visible on brokerage sites.

**CRM (admin.php)** — Form submissions create leads. Site analytics feed into lead scoring. Testimonials pull from transaction data.

**Tenant Config (tenants table)** — Brand identity (colors, fonts, logo, voice) auto-applied to ALL templates via CSS variables. No per-site branding configuration needed beyond this.

---

## Cron Jobs (2 new jobs)

**`website_analytics_aggregate` (daily at 1 AM UTC)**
- Summarize website_pageviews into website_analytics daily records
- Group by site_id, date
- Calculate page_views, unique_visitors (COUNT DISTINCT session_id), bounce_rate, avg_session_duration
- Delete raw pageviews older than 90 days
- Execution: ~30 seconds for 50 sites

**`website_ssl_provision` (every 12 hours)**
- Auto-provision Let's Encrypt SSL for domains with dns_verified=1 AND ssl_status='pending'
- Call Let's Encrypt API via certbot/ACME protocol
- Store certificate path + expiry
- Update ssl_status='active'
- Monitor renewals (renewal at 60 days before expiry)
- Execution: ~2 minutes per 10 domains

---

## Testing Criteria (Definition of Done)

- [ ] Template library loads without authentication (public API works)
- [ ] Site creation from template clones all pages, sections, and navigation correctly
- [ ] Each section type renders valid HTML with proper CSS classes
- [ ] Drag-and-drop section reordering saves and persists
- [ ] Inline text editing updates content in real-time
- [ ] Image upload and crop works for all responsive sizes
- [ ] Brand colors auto-inject via CSS variables (test with 3+ different brand configs)
- [ ] Custom CSS overrides do not break responsive layout
- [ ] Subdomain resolves correctly (name.fogbreak.io)
- [ ] Custom domain DNS verification workflow functions end-to-end
- [ ] SSL provisioning completes within 60 seconds
- [ ] Form submissions create CRM leads with correct data
- [ ] Page view tracking records 100% of visits (sampled at scale)
- [ ] Analytics dashboard updates within 5 minutes of page visit
- [ ] Mobile responsive on all screen sizes (320px to 2560px)
- [ ] SEO metadata renders in page source (<title>, meta tags, schema.org)
- [ ] Performance: First contentful paint under 2 seconds with CDN
- [ ] Multiple sites per tenant work independently (no cross-tenant data leaks)
- [ ] Site publish/unpublish toggles accessibility correctly
- [ ] Testimonials and team members display correctly
- [ ] IDX search component integrates and functions on properties page
- [ ] Blog feed component pulls posts and displays correctly
- [ ] Valuation widget loads and calculates correctly
- [ ] Google One Tap appears and captures leads correctly
- [ ] reCAPTCHA prevents form spam (test with disabled JS)
- [ ] Rate limiting prevents DoS (10 form submissions/minute per IP)

---

## Deployment Checklist

1. **Database:** Run all CREATE TABLE migrations via auto-migration in api/website_templates.php
2. **API Module:** Deploy api/website_templates.php with all 40+ endpoints
3. **Cron Jobs:** Add website_analytics_aggregate and website_ssl_provision to api/cron.php
4. **DNS:** Add wildcard DNS A record: `*.fogbreak.io -> {fogbreak_ip}` + MX records for SSL verification
5. **CDN:** Configure Cloudflare or similar for `/media/*` caching (24-hour TTL)
6. **Let's Encrypt:** Configure certbot on Bluehost for SSL provisioning
7. **Next.js Templates:** Deploy template render functions and section components
8. **Tracking Pixel:** Inject tracking script into all templates via custom_head
9. **SSL Certificates:** Pre-generate wildcard cert for *.fogbreak.io
10. **Admin UI:** Add website builder to instruction 15 (Next.js dashboard)

---

## Revenue Model

- **Basic templates:** Included free with all Fogbreak plans (covered under $30-200/month SaaS fee)
- **Premium templates:** Included in PRO ($200) tier + above; available as add-on purchase for PLUS tier
- **Custom domains:** Free SSL provisioning (Let's Encrypt cost is <$1/year)
- **Future:** Template marketplace with 30/70 revenue share (Fogbreak/designer)
- **Estimate:** $10-20/agent/month in additional revenue at scale (50+ agents per tenant)

---

## Success Metrics

- **10 template library after 3 months**
- **500+ active websites deployed within 6 months**
- **2.5+ average rating on template gallery**
- **<30 second median page load time**
- **10%+ lead conversion rate from website forms**
- **Agents stay in Fogbreak longer due to unified platform (reduced churn)**

---

## What's Next (After 35)

This instruction completes the Fogbreak SaaS platform. No instruction 36. Focus shifts to:
1. **Scale:** Migrate remaining tenants, optimize database, implement read replicas
2. **Polish:** A/B test templates, improve conversion funnel, add more section types
3. **Revenue:** Launch template marketplace, premium tier upsells, custom design services
4. **Retention:** Agent analytics dashboard, lead performance insights, coaching integration
5. **Markets:** Geographic expansion (adapt instruction 01 compliance templates for new markets)

---

**End of Instruction 35**

**Build this. Don't hand it off. You know what to do.**

---

*Last Updated: March 29, 2026 | Instruction Version: 1.0 | Estimated Team Effort: 8 weeks | Status: Ready for implementation*
