Browse documentation

Reference

RankFlow AI — Directory Website Architecture

Document: Directory Website Technical Architecture

docs/directory-website-architecture.md
On this page

Document: Directory Website Technical Architecture
Version: 1.0.0
Date: 2026-06-13
Status: Post-Pivot (replaces individual landing page / subdomain model)
Scope: Complete technical specification for the high-DA directory website where each client gets a rich profile page.


1. Executive Summary#

Instead of individual AI-generated landing pages on subdomains (client.rankflow.in), RankFlow AI now operates a unified high-DA directory website on a separate domain. Each client gets a rich, SEO-optimized profile page (directory.com/clinics/kochi/dr-smith-cardiology).

Why this architecture:

  • Compounding authority: One domain with 500+ listings ranks better than 500 subdomains with zero authority
  • Better for clients: They get rankings, not a website they don't want
  • Better for RankFlow: The directory becomes a valuable, monetizable asset
  • Proven model: Yelp, Practo, Justdial, Healthgrades, Zocdoc all built billion-dollar businesses this way

The directory is NEVER publicly framed as "owned by RankFlow." It is a "trusted healthcare directory" that RankFlow "manages and optimizes listings on." The ownership relationship is never disclosed in any public-facing material.


2. Domain Strategy#

Phase 1: Launch (Month 1–2)#

Property Value
Domain New registration (e.g., indiandoctors.in, clinicrank.in, healthfind.in)
Starting DA 0 (fresh domain)
Target DA 10–15 by end of Month 2
Listings 10 beta clients (Kerala)
Strategy Focus on content quality per profile. Build city pages and specialty pages with unique content. Start backlink campaign.

Phase 2: Growth (Month 3–6)#

Property Value
Target DA 20–25
Listings 50–100 clients
Strategy Scale content. Build internal linking network. Press releases. Guest posts. Partnerships with health blogs.

Phase 3: Scale (Month 6–12)#

Property Value
Target DA 30–35
Listings 200–500 clients
Strategy Directory becomes a destination. Add premium listing tiers. Consider advertising. Partner with medical associations.

Phase 4: Acquisition (Month 12+)#

Property Value
Target DA 40+ (via acquisition)
Strategy Acquire an existing high-DA health domain (DA 30+ expired blog, old directory, or health portal). Migrate all listings. Instant authority boost.

Domain Acquisition Criteria:

  • DA 30+ (Moz or Ahrefs)
  • Health/medical relevance (topical authority)
  • Clean backlink profile (no spam, no penalties)
  • Existing traffic (10K+ monthly visitors preferred)
  • Reasonable price (₹50K–5L depending on DA and traffic)
  • .in or .com preferred

3. Technical Stack#

Layer Technology Purpose
Framework Next.js 14 (App Router) SSR + SSG for dynamic routes, SEO-first
Styling Tailwind CSS + shadcn/ui Consistent design system, rapid UI development
Database PostgreSQL (via Supabase or Neon) Profile data, cities, specialties, reviews, analytics
ORM Prisma Type-safe database access, migrations
CMS Sanity CMS (or Strapi) Directory homepage content, blog, city pages, specialty pages
API tRPC + REST Internal API (tRPC) for RankFlow platform; Public API (REST) for external integrations
Search Algolia or Meilisearch Instant search across clinics, cities, specialties
Analytics Plausible or Fathom Privacy-focused analytics (no cookies, GDPR/DPDPA compliant)
Hosting Vercel Edge CDN, fast global delivery, automatic SSL
CDN Cloudflare DNS, DDoS protection, caching, image optimization
Images Cloudflare R2 / AWS S3 + Cloudflare Images Profile photos, clinic images, optimized delivery
Schema JSON-LD generated server-side LocalBusiness, Physician, Service, FAQPage, Review, BreadcrumbList
Sitemap Dynamic XML sitemap All profile pages, city pages, specialty pages, blog posts

Why Next.js App Router over Pages Router:

  • Server Components for SEO-critical content (profile pages render with full schema on the server)
  • Dynamic routes with ISR: /clinics/[city]/[slug] — pre-rendered at build time, revalidated on content updates
  • Streaming SSR for fast Time to First Byte (TTFB)
  • Parallel data fetching for profile pages (fetch profile + reviews + related clinics simultaneously)

Why NOT subdomains + SSG (old model):

  • No wildcard DNS complexity
  • No per-client static builds
  • No Vercel deployment limits
  • One codebase, one domain, one SEO strategy
  • Caching is trivial (Cloudflare CDN + Next.js ISR)

4. URL Structure & Routing#

Directory Pages#

/                                      → Homepage (search, featured, categories)
/search?q=dentist+kochi               → Search results page
/clinics                             → All cities listing
/clinics/[city]                       → City page (all clinics in Kochi)
  e.g., /clinics/kochi
/clinics/[city]/[specialty]           → City + Specialty page
  e.g., /clinics/kochi/dentist
/clinics/[city]/[slug]                → Individual clinic profile (THE CLIENT'S PAGE)
  e.g., /clinics/kochi/dr-smith-dental
/specialty/[specialty]                → Specialty page (all dentists across India)
  e.g., /specialty/dentist
/specialty/[specialty]/[city]         → Specialty + City page
  e.g., /specialty/dentist/kochi
/blog                                → Blog index
/blog/[slug]                         → Individual blog post
/about                               → About the directory
/contact                             → Contact page
/sitemap.xml                         → XML sitemap
/robots.txt                          → Robots rules

Profile Page URL Convention#

/clinics/{city-slug}/{clinic-slug}

Examples:
/clinics/kochi/dr-smith-dental-clinic
/clinics/trivandrum/dr-anjali-nair-cardiology
/clinics/bangalore/smile-dental-care

URL Rules:

  • City slug: lowercase, hyphenated (kochi, trivandrum, bangalore)
  • Clinic slug: generated from business name + city + specialty for uniqueness
    • dr-smith-dental-clinic (not dr-smith — too generic, conflicts likely)
    • If conflict: append number dr-smith-dental-clinic-2
  • Max 60 characters for full URL path
  • No special characters except hyphens
  • Canonical URL always includes full path (no trailing slash variations)

Dynamic Route Implementation (Next.js)#

// app/clinics/[city]/[slug]/page.tsx
export async function generateStaticParams() {
  // Fetch all published profiles from database
  const profiles = await db.profile.findMany({
    where: { status: "PUBLISHED" },
    select: { citySlug: true, slug: true }
  });
  
  return profiles.map(p => ({
    city: p.citySlug,
    slug: p.slug
  }));
}

export const revalidate = 3600; // ISR: revalidate every hour

export default async function ProfilePage({
  params: { city, slug }
}: { params: { city: string; slug: string } }) {
  const profile = await db.profile.findUnique({
    where: { citySlug_slug: { citySlug: city, slug } },
    include: { services: true, reviews: true, faqs: true, relatedClinics: true }
  });
  
  if (!profile) return notFound();
  
  return (
    <>
      <ProfileSchema profile={profile} />
      <ProfileHero profile={profile} />
      <ProfileBio profile={profile} />
      <ProfileServices profile={profile} />
      <ProfileReviews profile={profile} />
      <ProfileFAQ profile={profile} />
      <ProfileMap profile={profile} />
      <ProfileContact profile={profile} />
      <RelatedClinics profile={profile} />
    </>
  );
}

5. Profile Page Content Model#

Each profile page is a rich, SEO-optimized page with 500+ words of unique content. The content is AI-generated during onboarding and refreshed monthly.

Profile Sections#

Section Content Min Words Schema Priority
Hero Clinic name, photo, specialty, city, star rating, CTA (call/WhatsApp) LocalBusiness / Physician Critical
Bio 300–500 word unique bio about the clinic/doctor 300 Critical
Services 5–10 services with descriptions 150 Service High
Reviews 5–10 embedded Google reviews + RankFlow review widget Review High
FAQ 10+ Q&A accordion (AI-generated from specialty + city) 300 FAQPage High
Map Google Maps embed with directions GeoCoordinates Medium
Contact Phone, WhatsApp, email, hours, address ContactPoint Medium
Related "Other dentists in Kochi" — 3–6 internal links Medium (SEO juice)

Profile Data Model (Prisma)#

model DirectoryProfile {
  id              String   @id @default(cuid())
  practiceId      String   @unique  // Links to RankFlow client
  status          ProfileStatus @default(DRAFT)
  
  // URL
  citySlug        String
  slug            String
  citySlug_slug   String   @unique  // Composite unique: "kochi/dr-smith-dental"
  
  // Content
  title           String   // "Dr. Smith Dental Clinic | Best Dentist in Kochi"
  metaDescription String   // 150-160 chars
  bio             String   @db.Text  // 300-500 words
  services        Service[]
  faqs            FAQ[]
  reviews         Review[]
  
  // Media
  photoUrl        String?  // Clinic/doctor photo
  logoUrl         String?  // Clinic logo
  galleryUrls     String[] // 3-5 clinic photos
  
  // Contact
  phone           String
  whatsapp        String?
  email           String?
  address         String
  hours           Json     // {monday: "9:00-18:00", ...}
  
  // Schema
  schemaMarkup    Json     // Full JSON-LD object
  
  // SEO
  keywords        String[] // Target keywords
  canonicalUrl    String   // Full URL
  
  // GBP
  gbpUrl          String?  // Google Business Profile URL
  gbpPlaceId      String?  // Google Place ID
  
  // Analytics
  viewCount       Int      @default(0)
  lastViewedAt    DateTime?
  
  // Timestamps
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
  publishedAt     DateTime?
  
  // Relations
  city            City     @relation(fields: [citySlug], references: [slug])
  relatedProfiles RelatedClinic[]
  
  @@index([citySlug])
  @@index([status])
  @@index([publishedAt])
}

model Service {
  id          String @id @default(cuid())
  profileId   String
  name        String // "Root Canal Treatment"
  description String @db.Text // 50-100 words
  icon        String? // Lucide icon name
  profile     DirectoryProfile @relation(fields: [profileId], references: [id])
}

model FAQ {
  id       String @id @default(cuid())
  profileId String
  question String
  answer   String @db.Text
  profile  DirectoryProfile @relation(fields: [profileId], references: [id])
}

model Review {
  id        String @id @default(cuid())
  profileId String
  author    String
  rating    Int    // 1-5
  text      String @db.Text
  source    String // "google" | "rankflow"
  date      DateTime
  profile   DirectoryProfile @relation(fields: [profileId], references: [id])
}

model RelatedClinic {
  id          String @id @default(cuid())
  profileId   String
  relatedId   String
  profile     DirectoryProfile @relation(fields: [profileId], references: [id])
}

model City {
  slug        String @id
  name        String // "Kochi"
  state       String // "Kerala"
  description String @db.Text // 200-300 words, unique per city
  profileCount Int @default(0)
  profiles    DirectoryProfile[]
}

enum ProfileStatus {
  DRAFT
  PENDING_REVIEW
  PUBLISHED
  PAUSED
  ARCHIVED
}

6. API Specification#

Internal API (tRPC — RankFlow Platform ↔ Directory)#

Used by the RankFlow admin dashboard and onboarding flow to create, update, and manage directory profiles.

// Router: directoryProfile

// Create a new directory profile (called during onboarding)
directoryProfile.create = publicProcedure
  .input(z.object({
    practiceId: z.string().uuid(),
    citySlug: z.string(),
    slug: z.string(),
    content: z.object({
      title: z.string().max(70),
      metaDescription: z.string().max(160),
      bio: z.string().min(300).max(1000),
      services: z.array(z.object({ name: z.string(), description: z.string() })),
      faqs: z.array(z.object({ question: z.string(), answer: z.string() })),
    }),
    media: z.object({
      photoUrl: z.string().url().optional(),
      logoUrl: z.string().url().optional(),
      galleryUrls: z.array(z.string().url()).optional(),
    }),
    contact: z.object({
      phone: z.string(),
      whatsapp: z.string().optional(),
      email: z.string().email().optional(),
      address: z.string(),
      hours: z.record(z.string()),
    }),
    keywords: z.array(z.string()),
  }))
  .mutation(async ({ input }) => {
    // 1. Validate city exists
    // 2. Check slug uniqueness within city
    // 3. Generate JSON-LD schema
    // 4. Create profile record
    // 5. Create services, FAQs
    // 6. Set status to DRAFT
    // 7. Return profile ID and URL
  });

// Publish a profile (make it live)
directoryProfile.publish = publicProcedure
  .input(z.object({ profileId: z.string().uuid() }))
  .mutation(async ({ input }) => {
    // 1. Set status to PUBLISHED
    // 2. Set publishedAt
    // 3. Trigger ISR revalidation for the route
    // 4. Update sitemap
    // 5. Return public URL
  });

// Update profile content (monthly refresh)
directoryProfile.update = publicProcedure
  .input(z.object({
    profileId: z.string().uuid(),
    content: z.object({
      bio: z.string().optional(),
      services: z.array(...).optional(),
      faqs: z.array(...).optional(),
    }).partial(),
  }))
  .mutation(async ({ input }) => {
    // 1. Update content
    // 2. Regenerate schema if needed
    // 3. Trigger ISR revalidation
    // 4. Return updated profile
  });

// Unpublish a profile (client cancellation)
directoryProfile.unpublish = publicProcedure
  .input(z.object({ profileId: z.string().uuid() }))
  .mutation(async ({ input }) => {
    // 1. Set status to ARCHIVED
    // 2. Trigger ISR revalidation (page returns 404)
    // 3. Update sitemap
    // 4. Return confirmation
  });

// Get profile by practice ID (for admin dashboard)
directoryProfile.getByPractice = publicProcedure
  .input(z.object({ practiceId: z.string().uuid() }))
  .query(async ({ input }) => {
    // Return profile with all relations
  });

// List all profiles (for admin)
directoryProfile.list = publicProcedure
  .input(z.object({
    status: z.enum(["DRAFT", "PENDING_REVIEW", "PUBLISHED", "PAUSED", "ARCHIVED"]).optional(),
    citySlug: z.string().optional(),
    specialty: z.string().optional(),
    limit: z.number().default(50),
    offset: z.number().default(0),
  }))
  .query(async ({ input }) => {
    // Return paginated list
  });

// Revalidate ISR for a profile (after content update)
directoryProfile.revalidate = publicProcedure
  .input(z.object({ profileId: z.string().uuid() }))
  .mutation(async ({ input }) => {
    // Call Next.js revalidate API
    // revalidatePath(`/clinics/${citySlug}/${slug}`)
  });

Public API (REST — External Integrations)#

Used by third-party integrations, widgets, and the GBP sync system.

GET /api/v1/profiles/{profileId}
  → Returns public profile data (JSON)
  
GET /api/v1/profiles/{profileId}/schema
  → Returns JSON-LD schema (for GBP sync)
  
GET /api/v1/cities
  → Returns all cities with listing counts
  
GET /api/v1/cities/{citySlug}/profiles
  → Returns all profiles in a city
  
GET /api/v1/specialties
  → Returns all specialties with listing counts
  
GET /api/v1/search?q={query}&city={city}&specialty={specialty}
  → Search profiles (returns Algolia/Meilisearch results)
  
POST /api/v1/webhooks/gbp-sync
  → Webhook for GBP sync system to update profile URL
  
POST /api/v1/webhooks/review-sync
  → Webhook for review sync (Google → RankFlow → profile)

7. Schema & SEO#

JSON-LD Schema (Server-Side Rendered)#

Each profile page includes a comprehensive JSON-LD block in <head>:

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "LocalBusiness",
      "@id": "https://directory.com/clinics/kochi/dr-smith-dental#business",
      "name": "Dr. Smith Dental Clinic",
      "description": "300-500 word bio...",
      "url": "https://directory.com/clinics/kochi/dr-smith-dental",
      "telephone": "+91-98765-43210",
      "email": "contact@drsmithdental.com",
      "address": {
        "@type": "PostalAddress",
        "streetAddress": "123 MG Road",
        "addressLocality": "Kochi",
        "addressRegion": "Kerala",
        "postalCode": "682011",
        "addressCountry": "IN"
      },
      "geo": {
        "@type": "GeoCoordinates",
        "latitude": "9.9312",
        "longitude": "76.2673"
      },
      "openingHoursSpecification": [
        { "@type": "OpeningHoursSpecification", "dayOfWeek": "Monday", "opens": "09:00", "closes": "18:00" },
        { "@type": "OpeningHoursSpecification", "dayOfWeek": "Tuesday", "opens": "09:00", "closes": "18:00" }
      ],
      "priceRange": "₹₹",
      "image": "https://cdn.directory.com/profiles/dr-smith-dental/photo.jpg",
      "aggregateRating": {
        "@type": "AggregateRating",
        "ratingValue": "4.7",
        "reviewCount": "127"
      }
    },
    {
      "@type": "Physician",
      "@id": "https://directory.com/clinics/kochi/dr-smith-dental#physician",
      "name": "Dr. John Smith",
      "medicalSpecialty": "Dentistry",
      "worksFor": { "@id": "https://directory.com/clinics/kochi/dr-smith-dental#business" }
    },
    {
      "@type": "FAQPage",
      "mainEntity": [
        { "@type": "Question", "name": "What are the root canal treatment costs in Kochi?", "acceptedAnswer": { "@type": "Answer", "text": "..." } },
        { "@type": "Question", "name": "Does Dr. Smith Dental Clinic accept dental insurance?", "acceptedAnswer": { "@type": "Answer", "text": "..." } }
      ]
    },
    {
      "@type": "BreadcrumbList",
      "itemListElement": [
        { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://directory.com/" },
        { "@type": "ListItem", "position": 2, "name": "Clinics in Kochi", "item": "https://directory.com/clinics/kochi" },
        { "@type": "ListItem", "position": 3, "name": "Dr. Smith Dental Clinic", "item": "https://directory.com/clinics/kochi/dr-smith-dental" }
      ]
    }
  ]
}

Meta Tags (Per Profile)#

<title>Dr. Smith Dental Clinic | Best Dentist in Kochi | Directory.com</title>
<meta name="description" content="Dr. Smith Dental Clinic in Kochi offers root canal, dental implants, teeth whitening, and braces. 4.7★ rating from 127 patients. Book appointment via WhatsApp.">
<link rel="canonical" href="https://directory.com/clinics/kochi/dr-smith-dental">

<!-- Open Graph -->
<meta property="og:title" content="Dr. Smith Dental Clinic | Best Dentist in Kochi">
<meta property="og:description" content="4.7★ rated dentist in Kochi. Root canal, implants, whitening, braces. Book via WhatsApp.">
<meta property="og:image" content="https://cdn.directory.com/profiles/dr-smith-dental/og-image.jpg">
<meta property="og:url" content="https://directory.com/clinics/kochi/dr-smith-dental">
<meta property="og:type" content="business.business">

<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Dr. Smith Dental Clinic | Best Dentist in Kochi">
<meta name="twitter:description" content="4.7★ rated dentist in Kochi. Root canal, implants, whitening, braces.">
<meta name="twitter:image" content="https://cdn.directory.com/profiles/dr-smith-dental/og-image.jpg">

Sitemap Strategy#

<!-- /sitemap.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap><loc>https://directory.com/sitemap-static.xml</loc></sitemap>
  <sitemap><loc>https://directory.com/sitemap-profiles.xml</loc></sitemap>
  <sitemap><loc>https://directory.com/sitemap-cities.xml</loc></sitemap>
  <sitemap><loc>https://directory.com/sitemap-blog.xml</loc></sitemap>
</sitemapindex>
  • Static sitemap: Homepage, about, contact, blog index (weekly)
  • Profile sitemap: All published profiles (daily — generated dynamically, paginated at 50K URLs per file)
  • City sitemap: All city pages + specialty pages (weekly)
  • Blog sitemap: All blog posts (weekly)

8. Content Generation Pipeline#

AI Content Generation (During Onboarding)#

Step 1: Bio Generation

Input: Business name, specialty, city, services, USPs, years of experience
Output: 300-500 word unique bio
Model: Claude Sonnet
System prompt: "You are a medical content writer. Write a professional, MCI-compliant bio for a healthcare clinic. No promotional claims. No 'best' or 'top' language. Use informational tone. Include specialty, services, city, and patient care philosophy."

Step 2: Service Descriptions

Input: Service name, specialty, city
Output: 50-100 word description per service
Model: Claude Haiku (cheaper, faster)

Step 3: FAQ Generation

Input: Specialty, city, common patient questions
Output: 10 Q&A pairs
Model: Claude Sonnet
System prompt: "Generate 10 FAQ questions patients in [city] ask about [specialty]. Answers must be informational, not promotional. Include costs where relevant."

Step 4: Schema Generation

Input: All profile data
Output: JSON-LD schema
Model: Custom function (no LLM needed — structured data)

Step 5: City Page Content (for new cities)

Input: City name, state, specialty distribution
Output: 200-300 word unique city description
Model: Claude Sonnet
Purpose: SEO value for city pages, internal linking

Monthly Content Refresh#

Trigger: 1st of every month (Inngest cron job)
Steps:
  1. Fetch all PUBLISHED profiles
  2. For each profile:
     a. Rewrite bio (Claude Haiku, same constraints)
     b. Refresh 3-5 FAQs with new questions
     c. Update service descriptions if services changed
     d. Regenerate schema if content changed
     e. Trigger ISR revalidation
  3. Log refresh history
  4. Send report to admin

9. GBP Integration#

Profile URL Sync#

When a client connects their GBP, RankFlow automatically updates:

GBP Field Value Source
websiteUri https://directory.com/clinics/{city}/{slug} Directory profile URL
description First 750 chars of bio Profile bio
primaryPhone Profile phone Profile contact
address Profile address Profile contact
hours Profile hours Profile contact
photos Profile gallery + logo Profile media
services Profile services Profile services

GBP Update Flow#

Client connects GBP (OAuth) → RankFlow fetches GBP data → 
RankFlow compares with profile → Updates changed fields → 
Pushes to GBP API → Confirms success → Logs update

Rate limits: Google GBP API has strict rate limits. Batch updates to 1 per profile per day. Queue updates in Inngest.


10. Admin Dashboard#

/admin/directory — Directory Overview#

  • Total profiles: published / draft / archived
  • Total cities covered
  • Total specialties covered
  • Average profile views (last 30 days)
  • Top performing cities
  • Recent profile updates
  • Content refresh queue

/admin/directory-profiles — Profile Management#

  • Table: Profile | City | Specialty | Status | Views | Last Updated | Actions
  • Filters: Status, City, Specialty, Date range
  • Actions: Edit, Preview, Publish, Unpublish, Refresh Content
  • Bulk actions: Refresh selected, Export data

/admin/directory-profiles/[id] — Profile Detail#

  • Profile preview (live render)
  • Edit content (bio, services, FAQs)
  • Edit media (photos, logo)
  • Edit contact info
  • Schema preview (JSON-LD)
  • GBP sync status
  • Content refresh history
  • Analytics: views, click-to-call, WhatsApp clicks

11. Performance & Caching#

Caching Strategy#

Layer Cache Target TTL Invalidation
Cloudflare CDN Static assets, images, CSS, JS 1 year Manual purge on deploy
Cloudflare CDN HTML pages (profile, city, specialty) 1 hour ISR revalidation
Next.js ISR Profile pages 1 hour On content update
Next.js ISR City pages, specialty pages 24 hours On new profile publish
Redis (Upstash) API responses (profile data, search) 5 minutes On content update
Database Profile queries No cache Real-time

Core Web Vitals Targets#

Metric Target Strategy
LCP < 2.0s Next.js Image optimization, Cloudflare Images, preconnect to Google Fonts
INP < 150ms Minimal client-side JS, no heavy frameworks on profile pages
CLS < 0.05 Explicit image dimensions, no layout shifts on load
TTFB < 400ms Edge deployment (Vercel), Cloudflare caching, database connection pooling
FCP < 1.5s Inline critical CSS, preload hero image, font-display: swap

Image Optimization#

  • All images: Cloudflare Images or Next.js Image component
  • Format: WebP/AVIF with JPEG fallback
  • Sizes: Responsive srcset for profile photos
  • Hero image: 800x600, preloaded
  • Gallery: 400x300, lazy loaded
  • OG image: 1200x630, generated per profile

12. Security & Compliance#

DPDPA 2023 Compliance#

  • Data Residency: All data stored in India (AWS Mumbai or Supabase India region)
  • Consent: Explicit consent captured during onboarding for profile publication
  • Deletion: Full profile deletion on request within 24 hours (unpublish + hard delete after 30 days)
  • Access: Client can request export of all profile data
  • No PII in logs: Phone numbers and emails hashed in analytics logs

MCI Ethics Compliance#

  • No promotional claims: All content is informational, not promotional
  • Doctor approval gate: First profile draft requires doctor approval before publish
  • Review moderation: All review replies queued for doctor approval
  • No "best" or "top" language: AI prompts explicitly forbid promotional claims
  • Disclaimers: Medical disclaimers on all profile pages ("This information is for educational purposes...")

Content Security Policy#

Content-Security-Policy: default-src 'self'; 
  script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; 
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; 
  img-src 'self' https://cdn.directory.com https://maps.googleapis.com data:; 
  frame-src https://www.google.com;

13. Build Timeline (Post-Pivot)#

Week 1–2: Foundation#

  • Register directory domain
  • Set up Next.js 14 project with App Router
  • Set up PostgreSQL database with Prisma
  • Set up Cloudflare DNS + CDN
  • Set up Vercel deployment pipeline
  • Design database schema (Profile, Service, FAQ, Review, City, Specialty)

Week 3–4: Profile System#

  • Build /clinics/[city]/[slug] dynamic route
  • Build profile page components (Hero, Bio, Services, Reviews, FAQ, Map, Contact)
  • Build JSON-LD schema generation
  • Build meta tag generation
  • Build ISR revalidation system
  • Build dynamic XML sitemap

Week 5–6: API & Admin#

  • Build tRPC API (create, update, publish, unpublish profiles)
  • Build REST API (public endpoints for GBP sync, widgets)
  • Build admin dashboard (/admin/directory, /admin/directory-profiles)
  • Build profile preview system
  • Build content approval gate (doctor approval workflow)

Week 7–8: Content & AI#

  • Build AI content generation pipeline (bio, services, FAQs)
  • Build monthly content refresh system (Inngest cron job)
  • Build city page content generation
  • Build specialty page content generation
  • Build content compliance filter (MCI check)

Week 9–10: GBP & Integration#

  • Update GBP sync to link to directory profile URL
  • Build GBP auto-update pipeline (description, services, photos, hours)
  • Build review sync (Google → RankFlow → profile)
  • Build "related clinics" internal linking system
  • Build analytics (profile views, click-to-call, WhatsApp clicks)

Week 11–12: Polish & Launch#

  • Build directory homepage (search, featured, categories)
  • Build search functionality (Algolia/Meilisearch integration)
  • Build blog system (for SEO content + backlink magnet)
  • Performance optimization (Core Web Vitals)
  • Security audit (CSP, headers, DPDPA compliance)
  • Beta launch with 10 Kerala clients

14. Migration Plan (From Old Subdomain Model)#

For existing beta clients on the old subdomain model:

Step Action Timeline
1 Create directory profiles for all existing clients Week 1
2 Update GBP websiteUri from subdomain to directory URL Week 1
3 Update all 30 citations to point to directory URL Week 1–2
4 Set up 301 redirects from old subdomains to directory profiles Week 2
5 Monitor ranking impact (should be neutral or positive) Week 2–4
6 Decommission old subdomain infrastructure Week 4+
7 Notify clients: "Your profile has been upgraded to India's trusted healthcare directory" Week 1

Client communication: Frame this as an UPGRADE, not a change. "Your clinic profile has been moved to India's most trusted healthcare directory — a higher-authority platform that ranks even better on Google."


15. Summary#

This architecture replaces the individual landing page model with a unified directory approach that is:

  • Better SEO: One high-DA domain vs. 100 zero-authority subdomains
  • Better for clients: They get rankings, not a website they don't want
  • Better for RankFlow: The directory becomes a compounding, monetizable asset
  • Simpler tech: No wildcard DNS, no per-client static builds, no custom domains
  • Proven model: Yelp, Practo, Justdial, Healthgrades all built empires this way

The key technical decisions are:

  1. Next.js App Router + dynamic routes (/clinics/[city]/[slug])
  2. ISR for performance (revalidate every hour, update on content change)
  3. Server-side JSON-LD schema (critical for LocalBusiness / Physician ranking)
  4. Separate domain (not rankflow.ai — a dedicated directory domain)
  5. API-driven profile creation (tRPC for internal, REST for external)
  6. AI content generation (bio, services, FAQs) + monthly refresh

Next step: Choose the directory domain name, register it, and begin Week 1 development.