590 lines
27 KiB
JavaScript
590 lines
27 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) => {
|
|
// 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,
|
|
'/signin': 2,
|
|
'/signin/': 2,
|
|
'/pricing': 3,
|
|
'/pricing/': 3,
|
|
'/contact-us': 4,
|
|
'/contact-us/': 4,
|
|
'/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
|
|
};
|
|
|
|
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: {
|
|
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/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
|
|
}
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
// 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, 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;
|
|
const distPath = path.join(__dirname, '..');
|
|
|
|
// 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 = /<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, '');
|
|
|
|
// 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 -->
|
|
<meta name="description" content="${escapeAttribute(seoData.description)}" />
|
|
${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>`);
|
|
|
|
res.setHeader('Content-Type', 'text/html');
|
|
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;
|