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

56 lines
6.2 KiB
JavaScript
Raw Permalink 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 { DOMParser } from 'linkedom';
const base = (process.env.VERIFY_URL || 'http://127.0.0.1:8080').replace(/\/$/, '');
const profiles = JSON.parse(await fs.readFile('../seo/service-profiles.json', 'utf8'));
async function get(path) {
const response = await fetch(base + path, { signal: AbortSignal.timeout(15000) });
if (!response.ok) throw new Error(`${path}: HTTP ${response.status}`);
return response;
}
const prices = (await (await get('/api/v1/prices')).json()).data;
const flatten = (tree) => (tree || []).flatMap((s) => [s, ...flatten(s.children)]);
const plastic = flatten(prices).find((s) => s.slug === 'plasticheskaya_hirurgiya');
const groups = new Map();
for (const item of plastic?.items || []) {
const key = item.name.toLowerCase().replace(/\s+/g, ' ').trim();
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(item);
}
const ambiguous = [...groups.values()].filter((items) => new Set(items.map((p) => `${p.price}:${p.unit || ''}:${Boolean(p.from)}`)).size > 1);
let md = '# Проверка контента SEO-посадочных\n\n';
md += `Проверено: ${new Date().toISOString()}. Источник: текущие публичные данные проекта. Это редакторский список, не медицинская или юридическая проверка. Новые операции, цены, авторство, гарантии и ответы не публикуются автоматически.\n\n`;
const pages = [];
for (const [path, profile] of Object.entries(profiles)) {
const url = `/services/${path}/`;
const doc = new DOMParser().parseFromString(await (await get(url)).text(), 'text/html');
const initial = JSON.parse(doc.getElementById('page-data').textContent);
const service = initial[`service:${path}`].data;
const headings = [...doc.querySelectorAll('h2')].map((n) => n.textContent.trim());
const content = (service.content_sections || []).map((s) => [s.title, ...s.paragraphs, ...s.items].join(' ')).join(' ');
const topics = [
['Показания', /показан|кому подход/i],
['Ограничения и противопоказания', /противопоказ|ограничени/i],
['Риски и возможные осложнения', /осложнен|риск/i],
['Подготовка', /подготов/i],
['Восстановление и наблюдение', /восстанов|реабилитац|послеоперац/i],
].map(([topic, re]) => ({ topic, mentioned: re.test(content) }));
const record = { path, headings, linkedPrices: service.prices?.length || 0, ownDoctors: service.doctors?.map((d) => ({ name: d.full_name, slug: d.slug })) || [], topics };
pages.push(record);
md += `## ${profile.cluster}\n\nURL: ${url}\n\n`;
md += `Связанных в БД позиций прайса: ${record.linkedPrices}. Врачей, явно связанных с услугой: ${record.ownDoctors.length}. Связь с услугой сама по себе не доказывает авторство текста или право проводить конкретную операцию.\n\n`;
md += `Видимые H2: ${headings.join('; ') || 'нет'}.\n\n`;
md += 'Темы в исходном описании (поиск слов, не оценка полноты/качества):\n\n';
for (const t of topics) md += `- ${t.topic}: ${t.mentioned ? 'упоминание найдено, требуется врачебная проверка' : 'упоминание не найдено, запросить проверенный текст'}.\n`;
md += '\nДо выпуска: согласовать медицинского автора и проверяющего с реальными профилями; дату проверки; источники; фактическое место проведения; состав цены; права на отзывы и фото до/после. FAQ и новое авторство не размечать до утверждения.\n\n';
}
md += '## Прайс: неоднозначные одноимённые позиции\n\n';
md += 'Одинаковые названия с разными суммами могут относиться к врачам, категориям или условиям. Их нельзя считать ошибкой без проверки. Публикация минимальной суммы как цены операции и автоматическая связь с посадочной заблокированы на редакционном уровне: код не делает такого сопоставления. Уже существующий прайс не изменён.\n\n';
for (const items of ambiguous) {
md += `- ${items[0].name}: ${items.map((p) => `ID ${p.id} — ${p.price} ₽${p.unit ? ` / ${p.unit}` : ''}`).join('; ')}.\n`;
}
md += '\nОсобенно проверить блефаропластику верхних век (ID 11390 / 11391: 3 000 / 30 000 ₽) и нижних век (ID 11399: 3 000 ₽). Не исправлять по догадке. Общие ссылки с услуги ведут в прайс направления, без обещания конкретной цены или доступности каждого варианта операции.\n\n';
md += '## Неперенесённые разделы\n\nО клинике: запись pages/about есть, но содержательный текст отсутствует. Новости, галерея, партнёры, надзорные органы, вакансии и FAQ — требуют переноса подтверждённых данных. Пункты подвала сохраняются визуально, но не ведут на 404. Внутренние старые URL по-прежнему возвращают 404, не пустые 200.\n';
await fs.writeFile('../seo/content-gaps.md', md);
await fs.writeFile('../seo/content-audit.json', JSON.stringify({ checkedAt: new Date().toISOString(), pages, ambiguousPrices: ambiguous }, null, 2));
console.log(`Content audit: ${pages.length} landing pages, ${ambiguous.length} ambiguous price groups. See seo/content-gaps.md.`);