Reference
RankFlow AI — Directory Website Architecture
Document: Directory Website Technical Architecture
docs/directory-website-architecture.mdOn this page
- 1. Executive Summary
- 2. Domain Strategy
- Phase 1: Launch (Month 1–2)
- Phase 2: Growth (Month 3–6)
- Phase 3: Scale (Month 6–12)
- Phase 4: Acquisition (Month 12+)
- 3. Technical Stack
- 4. URL Structure & Routing
- Directory Pages
- Profile Page URL Convention
- Dynamic Route Implementation (Next.js)
- 5. Profile Page Content Model
- Profile Sections
- Profile Data Model (Prisma)
- 6. API Specification
- Internal API (tRPC — RankFlow Platform ↔ Directory)
- Public API (REST — External Integrations)
- 7. Schema & SEO
- JSON-LD Schema (Server-Side Rendered)
- Meta Tags (Per Profile)
- Sitemap Strategy
- 8. Content Generation Pipeline
- AI Content Generation (During Onboarding)
- Monthly Content Refresh
- 9. GBP Integration
- Profile URL Sync
- GBP Update Flow
- 10. Admin Dashboard
- /admin/directory — Directory Overview
- /admin/directory-profiles — Profile Management
- /admin/directory-profiles/[id] — Profile Detail
- 11. Performance & Caching
- Caching Strategy
- Core Web Vitals Targets
- Image Optimization
- 12. Security & Compliance
- DPDPA 2023 Compliance
- MCI Ethics Compliance
- Content Security Policy
- 13. Build Timeline (Post-Pivot)
- Week 1–2: Foundation
- Week 3–4: Profile System
- Week 5–6: API & Admin
- Week 7–8: Content & AI
- Week 9–10: GBP & Integration
- Week 11–12: Polish & Launch
- 14. Migration Plan (From Old Subdomain Model)
- 15. Summary
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)
.inor.compreferred
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(notdr-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:
- Next.js App Router + dynamic routes (
/clinics/[city]/[slug]) - ISR for performance (revalidate every hour, update on content change)
- Server-side JSON-LD schema (critical for LocalBusiness / Physician ranking)
- Separate domain (not rankflow.ai — a dedicated directory domain)
- API-driven profile creation (tRPC for internal, REST for external)
- AI content generation (bio, services, FAQs) + monthly refresh
Next step: Choose the directory domain name, register it, and begin Week 1 development.