pozo-common-webapp/server/seo-middleware.js

1115 lines
47 KiB
JavaScript

// Express middleware to inject dynamic SEO meta tags
import fs from 'fs';
import path from 'path';
import axios from 'axios';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const seoMiddleware = async (req, res, next) => {
// CRITICAL: Skip ALL static assets IMMEDIATELY - don't even process them
const requestPath = req.path;
// Skip API routes
if (requestPath.startsWith('/api/')) {
return next();
}
// CRITICAL: Skip /src/ paths FIRST - these should NEVER be processed by SEO middleware
// In production, /src/ files shouldn't be requested, but if they are, skip them
if (requestPath.startsWith('/src/')) {
return next();
}
// Skip ALL static asset paths FIRST (before any other checks)
if (requestPath.startsWith('/assets/') ||
requestPath.startsWith('/@') ||
requestPath.startsWith('/og/') ||
requestPath.startsWith('/static/') ||
requestPath.startsWith('/home/assets/') ||
requestPath.startsWith('/home/og/') ||
requestPath.includes('/PozoAppFavicon.png') ||
requestPath.includes('/fav.ico') ||
requestPath.includes('/manifest.json')) {
return next();
}
// Skip files with extensions (except .html) - CRITICAL for JS/CSS files
if (requestPath.match(/\.[a-z]{2,4}$/i) && !requestPath.endsWith('.html')) {
return next();
}
// Skip if path contains any file extension indicators (double check)
if (requestPath.includes('.js') ||
requestPath.includes('.jsx') ||
requestPath.includes('.css') ||
requestPath.includes('.png') ||
requestPath.includes('.jpg') ||
requestPath.includes('.jpeg') ||
requestPath.includes('.gif') ||
requestPath.includes('.svg') ||
requestPath.includes('.woff') ||
requestPath.includes('.woff2') ||
requestPath.includes('.ttf') ||
requestPath.includes('.eot') ||
requestPath.includes('.ico') ||
requestPath.includes('.json') ||
requestPath.includes('.xml') ||
requestPath.includes('.webp')) {
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
// Enhanced route mapping with /home prefix support
const routeToPageId = {
// Home routes
'/': 1,
'/home': 1,
'/home/': 1,
// Sign in routes
'/signin': 2,
'/signin/': 2,
'/home/signin': 2,
'/home/signin/': 2,
// Pricing routes
'/pricing': 3,
'/pricing/': 3,
'/home/pricing': 3,
'/home/pricing/': 3,
'/pricing-pozoapp': 3,
'/home/pricing-pozoapp': 3,
'/home/pricing-pozoapp/': 3,
// Contact routes
'/contact-us': 4,
'/contact-us/': 4,
'/home/contact-us': 4,
'/home/contact-us/': 4,
// Blog routes
'/blog': 5,
'/blog/': 5,
'/home/blog': 5,
'/home/blog/': 5,
// Live session routes
'/live-session': 11,
'/live-session/': 11,
'/home/live-session': 11,
'/home/live-session/': 11,
'/live-Session': 11,
'/home/live-Session': 11,
'/home/live-Session/': 11,
// Other routes
'/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,
'/book-demo': 12,
'/book-demo/': 12,
'/schedule-demo': 13,
'/schedule-demo/': 13,
// Solution pages - specific pageIds for each solution
'/solutions/retail-billing': 14,
'/solutions/retail-billing/': 14,
'/home/solutions/retail-billing': 14,
'/home/solutions/retail-billing/': 14,
'/solutions/inventory-purchase': 15,
'/solutions/inventory-purchase/': 15,
'/home/solutions/inventory-purchase': 15,
'/home/solutions/inventory-purchase/': 15,
'/solutions/weighing-scale-pos': 16,
'/solutions/weighing-scale-pos/': 16,
'/home/solutions/weighing-scale-pos': 16,
'/home/solutions/weighing-scale-pos/': 16,
'/solutions/multi-store-erp': 17,
'/solutions/multi-store-erp/': 17,
'/home/solutions/multi-store-erp': 17,
'/home/solutions/multi-store-erp/': 17,
'/solutions/gst-billing-e-invoice': 18,
'/solutions/gst-billing-e-invoice/': 18,
'/home/solutions/gst-billing-e-invoice': 18,
'/home/solutions/gst-billing-e-invoice/': 18,
'/solutions/offline-billing': 19,
'/solutions/offline-billing/': 19,
'/home/solutions/offline-billing': 19,
'/home/solutions/offline-billing/': 19,
'/solutions/healthcare-management': 20,
'/solutions/healthcare-management/': 20,
'/home/solutions/healthcare-management': 20,
'/home/solutions/healthcare-management/': 20
};
// Get pageId with better matching
let pageId = routeToPageId[normalizedPath];
// If no exact match, try pattern matching with more specific rules
if (!pageId) {
console.log('🔍 Pattern matching for path:', normalizedPath);
// Solution pages - check for specific solutions first
if (normalizedPath.includes('/solutions/retail-billing')) {
pageId = 14;
console.log(' → Matched Retail Billing (pageId 14)');
} else if (normalizedPath.includes('/solutions/inventory-purchase')) {
pageId = 15;
console.log(' → Matched Inventory Purchase (pageId 15)');
} else if (normalizedPath.includes('/solutions/weighing-scale-pos')) {
pageId = 16;
console.log(' → Matched Weighing Scale POS (pageId 16)');
} else if (normalizedPath.includes('/solutions/multi-store-erp')) {
pageId = 17;
console.log(' → Matched Multi-Store ERP (pageId 17)');
} else if (normalizedPath.includes('/solutions/gst-billing-e-invoice')) {
pageId = 18;
console.log(' → Matched GST Billing E-Invoice (pageId 18)');
} else if (normalizedPath.includes('/solutions/offline-billing')) {
pageId = 19;
console.log(' → Matched Offline Billing (pageId 19)');
} else if (normalizedPath.includes('/solutions/healthcare-management')) {
pageId = 20;
console.log(' → Matched Healthcare Management (pageId 20)');
} else if (normalizedPath.includes('/blog')) {
pageId = 5; // Blog
console.log(' → Matched Blog (pageId 5)');
} else if (normalizedPath.includes('/pricing')) {
pageId = 3; // Pricing
console.log(' → Matched Pricing (pageId 3)');
} else if (normalizedPath.includes('/signin')) {
pageId = 2; // Sign in
console.log(' → Matched Sign In (pageId 2)');
} else if (normalizedPath.includes('/contact')) {
pageId = 4; // Contact
console.log(' → Matched Contact (pageId 4)');
} else if (normalizedPath.includes('/live-session') || normalizedPath.includes('/live-Session')) {
pageId = 11; // Live session
console.log(' → Matched Live Session (pageId 11)');
} else if (normalizedPath.includes('/about')) {
pageId = 6; // About
console.log(' → Matched About (pageId 6)');
} else if (normalizedPath.includes('/solutions')) {
pageId = 9; // General Solutions page
console.log(' → Matched General Solutions (pageId 9)');
} else {
pageId = 1; // Default to home
console.log(' → Default to Home (pageId 1)');
}
} else {
console.log('🎯 Exact match found for pageId:', pageId);
}
console.log('🎯 Route Analysis:');
console.log(' Original path:', req.path);
console.log(' Normalized path:', normalizedPath);
console.log(' Matched Page ID:', pageId);
console.log(' Page type:', getPageType(pageId));
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}`;
};
console.log('💾 Fetching SEO data for pageId:', pageId, 'path:', normalizedPath);
const dynamicSeo = await fetchSEOByPathOrSlug(normalizedPath);
let dbSeoData = dynamicSeo || (await fetchSEOFromDB(pageId));
console.log('📊 SEO Data Analysis:');
console.log(' Dynamic SEO (by path):', dynamicSeo ? 'Found' : 'Not found');
console.log(' DB SEO (by pageId):', dbSeoData ? 'Found' : 'Not found');
if (dbSeoData) {
console.log(' DB SEO Details:', {
title: dbSeoData.MetaTitle?.substring(0, 50) + '...',
description: dbSeoData.MetaDesc?.substring(0, 50) + '...',
image: dbSeoData.ImageUrl || dbSeoData.image,
pageId: pageId,
hasTitle: !!dbSeoData.MetaTitle,
hasDesc: !!dbSeoData.MetaDesc
});
} else {
console.log(' ⚠️ No DB data found - will use fallback for 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: {
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',
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: {
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.',
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
},
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/default-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
}
};
// Ensure we get the correct fallback data for the matched pageId
const chosenFallback = fallbackData[pageId] || fallbackData[1];
console.log('📊 SEO Data Selection:');
console.log(' Using pageId:', pageId);
console.log(' Fallback title:', chosenFallback.title.substring(0, 50) + '...');
console.log(' Has DB data:', !!dbSeoData);
// 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;
}
// 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`;
}
// 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, construct production URL
// Blog page (pageId 5) should have /blog in URL
if (pageId === 5 && finalUrl && !finalUrl.includes('/blog')) {
console.log('Warning: Blog page URL is incorrect, constructing correct path');
const blogPath = normalizedPath === '/' ? '/blog' : normalizedPath;
finalUrl = `${PRODUCTION_URL.replace(/\/$/, '')}${blogPath}`;
}
// 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 root');
finalUrl = `${PRODUCTION_URL.replace(/\/$/, '')}/`;
}
// Pricing page (pageId 3) should have /pricing in URL
if (pageId === 3 && finalUrl && !finalUrl.includes('/pricing')) {
console.log('Warning: Pricing page URL is incorrect, constructing correct path');
finalUrl = `${PRODUCTION_URL.replace(/\/$/, '')}/pricing`;
}
// CRITICAL: Replace localhost URLs with production URL
if (finalUrl && /^https?:\/\/localhost/i.test(finalUrl)) {
const pathPart = finalUrl.replace(/^https?:\/\/[^/]+/i, '');
finalUrl = `${PRODUCTION_URL.replace(/\/$/, '')}${pathPart}`;
console.log('Replaced localhost URL with production:', finalUrl);
}
// Also ensure finalUrl doesn't have localhost at all
if (finalUrl && finalUrl.includes('localhost')) {
const pathPart = finalUrl.replace(/^https?:\/\/[^/]+/i, '');
finalUrl = `${PRODUCTION_URL.replace(/\/$/, '')}${pathPart}`;
console.log('Force replaced any localhost in URL:', finalUrl);
}
// 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)');
// Construct production URL, don't use fullUrl (has localhost)
const blogPath = normalizedPath === '/' ? '/blog' : normalizedPath;
finalUrl = `${PRODUCTION_URL.replace(/\/$/, '')}${blogPath}`;
}
} 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 - ensure each page gets unique content
let seoData;
// CRITICAL: ALWAYS use fallback data to ensure unique content per page
// This ensures each page gets its own unique SEO content
seoData = {
title: chosenFallback.title,
description: chosenFallback.description,
keywords: chosenFallback.keywords || '',
image: finalImageUrl,
url: finalUrl
};
// Only override with database data if it's clearly different and page-specific
if (dbSeoData && dbSeoData.MetaTitle &&
dbSeoData.MetaTitle !== chosenFallback.title &&
!dbSeoData.MetaTitle.includes('POZO') &&
dbSeoData.MetaTitle.length > 10) {
seoData.title = dbSeoData.MetaTitle;
}
if (dbSeoData && dbSeoData.MetaDesc &&
dbSeoData.MetaDesc !== chosenFallback.description &&
dbSeoData.MetaDesc.length > 50) {
seoData.description = dbSeoData.MetaDesc;
}
if (dbSeoData && dbSeoData.Keywords &&
dbSeoData.Keywords !== chosenFallback.keywords) {
seoData.keywords = dbSeoData.Keywords;
}
console.log('🎆 Final SEO Data:');
console.log(' Title:', seoData.title.substring(0, 60) + '...');
console.log(' Description:', seoData.description.substring(0, 80) + '...');
console.log(' Image:', seoData.image.split('/').pop());
console.log(' URL:', seoData.url);
// CRITICAL: Final check - ensure seoData.url NEVER has localhost
if (seoData.url && (seoData.url.includes('localhost') || /^https?:\/\/localhost/i.test(seoData.url))) {
const pathPart = seoData.url.replace(/^https?:\/\/[^/]+/i, '');
seoData.url = `${PRODUCTION_URL.replace(/\/$/, '')}${pathPart}`;
console.log('FINAL FIX: Replaced localhost in seoData.url:', seoData.url);
}
// One more safety check - if URL still has localhost, force replace
if (seoData.url && seoData.url.includes('localhost')) {
const pathPart = seoData.url.replace(/^https?:\/\/[^/]+/i, '');
seoData.url = `${PRODUCTION_URL.replace(/\/$/, '')}${pathPart}`;
console.log('SAFETY CHECK: Force replaced localhost:', seoData.url);
}
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;
const distPath = path.join(__dirname, '..', 'dist');
// 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');
}
// Check if HTML file exists
if (!fs.existsSync(htmlPath)) {
console.error('❌ HTML file not found:', htmlPath);
console.error('❌ Please build the project first using: npm run build or BUILD-WITH-SEO.bat');
return res.status(500).send(`
<!DOCTYPE html>
<html>
<head>
<title>Build Required - POZO</title>
<style>
body { font-family: Arial; padding: 50px; text-align: center; }
h1 { color: #e74c3c; }
code { background: #f4f4f4; padding: 10px; border-radius: 5px; }
</style>
</head>
<body>
<h1>❌ Build Required</h1>
<p>The <code>dist/index.html</code> file is missing.</p>
<p>Please build the project first:</p>
<p><code>npm run build</code> or run <code>BUILD-WITH-SEO.bat</code></p>
</body>
</html>
`);
}
let html = fs.readFileSync(htmlPath, 'utf8');
// Check if HTML is empty
if (!html || html.trim().length === 0) {
console.error('❌ HTML file is empty:', htmlPath);
return res.status(500).send(`
<!DOCTYPE html>
<html>
<head>
<title>Empty HTML - POZO</title>
<style>
body { font-family: Arial; padding: 50px; text-align: center; }
h1 { color: #e74c3c; }
</style>
</head>
<body>
<h1>❌ HTML File is Empty</h1>
<p>Please rebuild the project.</p>
</body>
</html>
`);
}
// ULTRA AGGRESSIVE: Remove ALL existing SEO tags (handles multiline tags)
// Match from <meta until /> or > (handles multiline with [\s\S]*?)
const metaTagPattern = /<meta\s+[^>]*?\/?>/gs; // 's' flag makes . match newlines
// 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, '');
// Remove existing description and keywords (will be re-added)
html = html.replace(/<meta\s+name=["']description["'][\s\S]*?\/?>/gi, '');
html = html.replace(/<meta\s+name=["']keywords["'][\s\S]*?\/?>/gi, '');
// Remove empty SEO comment sections
html = html.replace(/<!--\s*SEO Meta Tags\s*-->\s*[\r\n\s]*/gi, '');
html = html.replace(/<!--\s*Open Graph \/ Facebook\s*-->\s*[\r\n\s]*/gi, '');
html = html.replace(/<!--\s*Twitter\s*-->\s*[\r\n\s]*/gi, '');
// Remove all existing structured data scripts
html = html.replace(/<script\s+type=["']application\/ld\+json["'][\s\S]*?<\/script>/gi, '');
// Remove old GTM script with placeholder ID (GTM-XXXXXXX)
html = html.replace(/<!--\s*Google Tag Manager\s*-->[\s\S]*?<!--\s*End Google Tag Manager\s*-->/gi, '');
html = html.replace(/<script[^>]*googletagmanager[^>]*GTM-XXXXXXX[^>]*><\/script>/gi, '');
// Update title
html = html.replace(/<title>[^<]*<\/title>/i, `<title>${escapeHtml(seoData.title)}</title>`);
// Update canonical - replace localhost URLs
// Use finalUrl which already has localhost replaced, or seoData.url
let canonicalUrl = finalUrl || seoData.url;
// Double-check: replace localhost if still present
if (canonicalUrl && /^https?:\/\/localhost[^/]*/i.test(canonicalUrl)) {
const pathPart = canonicalUrl.replace(/^https?:\/\/[^/]+/i, '');
canonicalUrl = `${PRODUCTION_URL.replace(/\/$/, '')}${pathPart}`;
}
// If still no valid URL, construct from normalizedPath
if (!canonicalUrl || canonicalUrl.includes('localhost')) {
canonicalUrl = `${PRODUCTION_URL.replace(/\/$/, '')}${normalizedPath === '/' ? '' : normalizedPath}`;
}
html = html.replace(/<link\s+rel=["']canonical["'][^>]*\/?>/i, `<link rel="canonical" href="${escapeAttribute(canonicalUrl)}">`);
// Generate all tracking scripts and structured data
const trackingScripts = `
<!-- Microsoft Verification (Bing Webmaster Tools) -->
<meta name="msvalidate.01" content="f7ee751123e6ba9075b7924bb38a5cae" />
<!-- Microsoft Clarity -->
<script type="text/javascript">
(function (c, l, a, r, i, t, y) {
c[a] = c[a] || function () { (c[a].q = c[a].q || []).push(arguments) };
t = l.createElement(r); t.async = 1; t.src = "https://www.clarity.ms/tag/" + i;
y = l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t, y);
})(window, document, "clarity", "script", "u49bg68ikk");
</script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-2QV0HX3QD6"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-2QV0HX3QD6');
</script>
<!-- Google Tag Manager -->
<script>(function (w, d, s, l, i) {
w[l] = w[l] || []; w[l].push({
'gtm.start': new Date().getTime(), event: 'gtm.js'
}); var f = d.getElementsByTagName(s)[0],
j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =
'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', 'GTM-W2NQZPX');</script>
<!-- End Google Tag Manager -->`;
// Generate structured data based on page
const generateStructuredData = () => {
const baseUrl = PRODUCTION_URL.replace(/\/$/, '');
// Use finalUrl (already has localhost replaced) or construct from normalizedPath
let pageUrl = finalUrl;
// If still has localhost, replace it
if (pageUrl && /^https?:\/\/localhost/i.test(pageUrl)) {
const pathPart = pageUrl.replace(/^https?:\/\/[^/]+/i, '');
pageUrl = `${baseUrl}${pathPart}`;
}
// If no URL, construct from normalizedPath
if (!pageUrl || pageUrl === fullUrl) {
pageUrl = `${baseUrl}${normalizedPath === '/' ? '' : normalizedPath}`;
}
// Base schemas (Organization + WebSite) - always included
const baseSchemas = [
{
"@type": "Organization",
"name": "POZO",
"url": baseUrl + "/",
"logo": baseUrl + "/static/brand/logo.png",
"foundingDate": "2019",
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+91-7324000011",
"contactType": "customer service"
},
"address": {
"@type": "PostalAddress",
"streetAddress": "No 51 Step Colony, Dharga",
"addressLocality": "Hosur",
"addressRegion": "Tamil Nadu",
"postalCode": "635126",
"addressCountry": "IN"
},
"sameAs": []
},
{
"@type": "WebSite",
"name": "POZO",
"url": baseUrl + "/",
"potentialAction": {
"@type": "SearchAction",
"target": baseUrl + "/search?q={query}",
"query-input": "required name=query"
}
}
];
// Page-specific schemas
let pageSchemas = [];
// Home page (pageId 1) - Organization + WebSite + WebPage only
if (pageId === 1) {
pageSchemas = [
{
"@type": "WebPage",
"url": pageUrl,
"name": seoData.title,
"isPartOf": { "@id": baseUrl + "/" },
"description": seoData.description
}
];
}
// Blog page (pageId 5) - CollectionPage instead of WebPage
else if (pageId === 5) {
pageSchemas = [
{
"@type": "CollectionPage",
"@id": pageUrl,
"name": seoData.title,
"isPartOf": { "@id": baseUrl + "/" },
"description": seoData.description
}
];
}
// Pricing page (pageId 3) - WebPage + SoftwareApplication
else if (pageId === 3) {
pageSchemas = [
{
"@type": "WebPage",
"@id": pageUrl,
"name": seoData.title,
"description": seoData.description
},
{
"@type": "SoftwareApplication",
"name": "POZO",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web",
"url": pageUrl,
"offers": {
"@type": "Offer",
"priceCurrency": "INR",
"availability": "https://schema.org/InStock"
},
"featureList": [
"Fast POS billing",
"Inventory & purchase",
"GST e-invoice",
"Weighing-scale integration",
"WhatsApp e-bill",
"Multi-store ERP"
]
}
];
}
// Other pages - default WebPage
else {
pageSchemas = [
{
"@type": "WebPage",
"url": pageUrl,
"name": seoData.title,
"isPartOf": { "@id": baseUrl + "/" },
"description": seoData.description
}
];
}
return {
"@context": "https://schema.org",
"@graph": [...baseSchemas, ...pageSchemas]
};
};
const structuredDataScript = `<script type="application/ld+json">${JSON.stringify(generateStructuredData(), null, 2)}</script>`;
// Add fresh SEO tags before </head>
// Include keywords if available
const keywordsTag = seoData.keywords ? `<meta name="keywords" content="${escapeHtml(seoData.keywords)}" />` : '';
// Shortened OG description for home page (pageId 1)
const ogDescription = pageId === 1
? 'Fast billing, smart inventory, GST-ready POS for kirana & supermarkets.'
: seoData.description;
const seoTags = `
<!-- SEO Meta Tags -->
<meta name="description" content="${escapeAttribute(seoData.description)}" />
${keywordsTag}
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="${escapeAttribute(canonicalUrl)}" />
<meta property="og:title" content="${escapeAttribute(seoData.title)}" />
<meta property="og:description" content="${escapeAttribute(ogDescription)}" />
<meta property="og:image" content="${escapeAttribute(seoData.image)}" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="${escapeAttribute(canonicalUrl)}" />
<meta name="twitter:title" content="${escapeAttribute(seoData.title)}" />
<meta name="twitter:description" content="${escapeAttribute(seoData.description)}" />
<meta name="twitter:image" content="${escapeAttribute(seoData.image)}" />
<link rel="canonical" href="${escapeAttribute(canonicalUrl)}" />
<link rel="sitemap" href="/sitemap.xml" />
${trackingScripts}
<!-- Structured Data -->
${structuredDataScript}`;
html = html.replace(/<\/head>/i, `${seoTags}\n</head>`);
// Remove ALL existing GTM noscript tags (old ones from index.html)
html = html.replace(/<!--\s*Google Tag Manager \(noscript\)\s*-->[\s\S]*?<!--\s*End Google Tag Manager \(noscript\)\s*-->/gi, '');
html = html.replace(/<noscript>\s*<iframe[^>]*googletagmanager[^>]*><\/iframe>\s*<\/noscript>/gi, '');
// Add Google Tag Manager noscript right after <body> tag
const gtmNoscript = `
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-W2NQZPX" height="0" width="0"
style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->`;
// Insert GTM noscript right after <body> tag
html = html.replace(/<body[^>]*>/i, (match) => `${match}\n${gtmNoscript}`);
res.setHeader('Content-Type', 'text/html');
res.send(html);
} catch (error) {
console.error('SEO middleware error:', error);
console.error('Error stack:', error.stack);
// If error, try to serve the HTML file directly without SEO injection
try {
const distPath = path.join(__dirname, '..');
const htmlPath = path.join(distPath, 'index.html');
if (fs.existsSync(htmlPath)) {
const html = fs.readFileSync(htmlPath, 'utf8');
res.setHeader('Content-Type', 'text/html');
res.send(html);
return;
}
} catch (fallbackError) {
console.error('Fallback also failed:', fallbackError);
}
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 or /Seo?Path=/solutions/retail-billing
// This is the PRIMARY method - tries to fetch SEO data by exact path from database
const byPath = await safeGet(`${API_URL}/Seo`, { Path: normalizedPath });
if (byPath) {
console.log('✅ Found SEO data by path:', normalizedPath);
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) {
console.log('✅ Found SEO data by blog slug:', slug);
return blogSeo;
}
}
}
// 3) Try for solution pages: /solutions/retail-billing
if (/^\/(home\/)?solutions\//.test(normalizedPath)) {
const solutionPath = normalizedPath.replace(/^\/(home\/)?/, '').replace(/\/$/, '');
if (solutionPath) {
const solutionSeo = await safeGet(`${API_URL}/Seo`, { Path: `/${solutionPath}` })
|| await safeGet(`${API_URL}/Seo`, { Path: solutionPath });
if (solutionSeo) {
console.log('✅ Found SEO data by solution path:', solutionPath);
return solutionSeo;
}
}
}
// 4) Try for industry pages: /industries/restaurant or /home/restaurant
if (/^\/(home\/)?industries\//.test(normalizedPath) || /^\/(home\/)(?!blog|pricing|contact|signin|solutions|about|privacy|cookie|case-studies|live-session|book-demo|schedule-demo)/.test(normalizedPath)) {
const industryPath = normalizedPath.replace(/^\/(home\/)?/, '').replace(/\/$/, '');
if (industryPath && !industryPath.includes('/')) {
const industrySeo = await safeGet(`${API_URL}/Seo`, { Path: `/${industryPath}` })
|| await safeGet(`${API_URL}/Seo`, { Path: industryPath });
if (industrySeo) {
console.log('✅ Found SEO data by industry path:', industryPath);
return industrySeo;
}
}
}
return null;
} catch (e) {
console.error('Error in fetchSEOByPathOrSlug:', e.message);
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function escapeAttribute(str) {
return String(str || '')
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// Helper function to get page type name
function getPageType(pageId) {
const pageTypes = {
1: 'Home',
2: 'Sign In',
3: 'Pricing',
4: 'Contact Us',
5: 'Blog',
6: 'About Us',
7: 'Privacy Policy',
8: 'Cookie Policy',
9: 'Solutions',
10: 'Case Studies',
11: 'Live Session',
12: 'Book Demo',
13: 'Schedule Demo'
};
return pageTypes[pageId] || 'Unknown';
}
export default seoMiddleware;