2025-11-13 09:53:08 +05:30
// Express middleware to inject dynamic SEO meta tags
import fs from 'fs' ;
import path from 'path' ;
import axios from 'axios' ;
import { dirname } from 'path' ;
2025-11-19 17:26:31 +05:30
import { fileURLToPath } from 'url' ;
2025-11-13 09:53:08 +05:30
const _ _filename = fileURLToPath ( import . meta . url ) ;
const _ _dirname = dirname ( _ _filename ) ;
const seoMiddleware = async ( req , res , next ) => {
// Skip non-HTML requests and static assets
// Skip files with extensions (except .html) - CSS, JS, images, etc.
const hasExtension = req . path . match ( /\.[a-z]{2,4}$/i ) ;
if ( hasExtension && ! req . path . endsWith ( '.html' ) ) {
return next ( ) ; // Let static middleware serve CSS, JS, images, etc.
}
// Skip if it's an API route or static asset
if ( req . path . startsWith ( '/api/' ) || req . path . startsWith ( '/assets/' ) || req . path . startsWith ( '/home/assets/' ) ) {
return next ( ) ;
}
// CRITICAL: Process ALL routes that could be HTML pages
// This includes: /, /home, /home/blog, /home/pricing, etc.
// Don't call next() - we will send the response ourselves
console . log ( '🔍 SEO Middleware checking path:' , req . path ) ;
console . log ( '🔍 Request method:' , req . method ) ;
console . log ( '🔍 Full URL:' , req . url ) ;
try {
console . log ( '==== SEO Middleware triggered for:' , req . path ) ;
console . log ( '🔍 Raw req.path:' , req . path ) ;
const normalizedPath = normalizePath ( mapToHomeNamespace ( req . path ) ) ;
console . log ( 'Normalized path:' , normalizedPath ) ;
console . log ( '🔍 After normalization:' , normalizedPath ) ;
// Attempt dynamic SEO fetch by path/slug first, fallback to pageId map
const routeToPageId = {
'/' : 1 ,
2025-11-19 17:26:31 +05:30
'/signin' : 2 ,
'/signin/' : 2 ,
2025-11-13 09:53:08 +05:30
'/pricing' : 3 ,
'/pricing/' : 3 ,
'/contact-us' : 4 ,
'/contact-us/' : 4 ,
2025-11-19 17:26:31 +05:30
'/blog' : 5 ,
'/blog/' : 5 ,
'/about-us' : 6 ,
'/about-us/' : 6 ,
'/privacy-policy' : 7 ,
'/privacy-policy/' : 7 ,
'/cookie-policy' : 8 ,
'/cookie-policy/' : 8 ,
'/solutions' : 9 ,
'/solutions/' : 9 ,
'/case-studies' : 10 ,
'/case-studies/' : 10 ,
'/live-session' : 11 ,
'/live-session/' : 11 ,
'/book-demo' : 12 ,
'/book-demo/' : 12 ,
'/schedule-demo' : 13 ,
'/schedule-demo/' : 13 ,
'/solutions/retail-billing' : 14 ,
'/solutions/retail-billing/' : 14 ,
'/solutions/inventory-purchase' : 15 ,
'/solutions/inventory-purchase/' : 15 ,
'/solutions/weighing-scale-pos' : 16 ,
'/solutions/weighing-scale-pos/' : 16 ,
'/solutions/multi-store-erp' : 17 ,
'/solutions/multi-store-erp/' : 17 ,
'/solutions/gst-billing-e-invoice' : 18 ,
'/solutions/gst-billing-e-invoice/' : 18 ,
'/solutions/offline-billing' : 19 ,
'/solutions/offline-billing/' : 19 ,
'/solutions/healthcare-management' : 20 ,
'/solutions/healthcare-management/' : 20 ,
'/industries/bakery' : 21 ,
'/industries/bakery/' : 21 ,
'/industries/restaurant' : 22 ,
'/industries/restaurant/' : 22 ,
'/industries/salon' : 23 ,
'/industries/salon/' : 23 ,
'/industries/fashion' : 24 ,
'/industries/fashion/' : 24 ,
'/industries/electronics' : 25 ,
'/industries/electronics/' : 25 ,
'/industries/grocery' : 26 ,
'/industries/grocery/' : 26 ,
'/industries/healthcare' : 27 ,
'/industries/healthcare/' : 27 ,
'/industries/manufacturing' : 28 ,
'/industries/manufacturing/' : 28
2025-11-13 09:53:08 +05:30
} ;
const pageId = routeToPageId [ normalizedPath ] || 1 ;
console . log ( 'Page ID matched:' , pageId , 'for path:' , normalizedPath ) ;
console . log ( 'Available routes:' , Object . keys ( routeToPageId ) . filter ( k => routeToPageId [ k ] === 5 ) ) ;
const fullUrl = buildFullUrl ( req ) ;
const baseUrl = ` ${ req . protocol || 'http' } :// ${ req . headers . host || 'localhost:3000' } ` ;
// Production URL for images - always use production domain for OG images
const PRODUCTION _URL = process . env . PRODUCTION _URL || 'https://www.pozo.app' ;
// Helper function to ensure image URLs are absolute and use production URL
const ensureAbsoluteImageUrl = ( imagePath ) => {
if ( ! imagePath ) return null ;
// If already absolute URL (starts with http/https), use as-is
if ( /^https?:\/\//i . test ( imagePath ) ) {
// If it's localhost, replace with production URL
if ( /^https?:\/\/localhost/i . test ( imagePath ) ) {
return imagePath . replace ( /^https?:\/\/[^/]+/i , PRODUCTION _URL ) ;
}
return imagePath ;
}
// If relative path, make it absolute with production URL
const cleanPath = imagePath . startsWith ( '/' ) ? imagePath : ` / ${ imagePath } ` ;
return ` ${ PRODUCTION _URL } ${ cleanPath } ` ;
} ;
const dynamicSeo = await fetchSEOByPathOrSlug ( normalizedPath ) ;
let dbSeoData = dynamicSeo || ( await fetchSEOFromDB ( pageId ) ) ;
console . log ( 'SEO Data fetched:' , dbSeoData ? 'from DB' : 'using fallback' ) ;
if ( dbSeoData ) {
console . log ( 'DB SEO Data:' , {
title : dbSeoData . MetaTitle ? . substring ( 0 , 50 ) ,
image : dbSeoData . ImageUrl || dbSeoData . image ,
pageId : pageId
} ) ;
}
const fallbackData = {
1 : {
title : 'Retail ERP & POS for Indian MSMEs | POZO' ,
description : 'Fast billing, smart inventory, GST-ready POS. POZO helps kirana, mini-supermarkets & retail chains speed checkout, connect weighing scales, and manage multi-store ops.' ,
keywords : 'POS software, retail ERP, billing software, inventory management, GST billing, weighing scale POS, multi-store ERP' ,
image : ` ${ PRODUCTION _URL } /og/home.jpg ` ,
url : fullUrl
} ,
2 : {
2025-11-19 17:26:31 +05:30
title : 'Sign In to PozoApp | Retail ERP & POS Login' ,
description : 'Access your business dashboard. Sign in to POZO retail ERP & POS system for billing, inventory management, and business analytics.' ,
keywords : 'PozoApp login, sign in, business dashboard, retail management, POS login, ERP login' ,
2025-11-13 09:53:08 +05:30
image : ` ${ PRODUCTION _URL } /og/Signin-og.jpg ` ,
url : fullUrl
} ,
3 : {
title : 'Pricing - Retail ERP & POS Plans | POZO' ,
description : 'Simple plans for MSMEs. Fast billing, inventory, GST e-invoice, weighing-scale integration, WhatsApp e-bills & multi-store controls. Book a demo.' ,
keywords : 'pricing, POS software pricing, retail ERP plans, billing software pricing, inventory management pricing, GST billing plans' ,
image : ` ${ PRODUCTION _URL } /og/pricing-og.jpg ` ,
url : fullUrl
} ,
4 : {
2025-11-19 17:26:31 +05:30
title : 'Contact POZO | Retail ERP & POS Support' ,
description : 'Get support and sales information for POZO retail ERP & POS solutions. Contact us for billing software, inventory management, and business automation.' ,
2025-11-13 09:53:08 +05:30
keywords : 'contact PozoApp, support, sales, retail ERP support, POS software support' ,
image : ` ${ PRODUCTION _URL } /og/contact-og.jpg ` ,
url : fullUrl
} ,
5 : {
title : 'POZO Blog — Retail ERP, POS & Grocery Billing Guides' ,
description : 'Practical guides on POS billing, weighing-scale integration, GST e-invoices, multi-store ERP & inventory control for Indian retailers.' ,
keywords : 'POS billing, weighing scale integration, GST e-invoices, retail ERP, inventory management, grocery billing guides' ,
image : ` ${ PRODUCTION _URL } /og/blog-og.jpg ` ,
url : fullUrl
2025-11-19 17:26:31 +05:30
} ,
6 : {
title : 'About POZO | Retail ERP & POS Solutions for MSMEs' ,
description : 'Learn about POZO\'s mission to digitize Indian retail businesses with affordable ERP & POS solutions, billing software, and inventory management.' ,
keywords : 'about POZO, retail ERP company, POS software company, Indian retail solutions, MSME digitization' ,
image : ` ${ PRODUCTION _URL } /og/about-og.jpg ` ,
url : fullUrl
} ,
7 : {
title : 'Privacy Policy | POZO Retail ERP & POS' ,
description : 'POZO\'s privacy policy for retail ERP & POS users. Learn how we protect your business data, billing information, and customer details.' ,
keywords : 'privacy policy, data protection, business data security, POS privacy, ERP data protection' ,
image : ` ${ PRODUCTION _URL } /og/privacy-og.jpg ` ,
url : fullUrl
} ,
8 : {
title : 'Cookie Policy | POZO Retail ERP & POS' ,
description : 'POZO\'s cookie policy explaining how we use cookies to improve your retail ERP & POS experience and website functionality.' ,
keywords : 'cookie policy, website cookies, user experience, POS software cookies' ,
image : ` ${ PRODUCTION _URL } /og/cookie-og.jpg ` ,
url : fullUrl
} ,
9 : {
title : 'Retail Solutions | ERP & POS Software for Every Business' ,
description : 'Comprehensive retail solutions: billing software, inventory management, GST compliance, weighing scale integration, and multi-store ERP for Indian businesses.' ,
keywords : 'retail solutions, ERP software, POS solutions, billing software, inventory management, GST compliance' ,
image : ` ${ PRODUCTION _URL } /og/default-og.jpg ` ,
url : fullUrl
} ,
10 : {
title : 'Case Studies | POZO Retail ERP & POS Success Stories' ,
description : 'Real success stories of Indian retailers using POZO ERP & POS. See how businesses improved billing speed, inventory control, and customer satisfaction.' ,
keywords : 'case studies, success stories, retail ERP results, POS software benefits, customer testimonials' ,
image : ` ${ PRODUCTION _URL } /og/case-studies-og.jpg ` ,
url : fullUrl
} ,
11 : {
title : 'Live Demo Session | POZO Retail ERP & POS' ,
description : 'Join free live demo sessions of POZO retail ERP & POS. See billing, inventory management, GST features, and weighing scale integration in action.' ,
keywords : 'live demo, POS demo, ERP demonstration, free trial, retail software demo' ,
image : ` ${ PRODUCTION _URL } /og/live-session-og.jpg ` ,
url : fullUrl
} ,
12 : {
title : 'Book Demo | POZO Retail ERP & POS Free Trial' ,
description : 'Book a free demo of POZO retail ERP & POS. Experience fast billing, smart inventory, GST compliance, and business automation for your store.' ,
keywords : 'book demo, free trial, POS trial, ERP demo booking, retail software trial' ,
image : ` ${ PRODUCTION _URL } /og/book-demo-og.jpg ` ,
url : fullUrl
} ,
13 : {
title : 'Schedule Demo | POZO Retail ERP & POS Consultation' ,
description : 'Schedule a personalized demo consultation for POZO retail ERP & POS. Get expert guidance on billing, inventory, and business digitization.' ,
keywords : 'schedule demo, consultation, personalized demo, expert guidance, business consultation' ,
image : ` ${ PRODUCTION _URL } /og/schedule-demo-og.jpg ` ,
url : fullUrl
} ,
14 : {
title : 'Retail Billing Software | Fast POS for Kirana & Supermarkets' ,
description : 'Lightning-fast retail billing software with barcode scanning, GST compliance, customer management, and real-time inventory updates for Indian retailers.' ,
keywords : 'retail billing, POS software, kirana billing, supermarket POS, barcode scanning, GST billing' ,
image : ` ${ PRODUCTION _URL } /og/retail-billing-og.jpg ` ,
url : fullUrl
} ,
15 : {
title : 'Inventory & Purchase Management | Smart Stock Control' ,
description : 'Advanced inventory management with purchase orders, supplier management, stock alerts, expiry tracking, and automated reordering for retail businesses.' ,
keywords : 'inventory management, purchase management, stock control, supplier management, automated reordering' ,
image : ` ${ PRODUCTION _URL } /og/inventory-purchase-og.jpg ` ,
url : fullUrl
} ,
16 : {
title : 'Weighing Scale POS | Integrated Billing for Grocery Stores' ,
description : 'POS system with weighing scale integration for grocery stores, fruit vendors, and bulk retailers. Accurate billing with weight-based pricing.' ,
keywords : 'weighing scale POS, grocery POS, weight-based billing, fruit vendor POS, bulk retail billing' ,
image : ` ${ PRODUCTION _URL } /og/weighing-scale-pos-og.jpg ` ,
url : fullUrl
} ,
17 : {
title : 'Multi-Store ERP | Centralized Retail Chain Management' ,
description : 'Manage multiple retail locations with centralized inventory, unified reporting, inter-store transfers, and consolidated business analytics.' ,
keywords : 'multi-store ERP, retail chain management, centralized inventory, unified reporting, inter-store transfers' ,
image : ` ${ PRODUCTION _URL } /og/multi-store-erp-og.jpg ` ,
url : fullUrl
} ,
18 : {
title : 'GST Billing & E-Invoice | Compliant Retail Software' ,
description : 'GST-compliant billing with automatic e-invoice generation, GSTR filing support, tax calculations, and government portal integration.' ,
keywords : 'GST billing, e-invoice, GST compliance, GSTR filing, tax calculations, government integration' ,
image : ` ${ PRODUCTION _URL } /og/gst-billing-og.jpg ` ,
url : fullUrl
} ,
19 : {
title : 'Offline Billing Software | Works Without Internet' ,
description : 'Reliable offline billing software that works without internet. Automatic sync when online, ensuring uninterrupted business operations.' ,
keywords : 'offline billing, offline POS, no internet billing, automatic sync, reliable billing software' ,
image : ` ${ PRODUCTION _URL } /og/offline-billing-og.jpg ` ,
url : fullUrl
} ,
20 : {
title : 'Healthcare Management System | Medical Store & Clinic ERP' ,
description : 'Specialized ERP for medical stores, clinics, and healthcare providers with medicine inventory, prescription management, and patient records.' ,
keywords : 'healthcare ERP, medical store software, clinic management, medicine inventory, prescription management' ,
image : ` ${ PRODUCTION _URL } /og/healthcare-og.jpg ` ,
url : fullUrl
} ,
21 : {
title : 'Bakery POS & Management Software | POZO' ,
description : 'Complete bakery management solution with POS billing, inventory tracking, recipe management, expiry alerts & GST compliance for bakeries in India.' ,
keywords : 'bakery POS, bakery software, bakery management, recipe management, inventory tracking, expiry alerts, GST billing' ,
image : ` ${ PRODUCTION _URL } /og/default-og.jpg ` ,
url : fullUrl
2025-11-13 09:53:08 +05:30
}
} ;
const chosenFallback = fallbackData [ pageId ] || fallbackData [ 1 ] ;
// Ensure image URL is always absolute and uses production URL
// If database image URL is missing or points to wrong page (e.g., home.jpg for blog), use fallback
const dbImageUrl = dbSeoData ? . ImageUrl || dbSeoData ? . image ;
let finalImageUrl = ensureAbsoluteImageUrl ( dbImageUrl ) ;
// Validate image URL - if it's pointing to wrong page image, use correct fallback
// For blog page (pageId 5), ensure it's not using home.jpg
if ( pageId === 5 && finalImageUrl && finalImageUrl . includes ( '/og/home.jpg' ) ) {
console . log ( 'Warning: Blog page has home.jpg image, using blog-og.jpg instead' ) ;
finalImageUrl = chosenFallback . image ;
}
// For home page (pageId 1), ensure it's not using blog-og.jpg
if ( pageId === 1 && finalImageUrl && finalImageUrl . includes ( '/og/blog-og.jpg' ) ) {
console . log ( 'Warning: Home page has blog-og.jpg image, using home.jpg instead' ) ;
finalImageUrl = chosenFallback . image ;
}
// If no valid image URL, use fallback
if ( ! finalImageUrl ) {
finalImageUrl = chosenFallback . image ;
}
2025-11-19 17:26:31 +05:30
// Ensure we always have a valid image - use default-og.jpg if specific image doesn't exist
const industryImages = [ 'bakery-og.jpg' , 'restaurant-og.jpg' , 'salon-og.jpg' , 'fashion-og.jpg' , 'electronics-og.jpg' , 'grocery-og.jpg' , 'healthcare-og.jpg' , 'manufacturing-og.jpg' ] ;
if ( ! finalImageUrl || finalImageUrl . includes ( 'undefined' ) || industryImages . some ( img => finalImageUrl . includes ( img ) ) ) {
finalImageUrl = ` ${ PRODUCTION _URL } /og/default-og.jpg ` ;
}
2025-11-13 09:53:08 +05:30
// Validate URL - ensure it matches the current page path
let finalUrl = dbSeoData ? . CanonicalUrl || dbSeoData ? . url || fullUrl ;
// If database URL doesn't match the current page path, use correct fullUrl
// Blog page (pageId 5) should have /blog in URL
if ( pageId === 5 && finalUrl && ! finalUrl . includes ( '/blog' ) ) {
console . log ( 'Warning: Blog page URL is incorrect, using correct path:' , fullUrl ) ;
finalUrl = fullUrl ;
}
// Home page (pageId 1) should be root or /home, not other paths
if ( pageId === 1 && finalUrl && finalUrl . includes ( '/blog' ) ) {
console . log ( 'Warning: Home page URL is pointing to blog, using correct path:' , fullUrl ) ;
finalUrl = fullUrl ;
}
// Pricing page (pageId 3) should have /pricing in URL
if ( pageId === 3 && finalUrl && ! finalUrl . includes ( '/pricing' ) ) {
console . log ( 'Warning: Pricing page URL is incorrect, using correct path:' , fullUrl ) ;
finalUrl = fullUrl ;
}
// CRITICAL: Force correct image and URL based on pageId
// Blog page (pageId 5) MUST use blog-og.jpg, not home.jpg
// Home page (pageId 1) MUST use home.jpg, not blog-og.jpg
if ( pageId === 5 ) {
// Blog page - force blog image and URL
if ( ! finalImageUrl || finalImageUrl . includes ( '/og/home.jpg' ) ) {
console . log ( 'FORCING blog-og.jpg for blog page (pageId 5)' ) ;
finalImageUrl = ` ${ PRODUCTION _URL } /og/blog-og.jpg ` ;
}
if ( ! finalUrl || ! finalUrl . includes ( '/blog' ) ) {
console . log ( 'FORCING blog URL for blog page (pageId 5)' ) ;
finalUrl = fullUrl ;
}
} else if ( pageId === 1 ) {
// Home page - force home image
if ( ! finalImageUrl || finalImageUrl . includes ( '/og/blog-og.jpg' ) ) {
console . log ( 'FORCING home.jpg for home page (pageId 1)' ) ;
finalImageUrl = ` ${ PRODUCTION _URL } /og/home.jpg ` ;
}
}
// Force correct data based on pageId - if database returns wrong page data, use fallback
// This ensures blog page always gets blog data, not home data
let seoData ;
if ( dbSeoData ) {
// Use database data but with forced correct image/URL
seoData = {
title : dbSeoData . MetaTitle || dbSeoData . title || chosenFallback . title ,
description : dbSeoData . MetaDesc || dbSeoData . description || chosenFallback . description ,
keywords : dbSeoData . Keywords || dbSeoData . keywords || chosenFallback . keywords || '' ,
image : finalImageUrl , // This is now forced to be correct
url : finalUrl // This is now forced to be correct
} ;
} else {
// No database data, use fallback (includes keywords)
seoData = chosenFallback ;
}
console . log ( 'Final SEO Data:' , { title : seoData . title . substring ( 0 , 50 ) , image : seoData . image , url : seoData . url } ) ;
// Read HTML - try to read from specific path first, fallback to root index.html
let htmlPath ;
2025-11-19 17:26:31 +05:30
const distPath = path . join ( _ _dirname , '../dist' ) ;
2025-11-13 09:53:08 +05:30
// Try to find HTML file for the specific route
// For /home/blog, try dist/home/blog/index.html, then dist/index.html
if ( normalizedPath !== '/' && normalizedPath !== '/home' && normalizedPath !== '/home/' ) {
const routeHtmlPath = path . join ( distPath , normalizedPath , 'index.html' ) ;
if ( fs . existsSync ( routeHtmlPath ) ) {
htmlPath = routeHtmlPath ;
console . log ( 'Reading HTML from:' , routeHtmlPath ) ;
} else {
// Try without leading slash
const routeHtmlPath2 = path . join ( distPath , normalizedPath . replace ( /^\// , '' ) , 'index.html' ) ;
if ( fs . existsSync ( routeHtmlPath2 ) ) {
htmlPath = routeHtmlPath2 ;
console . log ( 'Reading HTML from:' , routeHtmlPath2 ) ;
} else {
htmlPath = path . join ( distPath , 'index.html' ) ;
console . log ( 'Using root index.html' ) ;
}
}
} else {
htmlPath = path . join ( distPath , 'index.html' ) ;
console . log ( 'Using root index.html for home page' ) ;
}
let html = fs . readFileSync ( htmlPath , 'utf8' ) ;
// ULTRA AGGRESSIVE: Remove ALL existing SEO tags (handles multiline tags)
// Match from <meta until /> or > (handles multiline with [\s\S]*?)
const metaTagPattern = / < m e t a \ s + [ ^ > ] * ? \ / ? > / g s ; / / ' s ' f l a g m a k e s . m a t c h n e w l i n e s
// Remove all OG tags (multiline aware)
html = html . replace ( /<meta\s+property=["']og:type["'][\s\S]*?\/?>/gi , '' ) ;
html = html . replace ( /<meta\s+property=["']og:url["'][\s\S]*?\/?>/gi , '' ) ;
html = html . replace ( /<meta\s+property=["']og:title["'][\s\S]*?\/?>/gi , '' ) ;
html = html . replace ( /<meta\s+property=["']og:description["'][\s\S]*?\/?>/gi , '' ) ;
html = html . replace ( /<meta\s+property=["']og:image["'][\s\S]*?\/?>/gi , '' ) ;
html = html . replace ( /<meta\s+property=["']og:site_name["'][\s\S]*?\/?>/gi , '' ) ;
html = html . replace ( /<meta\s+property=["']og:locale["'][\s\S]*?\/?>/gi , '' ) ;
// Remove Twitter tags (multiline aware)
html = html . replace ( /<meta\s+(name|property)=["']twitter:card["'][\s\S]*?\/?>/gi , '' ) ;
html = html . replace ( /<meta\s+(name|property)=["']twitter:url["'][\s\S]*?\/?>/gi , '' ) ;
html = html . replace ( /<meta\s+(name|property)=["']twitter:title["'][\s\S]*?\/?>/gi , '' ) ;
html = html . replace ( /<meta\s+(name|property)=["']twitter:description["'][\s\S]*?\/?>/gi , '' ) ;
html = html . replace ( /<meta\s+(name|property)=["']twitter:image["'][\s\S]*?\/?>/gi , '' ) ;
html = html . replace ( /<meta\s+(name|property)=["']twitter:site["'][\s\S]*?\/?>/gi , '' ) ;
2025-11-19 17:26:31 +05:30
// Remove existing description and keywords (will be re-added)
2025-11-13 09:53:08 +05:30
html = html . replace ( /<meta\s+name=["']description["'][\s\S]*?\/?>/gi , '' ) ;
html = html . replace ( /<meta\s+name=["']keywords["'][\s\S]*?\/?>/gi , '' ) ;
// Update title
html = html . replace ( /<title>[^<]*<\/title>/i , ` <title> ${ escapeHtml ( seoData . title ) } </title> ` ) ;
// Update canonical
html = html . replace ( /<link\s+rel="canonical"[^>]*\/?>/i , ` <link rel="canonical" href=" ${ escapeAttribute ( seoData . url ) } "> ` ) ;
// Add fresh SEO tags before </head>
// Include keywords if available
const keywordsTag = seoData . keywords ? ` <meta name="keywords" content=" ${ escapeHtml ( seoData . keywords ) } " /> ` : '' ;
const seoTags = `
<!-- SEO Meta Tags -- >
2025-11-19 17:26:31 +05:30
< meta name = "description" content = "${escapeAttribute(seoData.description)}" / >
2025-11-13 09:53:08 +05:30
$ { keywordsTag }
<!-- Open Graph / Facebook -- >
< meta property = "og:type" content = "website" / >
< meta property = "og:url" content = "${escapeAttribute(seoData.url)}" / >
< meta property = "og:title" content = "${escapeAttribute(seoData.title)}" / >
< meta property = "og:description" content = "${escapeAttribute(seoData.description)}" / >
< meta property = "og:image" content = "${escapeAttribute(seoData.image)}" / >
<!-- Twitter -- >
< meta name = "twitter:card" content = "summary_large_image" / >
< meta name = "twitter:url" content = "${escapeAttribute(seoData.url)}" / >
< meta name = "twitter:title" content = "${escapeAttribute(seoData.title)}" / >
< meta name = "twitter:description" content = "${escapeAttribute(seoData.description)}" / >
< meta name = "twitter:image" content = "${escapeAttribute(seoData.image)}" / > ` ;
html = html . replace ( /<\/head>/i , ` ${ seoTags } \n </head> ` ) ;
2025-11-19 17:26:31 +05:30
res . setHeader ( 'Content-Type' , 'text/html' ) ;
2025-11-13 09:53:08 +05:30
res . send ( html ) ;
} catch ( error ) {
console . error ( 'SEO middleware error:' , error ) ;
next ( ) ;
}
} ;
// Function to fetch SEO data from your database
async function fetchSEOFromDB ( pageId ) {
try {
const API _URL = process . env . API _URL || 'https://api.pozo.app' ;
const response = await axios . get ( ` ${ API _URL } /Seo?PageId= ${ pageId } ` ) ;
if ( response . data ? . statusCode === 1 && response . data ? . data ? . length > 0 ) {
return response . data . data [ 0 ] ;
}
return null ;
} catch ( error ) {
console . error ( 'Failed to fetch SEO data from database:' , error . message ) ;
return null ;
}
}
// Try to fetch SEO by exact path or blog slug, with graceful fallbacks
async function fetchSEOByPathOrSlug ( normalizedPath ) {
try {
const API _URL = process . env . API _URL || 'https://api.pozo.app' ;
// 1) Try path-based SEO: /Seo?Path=/home/blog/my-post
const byPath = await safeGet ( ` ${ API _URL } /Seo ` , { Path : normalizedPath } ) ;
if ( byPath ) return byPath ;
// 2) If looks like blog detail: /blog/slug or /home/blog/slug
if ( /^\/(home\/)?blog\// . test ( normalizedPath ) ) {
const slug = normalizedPath . replace ( /^\/(home\/)?blog\// , '' ) . replace ( /\/$/ , '' ) ;
if ( slug ) {
// Try common blog SEO endpoints
const blogSeo = await safeGet ( ` ${ API _URL } /Seo/Blog ` , { slug } )
|| await safeGet ( ` ${ API _URL } /Blog/Seo ` , { slug } )
|| await safeGet ( ` ${ API _URL } /Blog ` , { slug } ) ;
if ( blogSeo ) return blogSeo ;
}
}
return null ;
} catch ( e ) {
return null ;
}
}
async function safeGet ( baseUrl , queryObj ) {
try {
const qs = new URLSearchParams ( queryObj ) . toString ( ) ;
const url = ` ${ baseUrl } ? ${ qs } ` ;
const response = await axios . get ( url ) ;
const data = response . data ;
if ( data ? . statusCode === 1 && Array . isArray ( data ? . data ) && data . data . length > 0 ) {
return data . data [ 0 ] ;
}
// Some APIs return object directly
if ( data && typeof data === 'object' && ! Array . isArray ( data ) ) return data ;
return null ;
} catch ( e ) {
return null ;
}
}
function normalizePath ( p ) {
if ( ! p ) return '/' ;
try {
// Remove query/hash, ensure leading slash, collapse duplicate slashes
const onlyPath = p . split ( '?' ) [ 0 ] . split ( '#' ) [ 0 ] || '/' ;
// Collapse multiple slashes to single slash, but preserve path structure
let normalized = onlyPath . replace ( /\/+/g , '/' ) ;
if ( ! normalized . startsWith ( '/' ) ) normalized = ` / ${ normalized } ` ;
// Remove trailing slash except for root
if ( normalized . length > 1 && normalized . endsWith ( '/' ) ) {
normalized = normalized . slice ( 0 , - 1 ) ;
}
console . log ( '🔍 normalizePath input:' , p , '→ output:' , normalized ) ;
return normalized ;
} catch {
return '/' ;
}
}
// Map routes - support both /home/* and clean routes
function mapToHomeNamespace ( p ) {
if ( ! p ) return '/' ;
const raw = p . split ( '?' ) [ 0 ] . split ( '#' ) [ 0 ] || '/' ;
// Just normalize and return - we support both /home/ and clean routes now
return normalizePath ( raw ) ;
}
function buildFullUrl ( req ) {
const proto = ( req . headers [ 'x-forwarded-proto' ] || req . protocol || 'https' ) . split ( ',' ) [ 0 ] ;
const host = req . headers [ 'x-forwarded-host' ] || req . headers . host || 'www.pozo.app' ;
const pathOnly = normalizePath ( req . path ) ;
return ` ${ proto } :// ${ host } ${ pathOnly } ` ;
}
function replaceOrInsert ( html , regex , newTagHtml ) {
if ( regex . test ( html ) ) {
return html . replace ( regex , newTagHtml ) ;
}
// Insert before </head>
if ( /<\/head>/i . test ( html ) ) {
return html . replace ( /<\/head>/i , ` ${ newTagHtml } \n </head> ` ) ;
}
// As a last resort, prepend to document
return ` ${ newTagHtml } \n ${ html } ` ;
}
function escapeHtml ( str ) {
return String ( str || '' )
. replace ( /&/g , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' ) ;
}
function escapeAttribute ( str ) {
return String ( str || '' )
. replace ( /&/g , '&' )
. replace ( /"/g , '"' )
. replace ( /</g , '<' )
. replace ( />/g , '>' ) ;
}
export default seoMiddleware ;