// Express middleware to inject dynamic SEO meta tags import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import axios from 'axios'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const seoMiddleware = async (req, res, next) => { // Skip non-HTML requests and static assets (only skip if it has a file extension that's not .html) if (req.path.includes('.') && !req.path.endsWith('.html') && !req.path.endsWith('/')) { return next(); } // 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(); } try { console.log('SEO Middleware triggered for:', req.path); const normalizedPath = normalizePath(mapToHomeNamespace(req.path)); // Attempt dynamic SEO fetch by path/slug first, fallback to pageId map const routeToPageId = { '/home/': 1, '/home/blog': 5, '/home/blog/': 5, '/home/pricing-pozoapp': 3, '/home/pricing-pozoapp/': 3, '/home/contact-us': 4, '/home/contact-us/': 4, '/home/signin': 2, '/home/signin/': 2 }; const pageId = routeToPageId[normalizedPath] || 1; const fullUrl = buildFullUrl(req); const dynamicSeo = await fetchSEOByPathOrSlug(normalizedPath); let dbSeoData = dynamicSeo || (await fetchSEOFromDB(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.', image: 'https://www.pozo.app/og/home.jpg', url: fullUrl }, 2: { title: 'Sign In to PozoApp', description: 'Access your business dashboard', image: 'https://www.pozo.app/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.', image: 'https://www.pozo.app/og/pricing-og.jpg', url: fullUrl }, 4: { title: 'Contact PozoApp', description: 'Get support and sales information', image: 'https://www.pozo.app/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.', image: 'https://www.pozo.app/og/blog-og.jpg', url: fullUrl } }; const chosenFallback = fallbackData[routeToPageId[normalizedPath]] || fallbackData[1]; const seoData = dbSeoData ? { title: dbSeoData.MetaTitle || dbSeoData.title || chosenFallback.title, description: dbSeoData.MetaDesc || dbSeoData.description || chosenFallback.description, image: dbSeoData.ImageUrl || dbSeoData.image || chosenFallback.image, url: dbSeoData.CanonicalUrl || dbSeoData.url || fullUrl } : chosenFallback; // Read HTML for route or fallback to index.html // For dist deployment: __dirname is dist/server/, so go up to dist/ let htmlPath = path.join( __dirname, '..', normalizedPath.endsWith('/') ? normalizedPath.slice(0, -1) : normalizedPath, 'index.html' ); if (!fs.existsSync(htmlPath)) { htmlPath = path.join(__dirname, '..', 'index.html'); } let html = fs.readFileSync(htmlPath, 'utf8'); // Ensure exists if (!//i.test(html)) { html = html.replace(/]*>/i, match => `${match}\n`); } // Title html = replaceOrInsert(html, /[^<]*<\/title>/i, `<title>${escapeHtml(seoData.title)}`); // Description html = replaceOrInsert( html, /]*>/i, `` ); // Canonical html = replaceOrInsert( html, /]*>/i, `` ); // Open Graph html = replaceOrInsert(html, /]*>/i, ``); html = replaceOrInsert(html, /]*>/i, ``); html = replaceOrInsert(html, /]*>/i, ``); html = replaceOrInsert(html, /]*>/i, ``); html = replaceOrInsert(html, /]*>/i, ``); // Twitter Cards html = replaceOrInsert(html, /]*>/i, ``); html = replaceOrInsert(html, /]*>/i, ``); html = replaceOrInsert(html, /]*>/i, ``); html = replaceOrInsert(html, /]*>/i, ``); 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: /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] || '/'; 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); } return normalized; } catch { return '/'; } } // Map routes without the "/home" prefix into the SPA's "/home" namespace function mapToHomeNamespace(p) { if (!p) return '/home/'; const raw = p.split('?')[0].split('#')[0] || '/'; // Already in /home namespace if (raw === '/' || raw.startsWith('/home/')) return raw; // Known top-level marketing routes to map into /home/* const known = ['pricing-pozoapp', 'contact-us', 'signin', 'blog']; const parts = raw.replace(/^\/+/, '').split('/'); const head = parts[0]; if (known.includes(head)) { // blog detail: /blog/slug -> /home/blog/slug if (head === 'blog' && parts.length > 1) { return `/home/${parts.join('/')}`; } // simple mapping: /pricing-pozoapp -> /home/pricing-pozoapp return `/home/${raw.replace(/^\/+/, '')}`; } // default: keep as-is return 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 if (/<\/head>/i.test(html)) { return html.replace(/<\/head>/i, `${newTagHtml}\n`); } // As a last resort, prepend to document return `${newTagHtml}\n${html}`; } function escapeHtml(str) { return String(str || '') .replace(/&/g, '&') .replace(//g, '>'); } function escapeAttribute(str) { return String(str || '') .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); } export default seoMiddleware;