import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = dirname(fileURLToPath(import.meta.url)); const distHtml = join(root, 'dist', 'index.html'); const { origin, routes } = JSON.parse( readFileSync(join(root, 'src', 'seo.routes.json'), 'utf8'), ); const faq = JSON.parse(readFileSync(join(root, 'src', 'faq.json'), 'utf8')); const CHROME_STORE = 'https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk'; const VSCODE_MARKETPLACE = 'https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode'; const REPO = 'https://git.juankibin.space/kibin/LexAI'; const homeDescription = routes.find((r) => r.path === '/')?.description ?? ''; function softwareGraph() { return { '@type': 'SoftwareApplication', name: 'LexAI', applicationCategory: 'BrowserApplication', applicationSubCategory: 'Writing assistance', operatingSystem: 'Chrome, VS Code, Windows, macOS, Linux', url: `${origin}/`, downloadUrl: [CHROME_STORE, VSCODE_MARKETPLACE], license: `${REPO}/src/branch/main/LICENSE`, sameAs: [REPO, CHROME_STORE, VSCODE_MARKETPLACE], offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' }, }; } function jsonLd(route) { const url = route.path === '/' ? `${origin}/` : `${origin}${route.path}`; const graph = [ { '@type': 'WebSite', name: 'LexAI', url: `${origin}/`, description: homeDescription, }, softwareGraph(), ]; if (route.path === '/') { graph.push({ '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.q, acceptedAnswer: { '@type': 'Answer', text: item.a }, })), }); } else { graph.push({ '@type': 'BreadcrumbList', itemListElement: [ { '@type': 'ListItem', position: 1, name: 'Home', item: `${origin}/` }, { '@type': 'ListItem', position: 2, name: route.title, item: url }, ], }); } return JSON.stringify({ '@context': 'https://schema.org', '@graph': graph }); } function apply(html, { title, description, url, robots, ld }) { let out = html; out = out.replace(/[\s\S]*?<\/title>/, `<title>${title}`); out = out.replace( /([\s\S]*?<\/script>/, ``, ); return out; } const template = readFileSync(distHtml, 'utf8'); if (!template.includes('rel="canonical"') || !template.includes('og:title')) { console.error('dist/index.html is missing SEO tags — check index.html'); process.exit(1); } const written = []; for (const route of routes) { const url = route.path === '/' ? `${origin}/` : `${origin}${route.path}`; const file = route.path === '/' ? 'index.html' : `${route.path.replace(/^\//, '')}/index.html`; const dest = join(root, 'dist', file); mkdirSync(dirname(dest), { recursive: true }); writeFileSync( dest, apply(template, { title: route.title, description: route.description, url, robots: 'index, follow', ld: jsonLd(route), }), ); written.push(file); } writeFileSync( join(root, 'dist', '404.html'), apply(template, { title: 'Page not found — LexAI', description: 'That address is not a LexAI page.', url: `${origin}/`, robots: 'noindex, follow', ld: JSON.stringify({ '@context': 'https://schema.org', '@graph': [{ '@type': 'WebSite', name: 'LexAI', url: `${origin}/` }, softwareGraph()], }), }), ); written.push('404.html'); const today = new Date().toISOString().slice(0, 10); const sitemap = ` ${routes .map( (r, i) => ` ${r.path === '/' ? `${origin}/` : `${origin}${r.path}`} ${today} ${i < 2 ? 'weekly' : 'monthly'} ${r.path === '/' ? '1.0' : r.path === '/docs' ? '0.8' : '0.5'} `, ) .join('\n')} `; writeFileSync(join(root, 'dist', 'sitemap.xml'), sitemap); written.push('sitemap.xml'); console.log('SEO HTML written for', written.join(', '));