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