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.mdOn this page
- 1. Landing Page Content Collection During Onboarding
- The Problem
- The Solution: Structured Media & Design Onboarding
- Step 2b: Design & Media Collection (Detailed)
- 2. Permutation-Combination Template Design System
- The Problem with Fixed Templates
- The Solution: Composable Design System
- 2.1 Layout Engines (8 Variants)
- 2.2 Color Palettes (12 Variants)
- 2.3 Typography Pairs (8 Variants)
- 2.4 Section Modules (12 Variants, Toggleable)
- 2.5 How the Composable System Renders
- 3. AI Chat-Based Site Editor
- The Problem
- The Solution: Natural Language Site Editor
- How It Works (Architecture)
- Edit Command Schema
- AI Chat Handler (tRPC)
- Example Chat Flows
- Undo/Redo System
- 4. WordPress Plugin / JS Tag for External Sites
- The Problem
- The Solution: Two Integration Paths
- 4.1 WordPress Plugin
- 4.2 JS Snippet (Universal Tag)
- 4.3 External Site Integration Flow in Onboarding
- 5. Integration with Business Flow Map
- Revised Onboarding Steps (Landing Page Focus)
- Dashboard Integration
- 6. Database Schema Updates
- New Tables
- Updated Practice Table
- 7. Implementation Phases
- Phase 1: Foundation (Weeks 1-2)
- Phase 2: Composable System (Weeks 3-4)
- Phase 3: AI Chat Editor (Weeks 5-6)
- Phase 4: External Site Integration (Weeks 7-8)
- Phase 5: Polish (Weeks 9-10)
⚠️ DEPRECATED — 2026-06-13: This document describes the OLD landing page / subdomain model. Replaced by the Directory Profile System (see
docs/directory-profile-plan.mdanddocs/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
SiteDesigntable - Copy/tone inputs stored in
SiteContenttable - 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