63 lines
3.8 KiB
JavaScript
63 lines
3.8 KiB
JavaScript
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 = `<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}`);
|