import fs from 'node:fs/promises'; import path from 'node:path'; import assert from 'node:assert/strict'; import { DOMParser } from 'linkedom'; const manifest = JSON.parse(await fs.readFile('dist/seo-manifest.json', 'utf8')); const profiles = JSON.parse(await fs.readFile('../seo/service-profiles.json', 'utf8')); globalThis.DOMParser = DOMParser; const { servicePriceSection, priceDirection } = await import('../src/lib/serviceNavigation.js'); assert.equal(servicePriceSection('estetic/rinoplastika', []), undefined); assert.equal(servicePriceSection('estetic/rinoplastika', [{ slug: 'plasticheskaya_hirurgiya', items: [] }]), undefined); assert.equal(servicePriceSection('estetic/rinoplastika', [{ children: [{ slug: 'plasticheskaya_hirurgiya', items: [{ id: 1 }] }] }]).slug, 'plasticheskaya_hirurgiya'); assert.equal(priceDirection('plasticheskaya_hirurgiya', []), undefined); assert.equal(priceDirection('plasticheskaya_hirurgiya', [{ path: 'estetic' }]).path, 'estetic'); const { sanitizeHTML } = await import('../src/lib/sanitize.js'); const { canonicalInternalHref } = await import('../src/lib/links.js'); assert.equal(canonicalInternalHref('/include/licenses_detail.php?utm_source=test#info'), '/legaldocs/privacy/?utm_source=test#info'); assert.equal(canonicalInternalHref('/company/licenses/'), '/company/licenses/'); assert.equal(canonicalInternalHref('/privacy'), '/legaldocs/privacy/'); assert.equal(canonicalInternalHref('https://other.example/privacy'), 'https://other.example/privacy'); const sanitized = sanitizeHTML('

Текст документа

Контактыbad linkbad newline'); assert.ok(sanitized.includes('документа')); assert.ok(sanitized.includes('href="/contacts/"')); assert.ok(!/javascript:|onclick|onerror|onload|Описание процедуры

Показания

' }]); assert.equal(parsed.length, 2); assert.equal(parsed[0].paragraphs[0], 'Описание процедуры'); assert.equal(parsed[1].title, 'Показания'); assert.equal(parsed[1].items[0], 'Первый пункт'); assert.ok(!JSON.stringify(parsed).includes('bad()')); let sections = 0, assets = new Set(); for (const servicePath of manifest.services) { const html = await fs.readFile(path.join('dist/services', servicePath, 'index.html'), 'utf8'); const doc = new DOMParser().parseFromString(html, 'text/html'); for (const selector of ['title', 'meta[name="description"]', 'link[rel="canonical"]', 'h1', '#page-data']) { assert.equal(doc.querySelectorAll(selector).length, 1, `${servicePath}: ${selector} must be unique`); } assert.equal(doc.querySelector('link[rel="canonical"]').getAttribute('href'), `${manifest.siteURL}/services/${servicePath}/`); if (profiles[servicePath]) { assert.equal(doc.querySelector('h1').textContent, profiles[servicePath].h1); assert.equal(doc.querySelector('title').textContent, profiles[servicePath].title); assert.ok([...doc.querySelector('title').textContent].length <= 60); assert.ok([...doc.querySelector('meta[name="description"]').getAttribute('content')].length <= 160); } const data = JSON.parse(doc.querySelector('#page-data').textContent); const service = data[`service:${servicePath}`].data; assert.equal(service.path, servicePath); assert.ok(!Object.keys(data).some((k) => /admin|lead|session/i.test(k)), 'Public HTML must not include private API data'); if (service.content_sections.length && !service.children?.length) { assert.ok(doc.querySelectorAll('.service-section,.service-urgent').length >= service.content_sections.length, `${servicePath}: main text missing before JS`); sections += service.content_sections.length; } if (service.blocks?.some((b) => b.html?.replace(/<[^>]*>/g, '').trim()) && !service.children?.length) { assert.ok(service.content_sections.length > 0, `${servicePath}: nonempty source content must survive prerendering`); } assert.equal(doc.querySelectorAll('script[type="application/ld+json"]').length, 1, `${servicePath}: missing structured data`); for (const el of doc.querySelectorAll('script[type="application/ld+json"]')) { const schema = JSON.parse(el.textContent); assert.equal(schema['@context'], 'https://schema.org'); assert.ok(schema['@graph'].some((n) => n['@type'] === 'BreadcrumbList')); } for (const el of doc.querySelectorAll('[src],link[href]')) { const resource = el.getAttribute('src') || el.getAttribute('href'); if (resource.startsWith('/assets/')) assets.add(resource); } } for (const resource of assets) assert.ok((await fs.stat(path.join('dist', resource))).isFile(), `Missing SSR asset: ${resource}`); assert.ok(sections > 0, 'No source content made it into HTML'); console.log(`SEO HTML verified: ${manifest.services.length} pages, ${sections} content sections before JS, ${assets.size} local assets.`); // Живой рендер получает данные страницы через stdin кусками по 64 КБ. Русская буква на границе // куска не должна превращаться в «��»: пробуем оба сдвига, чтобы граница попала внутрь буквы. { const { spawnSync } = await import('node:child_process'); const template = await fs.readFile('dist/index.html', 'utf8'); for (const pad of ['', 'a']) { const text = pad + 'Субъект персональных данных имеет право. '.repeat(6000); const request = JSON.stringify({ url: '/legaldocs/privacy/', siteURL: manifest.siteURL, template, common: { branches: [], settings: {} }, initial: { 'page:privacy': { data: { id: 1, slug: 'privacy', title: 'Политика', blocks: [{ type: 'html', html: `

${text}

` }], seo: {} } } } }); const run = spawnSync(process.execPath, ['scripts/render-page.mjs'], { input: request, maxBuffer: 64 << 20 }); assert.equal(run.status, 0, run.stderr?.toString()); const html = run.stdout.toString('utf8'); assert.ok(html.includes('имеет право'), 'rendered page has the text'); assert.ok(!html.includes('\uFFFD'), 'multi-byte characters survived stdin chunking'); } console.log('Live renderer keeps UTF-8 intact across 64 KB input chunks.'); } if (process.env.VERIFY_URL) { const base = process.env.VERIFY_URL.replace(/\/$/, ''); for (const [url, status] of [['/services/estetic/rinoplastika/', 200], ['/services/not-a-service/', 404], ['/not-a-page/', 404], ['/robots.txt', 200], ['/sitemap.xml', 200]]) { const response = await fetch(base + url, { redirect: 'manual' }); assert.equal(response.status, status, `${url}: wrong status`); if (url.endsWith('rinoplastika/')) assert.ok((await response.text()).includes('Ринопластика в Махачкале')); if (url === '/sitemap.xml') { const body = await response.text(); const sitemap = new DOMParser().parseFromString(body, 'text/xml'); const locs = [...sitemap.querySelectorAll('loc')].map((n) => n.textContent); assert.equal(new Set(locs).size, locs.length); assert.ok(locs.includes(`${manifest.siteURL}/services/estetic/rinoplastika/`)); assert.ok(!locs.some((l) => l.includes('not-a-service'))); } } const redirect = await fetch(base + '/services/estetic/rinoplastika?utm_source=test', { redirect: 'manual' }); assert.equal(redirect.status, 301); assert.equal(redirect.headers.get('location'), '/services/estetic/rinoplastika/?utm_source=test'); console.log('Live Go routes verified: HTML, 301, 404, robots.txt, sitemap.xml.'); }