dana-medical-site/web/scripts/prerender.mjs
2026-10-14 13:33:13 +00:00

63 lines
3.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[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 = `<title>${escape(m.title)}</title>\n` +
`<meta name="description" content="${escape(m.description)}">\n` +
`<link rel="canonical" href="${escape(m.canonical)}">\n` +
`<meta property="og:type" content="website">\n<meta property="og:locale" content="ru_RU">\n` +
`<meta property="og:title" content="${escape(m.title)}">\n` +
`<meta property="og:description" content="${escape(m.description)}">\n` +
`<meta property="og:url" content="${escape(m.canonical)}">\n` +
(m.image ? `<meta property="og:image" content="${escape(m.image)}">\n` : '');
const document = template.replace(/<title>[\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}`);