import fs from 'node:fs/promises'; import path from 'node:path'; import { DOMParser } from 'linkedom'; globalThis.DOMParser = DOMParser; const { renderService } = await import('../.ssr/entry-server.js'); const apiURL = (process.env.DANA_API || 'http://127.0.0.1:8080').replace(/\/$/, ''); const siteURL = (process.env.SITE_URL || 'http://127.0.0.1:8080').replace(/\/$/, ''); const origin = new URL(siteURL); if (!['http:', 'https:'].includes(origin.protocol) || origin.pathname !== '/' || origin.search || origin.hash) { throw new Error('SITE_URL must be an HTTP(S) origin, for example https://danamedical.ru'); } const output = path.resolve('dist'); const template = await fs.readFile(path.join(output, 'index.html'), 'utf8'); async function get(endpoint) { const response = await fetch(`${apiURL}/api/v1${endpoint}`, { signal: AbortSignal.timeout(30000) }); if (!response.ok) throw new Error(`Prerender failed: ${endpoint} HTTP ${response.status}`); return (await response.json()).data; } function flatten(tree) { return tree.flatMap((s) => [s, ...flatten(s.children || [])]); } function escape(text) { return String(text).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); } const [tree, branches, settings, specialties, doctors, prices] = await Promise.all([ get('/services'), get('/branches'), get('/settings'), get('/specialties'), get('/doctors'), get('/prices'), ]); const first = branches[0]; const coords = branches.find((b) => b.lat && b.lon); const site = { phone: first?.phones?.[0] || '+7 928 962-00-94', hours: first?.work_hours?.[0] || 'Пн–Вс: 08:00–20:00', addresses: branches.map((b) => b.address.replace(/^г\.\s*/, '').replace(/(\d)\(/g, '$1 (')), branches, coords: coords ? { lat: coords.lat, lon: coords.lon } : null, maxUrl: settings.max_url || '', email: settings.email || '', legal: settings.legal || null, }; const services = flatten(tree); const manifest = { siteURL, services: [] }; for (const summary of services) { if (!/^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/.test(summary.path)) throw new Error(`Unsafe service path: ${summary.path}`); const service = await get(`/services/${summary.path}`); const parent = services.find((s) => s.id === service.parent_id); const rendered = renderService(service, parent, { site, specialties, doctors, prices }, siteURL); const m = rendered.metadata; const head = `${escape(m.title)}\n` + `\n` + `\n` + `\n\n` + `\n` + `\n` + `\n` + (m.image ? `\n` : ''); const document = template.replace(/[\s\S]*?<\/title>/, '') .replace(/<meta name="description"[^>]*>/, '') .replace('</head>', `${head}</head>`) .replace('<div id="root"></div>', `<div id="root">${rendered.html}</div>\n<script id="page-data" type="application/json">${rendered.initial}</script>`); if (!document.includes('<h1') || document.includes('service-loading')) throw new Error(`Empty service HTML: ${summary.path}`); const directory = path.join(output, 'services', summary.path); await fs.mkdir(directory, { recursive: true }); await fs.writeFile(path.join(directory, 'index.html'), document); manifest.services.push(summary.path); } await fs.writeFile(path.join(output, 'seo-manifest.json'), JSON.stringify(manifest, null, 2)); console.log(`Prerendered ${manifest.services.length} service pages using the existing React design. Canonical origin: ${siteURL}`);