#!/usr/bin/env node // Generates static comparison pages at dist/compare/{slug}/index.html. // Each page is pre-rendered for instant load and full SEO indexability. import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { TeknavData } from '../teknav-data.js'; import { cleanText, renderStaticFallback, replaceStaticFallback } from './static-fallback.mjs'; const SITE_URL = (process.env.TEKNAV_SITE_URL || process.env.VITE_SITE_URL || 'https://www.teknav.ir').replace(/\/$/, ''); const comparisons = TeknavData.comparisons ?? []; if (!comparisons.length) { console.log('[comparison-pages] no comparisons defined — skipping'); process.exit(0); } const template = readFileSync(resolve('dist/index.html'), 'utf8'); function esc(value) { return String(value ?? '') .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); } function clean(value) { return String(value ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); } function truncate(value, max = 155) { const text = clean(value); return text.length > max ? `${text.slice(0, max - 1).trim()}…` : text; } function buildPage(comparison) { const canonical = `${SITE_URL}/compare/${comparison.slug}`; const title = esc(`${comparison.title} | تکناو`); const description = esc(truncate(comparison.metaDescription || comparison.title, 160)); const keywords = esc([comparison.item1.name, comparison.item2.name, 'مقایسه', ...(comparison.tags || [])].join(', ')); const image = `${SITE_URL}/images/og/default.jpg`; const datePublished = comparison.dateEn || new Date().toISOString().slice(0, 10); // FAQ pairs from useCases + verdict const faqs = [ ...(comparison.useCases || []).map(([q, a]) => ({ q: `چه زمانی از ${comparison.item1.name} استفاده کنیم؟`, a: q })).slice(0, 1), ...(comparison.useCases || []).map(([q, a]) => ({ q: `چه زمانی از ${comparison.item2.name} استفاده کنیم؟`, a })).slice(0, 1), { q: `${comparison.item1.name} بهتر است یا ${comparison.item2.name}؟`, a: comparison.verdict || '' }, ].filter((f) => f.a); const jsonLd = JSON.stringify({ '@context': 'https://schema.org', '@graph': [ { '@type': ['Organization', 'NewsMediaOrganization'], '@id': `${SITE_URL}/#organization`, name: 'تکناو', url: SITE_URL, logo: { '@type': 'ImageObject', url: `${SITE_URL}/favicon.png` }, }, { '@type': 'WebPage', '@id': canonical, url: canonical, name: clean(comparison.title), description: clean(comparison.metaDescription || comparison.title), datePublished, inLanguage: 'fa-IR', publisher: { '@id': `${SITE_URL}/#organization` }, isPartOf: { '@id': `${SITE_URL}/#website` }, }, { '@type': 'ItemList', name: clean(comparison.title), description: clean(comparison.metaDescription || comparison.title), numberOfItems: 2, itemListElement: [ { '@type': 'ListItem', position: 1, name: comparison.item1.name, description: comparison.item1.summary, url: `${SITE_URL}/glossary/${comparison.item1.slug}` }, { '@type': 'ListItem', position: 2, name: comparison.item2.name, description: comparison.item2.summary, url: `${SITE_URL}/glossary/${comparison.item2.slug}` }, ], }, faqs.length > 0 ? { '@type': 'FAQPage', mainEntity: faqs.map((f) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a }, })), } : null, { '@type': 'BreadcrumbList', itemListElement: [ { '@type': 'ListItem', position: 1, name: 'تکناو', item: SITE_URL }, { '@type': 'ListItem', position: 2, name: 'مقایسه‌ها', item: `${SITE_URL}/compare` }, { '@type': 'ListItem', position: 3, name: clean(comparison.title) }, ], }, ].filter(Boolean), }); let html = template; html = html.replace(//, ''); html = html.replace(/[^<]*<\/title>/, `<title>${title}`); html = html.replace(/(/, ``); html = html.replace(/`); html = replaceStaticFallback(html, renderStaticFallback({ eyebrow: 'مقایسه فناوری', title: clean(comparison.title), description: clean(comparison.metaDescription || comparison.verdict || comparison.title), facts: [ { label: comparison.item1.name, value: cleanText(comparison.item1.summary) }, { label: comparison.item2.name, value: cleanText(comparison.item2.summary) }, comparison.verdict ? { label: 'جمع‌بندی', value: cleanText(comparison.verdict) } : null, ].filter(Boolean), sections: [ { title: 'مزایا و محدودیت‌ها', text: [ `${comparison.item1.name}: ${(comparison.item1.pros || []).join('، ')}`, `${comparison.item2.name}: ${(comparison.item2.pros || []).join('، ')}`, ].join(' / '), }, ...faqs.map(faq => ({ title: faq.q, text: cleanText(faq.a) })), ], links: [ { href: '/compare', label: 'مقایسه‌ها', text: 'همه مقایسه‌های تکناو' }, { href: '/articles', label: 'مقاله‌های فناوری', text: 'تحلیل‌های بیشتر' }, ], })); return html; } let count = 0; for (const comparison of comparisons) { const dir = resolve(`dist/compare/${comparison.slug}`); mkdirSync(dir, { recursive: true }); writeFileSync(`${dir}/index.html`, buildPage(comparison), 'utf8'); count++; } console.log(`[comparison-pages] generated ${count} comparison pages in dist/compare/`);