Browse documentation

Plans

RankFlow AI — Landing Page System: Complete Architecture Plan

⚠️ DEPRECATED — 2026-06-13: This document describes the OLD landing page / subdomain model. Replaced by the Directory Profile System (see docs/directory-profile-plan.md and docs/d

docs/client-landing-page-plan.md
On this page

⚠️ DEPRECATED — 2026-06-13: This document describes the OLD landing page / subdomain model. Replaced by the Directory Profile System (see docs/directory-profile-plan.md and docs/directory-website-architecture.md). Kept for reference only.nn

Version: 2.0.0
Date: June 2026
Status: Design Document — Ready for Implementation


1. Landing Page Content Collection During Onboarding#

The Problem#

Currently, the onboarding form treats logo/photos as optional fields buried in Step 2. The landing page is then AI-generated without the client's brand assets, resulting in generic, impersonal sites. The client sees their site for the first time AFTER it is built — with no input on layout, colors, or sections.

The Solution: Structured Media & Design Onboarding#

New onboarding flow inserts a dedicated "Design & Media" step between Step 2 (Business Profile) and Step 3 (Consent):

Step 1: Account Creation
    ↓
Step 2: Business Profile Capture (NAP, services, hours)
    ↓
Step 2b: DESIGN & MEDIA COLLECTION ← NEW
    ↓
Step 3: Consent & Terms
    ↓
Step 4: Payment
    ↓
Step 5: Subdomain Assignment
    ↓
Step 6: Landing Page Generation (uses Step 2b data)

Step 2b: Design & Media Collection (Detailed)#

Attribute Value
Actor [C]
Frequency One-time
Time Estimate 5–8 minutes
System Component /onboarding?step=design → Multi-step media wizard → S3 + Prisma SiteDesign

Sub-Step 2b.1: Brand Identity Upload

Field Required Format Max Size Processing Used For
Logo ✅ PNG, SVG, JPG 2MB Auto-resize to 5 variants (favicon 64px, header 200px, OG 1200x630, social 400x400, schema) Header, favicon, schema image, social posts, OG tags
Hero Photo ❌ JPG, PNG 5MB Auto-compress, WebP variant, blur placeholder Hero section background, about section
Doctor/Owner Portrait ❌ JPG, PNG 2MB Auto-crop to 1:1, 3:4, 16:9 About section, team section, GBP photos, social
Clinic/Interior Photos ❌ Up to 5 files, JPG/PNG 2MB each Gallery grid generation, lightbox Gallery section, GBP photos, schema
Team Photos ❌ Up to 3 files 2MB each Grid layout Team section

Upload UX:

┌─────────────────────────────────────────────────────────────┐
│  🎨 Step 2 of 4: Your Brand & Photos                        │
│                                                             │
│  [Logo Upload Zone]                                         │
│  ┌─────────────────────────────┐                           │
│  │    📤 Drag logo here          │  ← Dropzone with preview │
│  │    or click to browse        │                           │
│  │    PNG, SVG, JPG ≤ 2MB       │                           │
│  └─────────────────────────────┘                           │
│                                                             │
│  [Photos Upload Zone]                                        │
│  ┌─────────────────────────────┐                           │
│  │    📤 Drag up to 5 photos    │  ← Multi-file dropzone   │
│  │    Clinic, team, interior    │                           │
│  │    JPG/PNG ≤ 2MB each       │                           │
│  └─────────────────────────────┘                           │
│                                                             │
│  [Skip for now] → AI will use stock imagery                 │
│                                                             │
│              [Continue →]                                   │
└─────────────────────────────────────────────────────────────┘

Image Processing Pipeline (Upload → S3 → Variants):

Client uploads image
    ↓
Client-side: Compress to 80% quality, resize to max 1920px width
    ↓
tRPC mutation: `media.upload` → S3 `assets/{practiceId}/raw/`
    ↓
BullMQ job: `image-process` triggered
    ↓
Sharp (Node.js) generates variants:
    ├── original (preserved)
    ├── webp_1920 (hero size)
    ├── webp_800 (section size)
    ├── webp_400 (thumbnail)
    ├── webp_64 (favicon/icon)
    ├── og_1200x630 (social preview)
    └── blur_20x20 (placeholder base64)
    ↓
All variants uploaded to S3:
    s3://rankflow-assets/practices/{practiceId}/
    ├── logo.png
    ├── logo-favicon.png
    ├── logo-og.png
    ├── hero-photo.webp
    ├── hero-photo-blur.jpg (base64 for LQIP)
    ├── clinic-1.webp
    ├── clinic-2.webp
    └── ...
    ↓
Prisma `MediaAsset` records created with URLs

Sub-Step 2b.2: Design Preferences (Permutation-Selection)

Preference Type Options AI Default Editable Later
Layout Engine Single select See §2.1 Auto-selected from category ✅
Color Mood Single select Warm, Cool, Professional, Premium, Vibrant, Minimal AI from logo color extraction ✅
Typography Vibe Single select Modern, Classic, Friendly, Authoritative, Luxury AI from category ✅
Section Selection Multi-select See §2.2 AI-recommended ✅
Section Order Drag-and-drop Reorder selected sections AI-optimized ✅
Hero Style Single select Full-bleed image, Split layout, Gradient, Video AI from photo availability ✅
CTA Style Single select Pill buttons, Sharp buttons, Outlined, Ghost Auto ✅

UX for Design Preferences:

┌─────────────────────────────────────────────────────────────┐
│  🎨 Step 2b: Design Your Site (Quick)                       │
│                                                             │
│  [🎨] AI detected these colors from your logo:             │
│  ┌────┐┌────┐┌────┐  → Using "Warm Professional" mood    │
│  │ #1 ││ #2 ││ #3 │  [Change →]                           │
│  └────┘└────┘└────┘                                        │
│                                                             │
│  Layout Style:                                              │
│  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐             │
│  │ [img]  │ │ [img]  │ │ [img]  │ │ [img]  │             │
│  │ Clean  │ │ Bento  │ │ Split  │ │ Mag-   │             │
│  │ Modern │ │ Grid   │ │ Hero  │ │ azine  │             │
│  │   ✅   │ │        │ │        │ │        │             │
│  └────────┘ └────────┘ └────────┘ └────────┘             │
│                                                             │
│  Sections (drag to reorder):                                │
│  ☑ Hero    ☑ About    ☑ Services    ☑ Testimonials        │
│  ☑ FAQ     ☑ Gallery  ☑ Contact     ☐ Team                │
│  ☐ Blog    ☐ Stats    ☐ CTA Banner                       │
│                                                             │
│  [✨ Let AI Choose for Me]    [Preview →]                   │
│                                                             │
│              [Continue →]                                   │
└─────────────────────────────────────────────────────────────┘

Sub-Step 2b.3: Copy & Tone Input

Field Required Format AI Fallback Used For
Tagline / Headline ❌ 10–80 chars AI-generated from category + city Hero H1
About Us Story ❌ 100–500 chars AI-generated from onboarding info About section
USP / Differentiator ❌ 50–200 chars AI-generated from services Hero subtitle, CTA
Tone of Voice Single select Professional, Warm, Friendly, Authoritative, Casual AI from category All AI-generated content
Patient/Client Testimonials ❌ Up to 3, 100–300 chars each AI-generated placeholders Testimonials section

Success Criteria for Step 2b:

  • Logo uploaded and processed (all variants generated)
  • Photos uploaded and processed (if provided)
  • Design preferences stored in SiteDesign table
  • Copy/tone inputs stored in SiteContent table
  • AI can generate a complete, personalized landing page

Failure Handling:

Failure Detection Handling
Logo upload fails S3/network error Retry 2x → offer "Continue without logo" → AI generates text-based logo
Image too large Client-side validation + server-side check Compress further → offer image compression tool
Unsupported format File type check Convert to WebP/PNG → alert if conversion fails
Design preference save fails Prisma error Auto-save draft → retry on next interaction

2. Permutation-Combination Template Design System#

The Problem with Fixed Templates#

Current system: 5 fixed templates (medical-modern, dental-clean, clinic-premium, ca-professional, lawyer-authority). Every doctor in Kochi gets the same medical-modern template. This creates:

  • Template fatigue — clients see identical sites
  • No personalization — can't match brand personality
  • Scalability ceiling — new verticals require new templates
  • Cookie-cutter feel — hurts conversion and retention

The Solution: Composable Design System#

Instead of N fixed templates, we have M interchangeable design tokens that combine into M^N unique layouts.

┌─────────────────────────────────────────────────────────────┐
│           COMPOSABLE DESIGN SYSTEM                          │
│                                                             │
│  LAYOUT ENGINE × COLOR PALETTE × TYPOGRAPHY × SECTIONS   │
│                                                             │
│  8 layouts × 12 palettes × 8 type pairs × 12 sections      │
│  = 9,216 unique combinations per vertical                    │
│                                                             │
│  Plus: AI auto-selects based on logo, category, city       │
│  Plus: Client can override any token                       │
│  Plus: Every section is independently styled               │
└─────────────────────────────────────────────────────────────┘

2.1 Layout Engines (8 Variants)#

A layout engine defines the macro structure of the page — how sections relate to each other.

Layout Engine Visual Description Best For Schema
Clean Modern Single-column, generous whitespace, centered content Doctors, clinics, professional services layout: clean-modern
Bento Grid 2-3 column bento-style sections, asymmetric Clinics with multiple services, spas layout: bento-grid
Split Hero 50/50 split hero (image left, text right) Premium practices, individual doctors layout: split-hero
Magazine Editorial feel, large imagery, asymmetric text blocks Aesthetics, wellness, luxury layout: magazine
Card Stack Card-based sections, rounded corners, shadows Dental, pediatric, friendly practices layout: card-stack
Minimalist Maximum whitespace, single focal point, no clutter High-end specialists, consultants layout: minimalist
Bold Impact Large typography, strong colors, high contrast Younger brands, tech-forward layout: bold-impact
Trust Builder Stats-first, testimonials prominent, authority signals CAs, lawyers, established practices layout: trust-builder

Layout Engine Implementation:

// src/lib/design-system/layouts.ts

export interface LayoutEngine {
  id: string;
  name: string;
  description: string;
  // CSS grid/flex patterns
  sectionGap: string;        // e.g., "4rem", "8rem"
  containerMaxWidth: string; // e.g., "1200px", "1400px"
  heroVariant: string;       // "full-bleed" | "split" | "centered"
  sectionLayout: string;     // "stacked" | "bento" | "magazine"
  // Responsive breakpoints
  breakpoints: {
    mobile: string;
    tablet: string;
    desktop: string;
  };
  // Animation presets
  scrollAnimation: string;   // "fade-up" | "stagger" | "reveal"
}

export const layoutEngines: Record<string, LayoutEngine> = {
  "clean-modern": {
    id: "clean-modern",
    name: "Clean Modern",
    sectionGap: "6rem",
    containerMaxWidth: "1200px",
    heroVariant: "centered",
    sectionLayout: "stacked",
    breakpoints: { mobile: "640px", tablet: "768px", desktop: "1024px" },
    scrollAnimation: "fade-up",
  },
  "bento-grid": {
    id: "bento-grid",
    name: "Bento Grid",
    sectionGap: "2rem",
    containerMaxWidth: "1400px",
    heroVariant: "full-bleed",
    sectionLayout: "bento",
    breakpoints: { mobile: "640px", tablet: "768px", desktop: "1024px" },
    scrollAnimation: "stagger",
  },
  // ... 6 more
};

2.2 Color Palettes (12 Variants)#

Color is NOT hardcoded per template. It's a swappable token system.

// src/lib/design-system/palettes.ts

export interface ColorPalette {
  id: string;
  name: string;
  // Brand colors
  primary: string;           // Main brand color (buttons, links, accents)
  primaryHover: string;
  primaryLight: string;      // 10% opacity for backgrounds
  // Semantic colors
  background: string;      // Page background
  surface: string;           // Card/section backgrounds
  surfaceElevated: string;   // Elevated cards (shadow)
  // Text colors
  textPrimary: string;       // Headings
  textSecondary: string;     // Body text
  textMuted: string;         // Captions, metadata
  // Accent colors
  accent: string;            // CTA, highlights
  accentHover: string;
  success: string;           // Positive signals
  warning: string;
  error: string;
  // Borders
  border: string;
  borderLight: string;
}

export const colorPalettes: Record<string, ColorPalette> = {
  "warm-professional": {
    id: "warm-professional",
    name: "Warm Professional",
    primary: "#2563eb",      // Blue
    primaryHover: "#1d4ed8",
    primaryLight: "#eff6ff",
    background: "#fafafa",
    surface: "#ffffff",
    surfaceElevated: "#ffffff",
    textPrimary: "#0f172a",
    textSecondary: "#475569",
    textMuted: "#94a3b8",
    accent: "#f59e0b",       // Amber
    accentHover: "#d97706",
    success: "#22c55e",
    warning: "#f59e0b",
    error: "#ef4444",
    border: "#e2e8f0",
    borderLight: "#f1f5f9",
  },
  "medical-trust": {
    id: "medical-trust",
    name: "Medical Trust",
    primary: "#059669",      // Emerald green (medical)
    primaryHover: "#047857",
    primaryLight: "#ecfdf5",
    background: "#f8fafc",
    surface: "#ffffff",
    surfaceElevated: "#ffffff",
    textPrimary: "#1e293b",
    textSecondary: "#64748b",
    textMuted: "#94a3b8",
    accent: "#0ea5e9",       // Sky blue
    accentHover: "#0284c7",
    success: "#22c55e",
    warning: "#f59e0b",
    error: "#ef4444",
    border: "#e2e8f0",
    borderLight: "#f1f5f9",
  },
  "dental-fresh": {
    id: "dental-fresh",
    name: "Dental Fresh",
    primary: "#06b6d4",      // Cyan
    primaryHover: "#0891b2",
    primaryLight: "#ecfeff",
    background: "#f0fdfa",
    surface: "#ffffff",
    surfaceElevated: "#ffffff",
    textPrimary: "#134e4a",
    textSecondary: "#0f766e",
    textMuted: "#5eead4",
    accent: "#f97316",       // Orange
    accentHover: "#ea580c",
    success: "#22c55e",
    warning: "#f59e0b",
    error: "#ef4444",
    border: "#ccfbf1",
    borderLight: "#f0fdfa",
  },
  // ... 9 more palettes (minimal-gray, premium-gold, vibrant-purple, etc.)
};

AI Auto-Selection Logic:

// Auto-select palette from logo colors
function selectPaletteFromLogo(logoUrl: string): string {
  // 1. Extract dominant colors from logo (using node-vibrant or AI)
  const dominantColors = extractColors(logoUrl);
  
  // 2. Match to closest palette
  const paletteScores = Object.values(colorPalettes).map(palette => ({
    id: palette.id,
    score: colorDistance(dominantColors, [palette.primary, palette.accent]),
  }));
  
  // 3. Return best match, with category override
  const bestMatch = paletteScores.sort((a, b) => a.score - b.score)[0];
  
  // Category overrides (e.g., dental always gets fresh tones)
  if (category === "DENTAL_CLINIC") return "dental-fresh";
  if (category === "CLINIC") return "medical-trust";
  
  return bestMatch.id;
}

2.3 Typography Pairs (8 Variants)#

// src/lib/design-system/typography.ts

export interface TypographyPair {
  id: string;
  name: string;
  headingFont: string;       // Google Font or system font
  headingWeight: number;
  headingStyle: "normal" | "italic";
  bodyFont: string;
  bodyWeight: number;
  bodyLineHeight: number;
  // Scale
  scale: {
    hero: string;      // "clamp(2.5rem, 5vw, 4rem)"
    h1: string;
    h2: string;
    h3: string;
    body: string;
    small: string;
  };
}

export const typographyPairs: Record<string, TypographyPair> = {
  "inter-system": {
    id: "inter-system",
    name: "Modern Clean",
    headingFont: "Inter, system-ui, sans-serif",
    headingWeight: 700,
    headingStyle: "normal",
    bodyFont: "Inter, system-ui, sans-serif",
    bodyWeight: 400,
    bodyLineHeight: 1.6,
    scale: {
      hero: "clamp(2.5rem, 5vw, 4rem)",
      h1: "clamp(2rem, 4vw, 3rem)",
      h2: "clamp(1.5rem, 3vw, 2rem)",
      h3: "clamp(1.25rem, 2vw, 1.5rem)",
      body: "1rem",
      small: "0.875rem",
    },
  },
  "playfair-source": {
    id: "playfair-source",
    name: "Editorial Luxury",
    headingFont: "Playfair Display, Georgia, serif",
    headingWeight: 600,
    headingStyle: "italic",
    bodyFont: "Source Sans Pro, system-ui, sans-serif",
    bodyWeight: 400,
    bodyLineHeight: 1.7,
    scale: { /* ... */ },
  },
  // ... 6 more
};

2.4 Section Modules (12 Variants, Toggleable)#

Each section is a self-contained module that can be:

  • Enabled/disabled per client
  • Reordered via drag-and-drop
  • Styled independently (inherits from design tokens)
  • Populated with AI-generated or client-provided content
Section Module Key Content Source AI-Generated Client-Editable
Hero hero H1, subtitle, CTA, background image ✅ ✅
About about Bio, doctor story, credentials ✅ ✅
Services services Service list with descriptions ✅ ✅
Testimonials testimonials Patient quotes, ratings ✅ (placeholders) ✅
FAQ faq 10+ Q&A pairs with schema ✅ ✅
Contact contact Phone, email, hours, map, form From NAP data ✅
CTA Banner cta Call-to-action block ✅ ✅
Gallery gallery Photo grid/lightbox From client photos ✅
Team team Staff photos and bios ✅ ✅
Stats stats Numbers (patients, years, rating) ✅ ✅
Blog/Articles blog SEO articles ✅ ✅
Reviews Widget reviews Live Google reviews feed From GBP API ❌ (read-only)

Section Module Implementation:

// src/components/site/sections/index.ts

export interface SectionModule {
  key: string;
  name: string;
  defaultEnabled: boolean;
  defaultOrder: number;
  // Content requirements
  requiredFields: string[];
  optionalFields: string[];
  // Render component
  component: React.ComponentType<SectionProps>;
  // Schema types injected
  schemaTypes: string[];
  // AI generation prompt
  aiPrompt: string;
}

export const sectionModules: Record<string, SectionModule> = {
  hero: {
    key: "hero",
    name: "Hero Section",
    defaultEnabled: true,
    defaultOrder: 1,
    requiredFields: ["businessName", "tagline"],
    optionalFields: ["heroImage", "ctaText"],
    component: HeroSection,
    schemaTypes: ["WebSite"],
    aiPrompt: "Generate a compelling hero section for a {category} in {city}...",
  },
  about: {
    key: "about",
    name: "About Section",
    defaultEnabled: true,
    defaultOrder: 2,
    requiredFields: ["doctorName", "experience"],
    optionalFields: ["portraitImage", "credentials"],
    component: AboutSection,
    schemaTypes: ["Person", "Physician"],
    aiPrompt: "Generate an about section for {doctorName}, a {specialty} with {experience} years...",
  },
  // ... 10 more
};

2.5 How the Composable System Renders#

Client selects:
  Layout: "bento-grid"
  Palette: "medical-trust" (from logo extraction)
  Typography: "inter-system"
  Sections: [hero, about, services, testimonials, faq, contact, reviews]
  Order: [hero, about, services, testimonials, faq, contact, reviews]

System generates:
  CSS custom properties (design tokens):
    --layout-section-gap: 2rem
    --layout-container-max: 1400px
    --palette-primary: #059669
    --palette-primary-hover: #047857
    --palette-background: #f8fafc
    --typography-heading: "Inter, system-ui, sans-serif"
    --typography-hero: clamp(2.5rem, 5vw, 4rem)
    
  HTML structure:
    <div class="site-container" style="
      --layout: bento-grid;
      --palette: medical-trust;
      --typography: inter-system;
    ">
      <HeroSection layout="bento-grid" palette="medical-trust" />
      <AboutSection layout="bento-grid" palette="medical-trust" />
      <ServicesSection layout="bento-grid" palette="medical-trust" />
      ...
    </div>

Result: Every client gets a unique, personalized site that matches their brand, not a cookie-cutter template.


3. AI Chat-Based Site Editor#

The Problem#

Clients want to edit their landing page after it's generated, but:

  • They don't know HTML/CSS
  • They don't want to learn a complex CMS
  • They want to say "make the heading bigger" or "change the color to blue"

The Solution: Natural Language Site Editor#

┌─────────────────────────────────────────────────────────────┐
│  🎨 AI Site Editor — Chat with your website                 │
│                                                             │
│  [Live Preview]                    [💬 Chat Panel]          │
│  ┌────────────────────┐          ┌────────────────────┐    │
│  │                    │          │ You:               │    │
│  │  [Site Preview]    │          │ "Change the hero   │    │
│  │                    │          │  heading to        │    │
│  │  Dr. Smith Dental  │          │  'Best Dental Care│    │
│  │  Clinic in Kochi    │          │  in Kochi'"       │    │
│  │                    │          │                    │    │
│  │                    │          │ 🤖 AI: Done! I've │    │
│  │                    │          │  updated the hero  │    │
│  │                    │          │  heading. Here's   │    │
│  │                    │          │  the preview:      │    │
│  │                    │          │  [✅ Applied]      │    │
│  │                    │          │                    │    │
│  │                    │          │ You:               │    │
│  │                    │          │ "Make the CTA      │    │
│  │                    │          │  button green"      │    │
│  │                    │          │                    │    │
│  │                    │          │ 🤖 AI: Changed the  │    │
│  │                    │          │  primary color to  │    │
│  │                    │          │  emerald green.   │    │
│  │                    │          │  [✅ Applied]      │    │
│  └────────────────────┘          └────────────────────┘    │
│                                                             │
│  [💾 Save] [↩️ Undo] [🔄 Reset] [📱 Mobile Preview]        │
└─────────────────────────────────────────────────────────────┘

How It Works (Architecture)#

User types message in chat
    ↓
Natural Language Understanding (Claude Sonnet)
    ↓
Intent Classification:
  ├── "Change text" → Section key + field + new value
  ├── "Change color" → Palette token + new color
  ├── "Add section" → Section key + position
  ├── "Remove section" → Section key
  ├── "Reorder" → Section keys + new order
  ├── "Change layout" → Layout engine ID
  ├── "Change font" → Typography pair ID
  ├── "Upload image" → Section key + image slot
  └── "Generate content" → Section key + AI prompt
    ↓
Structured Edit Command (JSON)
    ↓
Apply to SiteState (React state / Prisma DB)
    ↓
Re-render preview (Next.js ISR revalidation)
    ↓
Show diff to user (before/after comparison)
    ↓
User confirms → Persist to DB

Edit Command Schema#

// src/lib/site-editor/commands.ts

export type EditCommand =
  | TextEditCommand
  | ColorEditCommand
  | SectionAddCommand
  | SectionRemoveCommand
  | SectionReorderCommand
  | LayoutChangeCommand
  | TypographyChangeCommand
  | ImageUploadCommand
  | ContentGenerateCommand;

export interface TextEditCommand {
  type: "TEXT_EDIT";
  sectionKey: string;        // "hero", "about", etc.
  fieldKey: string;          // "heading", "subtitle", "body"
  newValue: string;
  previousValue?: string;    // For undo
}

export interface ColorEditCommand {
  type: "COLOR_EDIT";
  tokenKey: string;          // "primary", "accent", "background"
  newValue: string;          // Hex code
  previousValue?: string;
}

export interface SectionAddCommand {
  type: "SECTION_ADD";
  sectionKey: string;        // "team", "blog", etc.
  position: number;          // Insert index
}

export interface SectionRemoveCommand {
  type: "SECTION_REMOVE";
  sectionKey: string;
}

export interface SectionReorderCommand {
  type: "SECTION_REORDER";
  sectionKeys: string[];     // New order
}

export interface LayoutChangeCommand {
  type: "LAYOUT_CHANGE";
  layoutId: string;          // "bento-grid", "clean-modern", etc.
}

export interface TypographyChangeCommand {
  type: "TYPOGRAPHY_CHANGE";
  typographyId: string;
}

export interface ImageUploadCommand {
  type: "IMAGE_UPLOAD";
  sectionKey: string;
  imageSlot: string;         // "heroImage", "portrait", etc.
  file: File;                // Uploaded file
}

export interface ContentGenerateCommand {
  type: "CONTENT_GENERATE";
  sectionKey: string;
  prompt: string;            // User's prompt for AI
  context: Record<string, any>; // Practice data
}

AI Chat Handler (tRPC)#

// src/server/api/routers/site-editor.ts

export const siteEditorRouter = createTRPCRouter({
  chatEdit: practiceProcedure
    .input(z.object({
      message: z.string().min(1).max(1000),
      practiceId: z.string(),
    }))
    .mutation(async ({ ctx, input }) => {
      // 1. Get current site state
      const siteState = await db.siteDesign.findUnique({
        where: { practiceId: input.practiceId },
      });
      
      // 2. Send to AI for intent classification
      const aiResponse = await ai.generate({
        task: "site_editor_chat",
        system: `You are a website editor AI. Parse the user's request into a structured edit command.
        Available sections: ${siteState.sections.map(s => s.key).join(", ")}
        Current layout: ${siteState.layoutId}
        Current palette: ${siteState.paletteId}
        
        Respond with JSON only: { "command": { ... }, "explanation": "..." }`,
        prompt: input.message,
        jsonMode: true,
      });
      
      // 3. Parse command
      const { command, explanation } = JSON.parse(aiResponse.text);
      
      // 4. Apply command (validate first)
      const validatedCommand = validateCommand(command, siteState);
      
      // 5. Apply to site state
      const newSiteState = applyCommand(siteState, validatedCommand);
      
      // 6. Save to DB
      await db.siteDesign.update({
        where: { practiceId: input.practiceId },
        data: newSiteState,
      });
      
      // 7. Trigger ISR revalidation
      await revalidateSite(input.practiceId);
      
      return {
        command: validatedCommand,
        explanation,
        previewUrl: `https://${siteState.subdomain}.rankflow.ai`,
      };
    }),
});

Example Chat Flows#

Flow 1: Text Edit

User: "Change the heading to 'Best Dental Clinic in Anna Nagar'"
AI: { command: { type: "TEXT_EDIT", sectionKey: "hero", fieldKey: "heading", newValue: "Best Dental Clinic in Anna Nagar" }, explanation: "Updated the hero heading." }
→ Applied. Preview updates immediately.

User: "Actually, make it 'Expert Dental Care in Anna Nagar'"
AI: { command: { type: "TEXT_EDIT", sectionKey: "hero", fieldKey: "heading", newValue: "Expert Dental Care in Anna Nagar" }
→ Applied. Previous value tracked for undo.

Flow 2: Color Change

User: "Make the buttons green instead of blue"
AI: { command: { type: "COLOR_EDIT", tokenKey: "primary", newValue: "#059669" }, explanation: "Changed primary color from blue to emerald green." }
→ Applied. All buttons, links, accents update.

Flow 3: Section Management

User: "Add a team section after about"
AI: { command: { type: "SECTION_ADD", sectionKey: "team", position: 3 }, explanation: "Added Team section after About." }
→ Section generated with AI content. Preview updates.

User: "Remove the blog section"
AI: { command: { type: "SECTION_REMOVE", sectionKey: "blog" }, explanation: "Removed Blog section." }
→ Section removed. Order preserved.

Flow 4: AI Content Generation

User: "Write a better about section that mentions my 15 years of experience"
AI: { command: { type: "CONTENT_GENERATE", sectionKey: "about", prompt: "Write an about section highlighting 15 years of experience...", context: { experience: 15 } } }
→ AI generates new content. User sees diff. Can approve or regenerate.

Undo/Redo System#

// src/lib/site-editor/history.ts

export interface HistoryEntry {
  id: string;
  timestamp: Date;
  command: EditCommand;
  previousState: SiteDesign;  // Snapshot before edit
  newState: SiteDesign;       // Snapshot after edit
  userId: string;
}

export class SiteHistory {
  private stack: HistoryEntry[] = [];
  private pointer: number = -1;
  
  push(entry: HistoryEntry): void {
    // Remove any redo entries
    this.stack = this.stack.slice(0, this.pointer + 1);
    this.stack.push(entry);
    this.pointer++;
  }
  
  undo(): SiteDesign | null {
    if (this.pointer < 0) return null;
    const entry = this.stack[this.pointer];
    this.pointer--;
    return entry.previousState;
  }
  
  redo(): SiteDesign | null {
    if (this.pointer >= this.stack.length - 1) return null;
    this.pointer++;
    const entry = this.stack[this.pointer];
    return entry.newState;
  }
}

4. WordPress Plugin / JS Tag for External Sites#

The Problem#

Not every client will want their site built on RankFlow. Many already have:

  • WordPress sites (50%+ of SMBs)
  • Wix/Squarespace sites
  • Custom-built sites
  • No site at all (just GBP)

We need a fallback mechanism that still delivers SEO value even when the landing page is NOT built on our platform.

The Solution: Two Integration Paths#

┌─────────────────────────────────────────────────────────────┐
│              EXTERNAL SITE INTEGRATION                      │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  PATH A: RankFlow-Hosted Landing Page (default)            │
│  ├── Subdomain: *.rankflow.ai                               │
│  ├── Custom domain: CNAME to RankFlow                        │
│  ├── Full design system, AI editor, schema injection         │
│  └── Full automation (citations, GBP, social, reviews)      │
│                                                              │
│  PATH B: External Site (WordPress/Wix/Custom)               │
│  ├── WordPress Plugin: rankflow-wp.zip                       │
│  ├── JS Snippet: <script src="cdn.rankflow.ai/tag.js">     │
│  ├── Schema injection, NAP sync, review widget               │
│  └── Citation validation, GBP sync, analytics               │
│                                                              │
│  PATH C: No Site (GBP-only)                                  │
│  ├── GBP landing page optimization                           │
│  ├── Citation network (still works)                         │
│  └── Review management, social posting                     │
│                                                              │
└─────────────────────────────────────────────────────────────┘

4.1 WordPress Plugin#

Plugin Name: RankFlow SEO
Plugin Slug: rankflow-seo
Distribution: WordPress.org plugin repository + direct download from RankFlow dashboard
Price: Free (included in all RankFlow plans)

What the Plugin Does:

Feature Description RankFlow API Used
Schema Injection Auto-injects JSON-LD (LocalBusiness, Physician, Service, FAQ) into <head> GET /api/v1/practice/{id}/schema
NAP Sync Syncs business name, address, phone from RankFlow to WordPress GET /api/v1/practice/{id}/nap
Review Widget Embeds live Google reviews widget via iframe GET /api/v1/practice/{id}/reviews
Citation Monitor Shows citation health dashboard in WP admin GET /api/v1/practice/{id}/citations
GBP Insights Displays GBP views, clicks, calls in WP dashboard GET /api/v1/practice/{id}/gbp-insights
Content Sync Auto-publishes GBP posts as WordPress blog posts GET /api/v1/practice/{id}/posts
SEO Audit Runs technical SEO audit on the WP site POST /api/v1/audit
Keyword Tracking Tracks keyword rankings for the WP site GET /api/v1/practice/{id}/rankings

Plugin Architecture:

<?php
// rankflow-seo.php (main plugin file)

/**
 * Plugin Name: RankFlow SEO
 * Description: Connect your WordPress site to RankFlow AI for automated local SEO
 * Version: 1.0.0
 * Author: RankFlow AI
 */

// 1. Admin page: Settings → RankFlow SEO
// 2. API connection: Client enters RankFlow practice ID + API key
// 3. Schema injection: wp_head hook injects JSON-LD
// 4. Shortcodes: [rankflow_reviews], [rankflow_map], [rankflow_contact]
// 5. Gutenberg blocks: RankFlow Review Widget, RankFlow Contact Card
// 6. REST API: Receives webhooks from RankFlow for real-time updates

class RankFlow_SEO {
    private $api_base = 'https://rankflow.ai/api/v1';
    private $practice_id;
    private $api_key;
    
    public function __construct() {
        // Load settings
        $this->practice_id = get_option('rankflow_practice_id');
        $this->api_key = get_option('rankflow_api_key');
        
        // Hooks
        add_action('wp_head', [$this, 'inject_schema']);
        add_action('admin_menu', [$this, 'add_admin_menu']);
        add_action('rest_api_init', [$this, 'register_webhooks']);
    }
    
    // Inject JSON-LD schema into <head>
    public function inject_schema() {
        if (!$this->practice_id) return;
        
        $schema = $this->api_call("/practice/{$this->practice_id}/schema");
        if ($schema) {
            echo '<script type="application/ld+json">' . 
                 wp_json_encode($schema) . 
                 '</script>';
        }
    }
    
    // Fetch data from RankFlow API
    private function api_call($endpoint) {
        $response = wp_remote_get($this->api_base . $endpoint, [
            'headers' => ['Authorization' => 'Bearer ' . $this->api_key],
            'timeout' => 10,
        ]);
        
        if (is_wp_error($response)) return null;
        return json_decode(wp_remote_retrieve_body($response), true);
    }
    
    // Register webhook endpoints for real-time updates
    public function register_webhooks() {
        register_rest_route('rankflow/v1', '/webhook', [
            'methods' => 'POST',
            'callback' => [$this, 'handle_webhook'],
            'permission_callback' => [$this, 'verify_webhook'],
        ]);
    }
    
    // Handle webhooks from RankFlow
    public function handle_webhook($request) {
        $body = $request->get_json_params();
        
        switch ($body['event']) {
            case 'schema.updated':
                // Clear cache, schema will be re-injected on next page load
                delete_transient('rankflow_schema');
                break;
                
            case 'review.new':
                // Update review widget cache
                delete_transient('rankflow_reviews');
                break;
                
            case 'post.published':
                // Auto-create WordPress post from GBP post
                $this->sync_gbp_post_to_wp($body['data']);
                break;
        }
        
        return new WP_REST_Response(['status' => 'ok'], 200);
    }
}

new RankFlow_SEO();

Plugin Settings Page (WordPress Admin):

┌─────────────────────────────────────────────────────────────┐
│  RankFlow SEO → Settings                                    │
│                                                             │
│  Connection Status: ✅ Connected to RankFlow                  │
│  Practice: Dr. Smith Dental Clinic                          │
│  Plan: Premium                                              │
│                                                             │
│  [Schema Injection]  [Review Widget]  [NAP Sync]          │
│                                                             │
│  ☑ Auto-inject JSON-LD schema                              │
│  ☑ Sync NAP data to footer/contact page                    │
│  ☑ Embed review widget on homepage                         │
│  ☑ Auto-publish GBP posts as blog posts                    │
│  ☐ Enable keyword tracking (requires upgrade)              │
│                                                             │
│  [Save Changes]  [Disconnect RankFlow]                      │
└─────────────────────────────────────────────────────────────┘

Gutenberg Blocks:

// Register Gutenberg block: RankFlow Review Widget
register_block_type('rankflow/review-widget', [
    'render_callback' => function($attributes) {
        $practice_id = get_option('rankflow_practice_id');
        $reviews = fetch_rankflow_reviews($practice_id, $attributes['count'] ?? 5);
        
        return render_review_widget($reviews, $attributes);
    },
]);

// Usage in WordPress editor:
// [rankflow_reviews count="5" layout="carousel"]
// or Gutenberg block: RankFlow → Review Widget

4.2 JS Snippet (Universal Tag)#

For non-WordPress sites (Wix, Squarespace, custom HTML, etc.), a lightweight JavaScript tag does the same job.

<!-- Add this to the <head> of any website -->
<script src="https://cdn.rankflow.ai/tag.js" 
        data-practice-id="YOUR_PRACTICE_ID" 
        data-api-key="YOUR_API_KEY"
        async></script>

What the JS tag does:

// cdn.rankflow.ai/tag.js (minified, ~5KB gzipped)

(function() {
  const config = document.currentScript.dataset;
  const practiceId = config.practiceId;
  const apiKey = config.apiKey;
  
  // 1. Fetch schema from RankFlow API
  fetch(`https://rankflow.ai/api/v1/practice/${practiceId}/schema`, {
    headers: { 'Authorization': `Bearer ${apiKey}` }
  })
  .then(r => r.json())
  .then(schema => {
    // Inject JSON-LD into <head>
    const script = document.createElement('script');
    script.type = 'application/ld+json';
    script.textContent = JSON.stringify(schema);
    document.head.appendChild(script);
  });
  
  // 2. Inject review widget (if container exists)
  const reviewContainer = document.getElementById('rankflow-reviews');
  if (reviewContainer) {
    fetch(`https://rankflow.ai/api/v1/practice/${practiceId}/reviews`)
    .then(r => r.json())
    .then(reviews => {
      reviewContainer.innerHTML = renderReviews(reviews);
    });
  }
  
  // 3. NAP consistency check (if NAP elements exist)
  const napElements = document.querySelectorAll('[data-rankflow-nap]');
  napElements.forEach(el => {
    const field = el.dataset.rankflowNap; // "name", "address", "phone"
    // Compare with RankFlow NAP data, highlight mismatches
  });
  
  // 4. Analytics tracking
  // Send page view + engagement data to RankFlow
})();

Usage on any site:

<!-- Wix: Add to Custom Code → Head -->
<script src="https://cdn.rankflow.ai/tag.js" 
        data-practice-id="abc123" 
        data-api-key="rkf_xxx"
        async></script>

<!-- Squarespace: Settings → Advanced → Code Injection → Header -->
<!-- Same script tag -->

<!-- Custom HTML -->
<head>
  <script src="https://cdn.rankflow.ai/tag.js" 
          data-practice-id="abc123" 
          data-api-key="rkf_xxx"
          async></script>
</head>
<body>
  <!-- Review widget container -->
  <div id="rankflow-reviews"></div>
  
  <!-- NAP sync elements -->
  <div data-rankflow-nap="name">Dr. Smith Dental Clinic</div>
  <div data-rankflow-nap="address">123 Main St, Kochi</div>
  <div data-rankflow-nap="phone">+91 98765 43210</div>
</body>

4.3 External Site Integration Flow in Onboarding#

When a client says "I already have a website":

Onboarding Step 2: Business Profile
    ↓
System asks: "Do you have an existing website?"
    ↓
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│  No website     │    │  Yes, WordPress │    │  Yes, other     │
│  (most common)  │    │                 │    │  (Wix, custom)  │
└─────────────────┘    └─────────────────┘    └─────────────────┘
    ↓                        ↓                        ↓
Build on RankFlow      Install plugin +        Provide JS tag
(Steps 5-7)            connect API key          + instructions
    ↓                        ↓                        ↓
Full automation        Schema + reviews        Schema + reviews
+ AI editor            + NAP sync +            + NAP monitoring
                        citation health         + analytics

Onboarding UI for External Site:

┌─────────────────────────────────────────────────────────────┐
│  Do you already have a website?                               │
│                                                             │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────┐ │
│  │   No website    │  │  WordPress      │  │  Other     │ │
│  │   (Recommended) │  │  site           │  │  platform  │ │
│  │                 │  │                 │  │            │ │
│  │  🏗️ We'll build │  │  🔌 Install     │  │  📋 Add    │ │
│  │  your site for  │  │  our plugin     │  │  JS tag   │ │
│  │  you on         │  │                 │  │            │ │
│  │  rankflow.ai    │  │  [Instructions] │  │  [Instructions]│
│  └─────────────────┘  └─────────────────┘  └─────────────┘ │
│                                                             │
│  [💡 Why RankFlow-hosted sites rank better?]               │
└─────────────────────────────────────────────────────────────┘

5. Integration with Business Flow Map#

Revised Onboarding Steps (Landing Page Focus)#

Step 1: Account Creation & Plan Selection [C]
    ↓
Step 2: Business Profile Capture [C]
    ├── NAP data, services, hours, USPs
    └── Logo + Photos upload (NEW — structured)
    ↓
Step 2b: Design & Media Collection [C] ← NEW
    ├── Sub-step 2b.1: Brand Identity Upload (logo, photos)
    ├── Sub-step 2b.2: Design Preferences (layout, palette, typography, sections)
    ├── Sub-step 2b.3: Copy & Tone Input (tagline, about story, USP, tone)
    └── AI auto-suggests based on uploaded assets + category
    ↓
Step 3: Consent & Terms Acceptance [C]
    ↓
Step 4: Payment Method Setup [C] + [S]
    ↓
Step 5: Subdomain Assignment [S]
    ↓
Step 6: Landing Page Generation [S]
    ├── Uses Step 2b design preferences
    ├── Uses Step 2b uploaded photos
    ├── Uses Step 2b copy/tone
    ├── AI generates content for selected sections
    ├── Composable design system applies layout + palette + typography
    └── Schema JSON-LD generated and injected
    ↓
Step 7: Landing Page Deployment [S]
    ├── SSG render with ISR
    ├── Cloudflare CDN cache
    └── Live at subdomain
    ↓
Step 8: AI Chat Editor Preview [C] ← NEW
    ├── Client sees first preview
    ├── AI chat: "Would you like to change anything?"
    ├── Client can edit via chat (or skip)
    └── Final approval
    ↓
Step 9: GBP OAuth Request [S] → [C]
    ↓
... (remaining steps)

Dashboard Integration#

Client Dashboard (/dashboard/site) — New Page:

┌─────────────────────────────────────────────────────────────┐
│  Your Website                    [🌐 Visit Live Site]      │
│                                                             │
│  [Live Preview]          [💬 AI Editor]    [⚙️ Settings]  │
│                                                             │
│  ┌────────────────────┐                                     │
│  │                    │  Layout: Clean Modern              │
│  │  [Site Preview]    │  Colors: Medical Trust           │
│  │                    │  Font: Modern Clean              │
│  │                    │  Sections: 7 active              │
│  │                    │                                     │
│  │                    │  [✨ Redesign with AI]             │
│  │                    │  [🎨 Change Colors]              │
│  │                    │  [📝 Edit Sections]                │
│  └────────────────────┘                                     │
│                                                             │
│  Quick Stats:                                               │
│  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐             │
│  │ 1.2k   │ │ 4.8★   │ │ 15     │ │ 98%    │             │
│  │ views  │ │ rating │ │ cit-   │ │ NAP    │             │
│  │/month  │ │        │ │ ations │ │ match  │             │
│  └────────┘ └────────┘ └────────┘ └────────┘             │
│                                                             │
│  External Site? [🔗 Connect WordPress]  [📋 Get JS Tag]   │
│                                                             │
│  Custom Domain:                                             │
│  Current: dr-smith-dental.rankflow.ai                       │
│  [Add Custom Domain →]                                      │
└─────────────────────────────────────────────────────────────┘

6. Database Schema Updates#

New Tables#

// SiteDesign — stores the composable design tokens
model SiteDesign {
  id              String   @id @default(cuid())
  practiceId      String   @unique
  
  // Design tokens
  layoutId        String   @default("clean-modern")
  paletteId       String   @default("warm-professional")
  typographyId    String   @default("inter-system")
  
  // Sections (ordered, toggleable)
  sections        Json     @default("[{\"key\":\"hero\",\"enabled\":true,\"order\":1},...]")
  
  // Hero style
  heroStyle       String   @default("full-bleed")
  ctaStyle        String   @default("pill")
  
  // Custom overrides (if client changed specific colors)
  paletteOverrides Json    @default("{}")
  
  // Chat editor history
  editHistory     Json     @default("[]")
  
  // Metadata
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
  
  practice        Practice @relation(fields: [practiceId], references: [id], onDelete: Cascade)
  
  @@map("site_designs")
}

// MediaAsset — stores all uploaded images with variants
model MediaAsset {
  id              String   @id @default(cuid())
  practiceId      String
  
  // Original upload
  originalUrl     String
  originalSize    Int      // bytes
  originalFormat  String   // "png", "jpg", "svg"
  
  // Variants (stored as JSON: { "webp_1920": "url", "webp_400": "url", ... })
  variants        Json
  
  // Usage metadata
  purpose         String   // "logo", "hero", "portrait", "clinic", "team"
  altText         String?
  
  // Processing status
  status          String   @default("PROCESSING") // PROCESSING, READY, FAILED
  
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
  
  practice        Practice @relation(fields: [practiceId], references: [id], onDelete: Cascade)
  
  @@index([practiceId, purpose])
  @@map("media_assets")
}

// SiteSection — content for each section module
model SiteSection {
  id              String   @id @default(cuid())
  practiceId      String
  
  sectionKey      String   // "hero", "about", "services", etc.
  
  // Content (flexible JSON per section type)
  content         Json     // { "heading": "...", "subtitle": "...", "body": "..." }
  
  // Media references
  mediaUrls       String[] // S3 URLs for section images
  
  // Visibility and order
  isVisible       Boolean  @default(true)
  sortOrder       Int
  
  // AI generation metadata
  aiGenerated     Boolean  @default(true)
  aiModel         String?
  aiTokensUsed    Int?
  
  // Versioning
  version         Int      @default(1)
  
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
  
  practice        Practice @relation(fields: [practiceId], references: [id], onDelete: Cascade)
  
  @@unique([practiceId, sectionKey])
  @@index([practiceId, sortOrder])
  @@map("site_sections")
}

// ExternalSite — for WordPress/JS tag integrations
model ExternalSite {
  id              String   @id @default(cuid())
  practiceId      String   @unique
  
  // Site type
  platform        String   // "wordpress", "wix", "squarespace", "custom", "none"
  
  // Connection details
  siteUrl         String?  // https://drsmith.com
  apiKey          String?  // For webhook authentication
  
  // Integration status
  pluginInstalled Boolean  @default(false)
  pluginVersion   String?
  jsTagInstalled  Boolean  @default(false)
  lastPingAt      DateTime?
  
  // Features enabled
  schemaInjection Boolean  @default(true)
  reviewWidget    Boolean  @default(true)
  napSync         Boolean  @default(true)
  contentSync     Boolean  @default(false)
  
  // Webhook endpoint (for WordPress REST API)
  webhookUrl      String?
  
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
  
  practice        Practice @relation(fields: [practiceId], references: [id], onDelete: Cascade)
  
  @@map("external_sites")
}

Updated Practice Table#

model Practice {
  // ... existing fields ...
  
  // Site design (new relation)
  siteDesign      SiteDesign?
  
  // External site integration (new relation)
  externalSite    ExternalSite?
  
  // Media assets (new relation)
  mediaAssets     MediaAsset[]
  
  // ... existing relations ...
}

7. Implementation Phases#

Phase 1: Foundation (Weeks 1-2)#

  • Database schema updates (SiteDesign, MediaAsset, SiteSection, ExternalSite)
  • Image upload pipeline (S3 + Sharp variants)
  • Basic design token system (3 layout engines, 6 palettes, 4 typography pairs)
  • 8 section modules (hero, about, services, testimonials, faq, contact, cta, reviews)

Phase 2: Composable System (Weeks 3-4)#

  • Complete design token system (8 layouts, 12 palettes, 8 typography pairs)
  • AI auto-selection from logo colors
  • Section reordering and toggle UI
  • Onboarding Step 2b (Design & Media) flow

Phase 3: AI Chat Editor (Weeks 5-6)#

  • Natural language intent classification (Claude Sonnet)
  • Edit command schema and validation
  • Undo/redo system
  • Real-time preview with ISR revalidation
  • Dashboard integration (/dashboard/site/editor)

Phase 4: External Site Integration (Weeks 7-8)#

  • WordPress plugin (schema injection, review widget, NAP sync)
  • JS tag (universal snippet for any platform)
  • ExternalSite API endpoints
  • Webhook system for real-time updates
  • Onboarding path for external sites

Phase 5: Polish (Weeks 9-10)#

  • Mobile-responsive preview in editor
  • Performance optimization (LCP < 2.5s)
  • A/B testing different layout engines
  • Client feedback loop and iteration

End of Landing Page System Architecture Plan