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

64 lines
3.6 KiB
JavaScript

import fs from 'node:fs/promises';
import { DOMParser } from 'linkedom';
const base = (process.env.VERIFY_URL || 'http://127.0.0.1:8080').replace(/\/$/, '');
const fetchPage = async (path, method = 'GET') => {
const response = await fetch(base + path, { method, redirect: 'manual', signal: AbortSignal.timeout(15000) });
return { status: response.status, location: response.headers.get('location'), text: method === 'GET' ? await response.text() : '' };
};
async function pool(values, work) {
let next = 0;
await Promise.all(Array.from({ length: 2 }, async () => {
while (next < values.length) await work(values[next++]);
}));
}
const sitemap = new DOMParser().parseFromString((await fetchPage('/sitemap.xml')).text, 'text/xml');
const paths = [...sitemap.querySelectorAll('loc')].map((n) => new URL(n.textContent).pathname);
const canonicalOrigin = new URL(sitemap.querySelector('loc').textContent).origin;
const documents = new Map(), links = new Map(), media = new Map();
const add = (map, url, source) => {
if (!map.has(url)) map.set(url, new Set());
map.get(url).add(source);
};
await pool(paths, async (path) => {
const result = await fetchPage(path);
documents.set(path, result);
if (result.status !== 200) return;
const doc = new DOMParser().parseFromString(result.text, 'text/html');
for (const el of doc.querySelectorAll('a[href]')) {
const raw = el.getAttribute('href');
let url; try { url = new URL(raw, canonicalOrigin + path); } catch { continue; }
if (![canonicalOrigin, base, 'https://danamedical.ru', 'http://danamedical.ru'].includes(url.origin)) continue;
if (/^\/(api|admin)(\/|$)/.test(url.pathname)) continue;
add(links, url.pathname + url.hash, path);
}
for (const el of doc.querySelectorAll('img[src],script[src],link[rel="stylesheet"][href]')) {
const raw = el.getAttribute('src') || el.getAttribute('href');
const url = new URL(raw, canonicalOrigin + path);
if ([canonicalOrigin, base].includes(url.origin)) add(media, url.pathname, path);
}
});
const issues = [], redirects = [];
await pool([...links.keys()], async (link) => {
const url = new URL(link, base), path = url.pathname;
let result = documents.get(path);
if (!result) { result = await fetchPage(path); documents.set(path, result); }
if (result.status >= 300 && result.status < 400) redirects.push({ url: link, to: result.location, sources: [...links.get(link)] });
else if (result.status !== 200) issues.push({ type: 'link', url: link, status: result.status, sources: [...links.get(link)] });
else if (url.hash) {
const doc = new DOMParser().parseFromString(result.text, 'text/html');
const anchor = decodeURIComponent(url.hash.slice(1));
if (!doc.getElementById(anchor) && ![...doc.querySelectorAll('a[name]')].some((el) => el.getAttribute('name') === anchor)) {
issues.push({ type: 'anchor', url: link, status: 200, sources: [...links.get(link)] });
}
}
});
await pool([...media.keys()], async (url) => {
const result = await fetchPage(url, 'HEAD');
if (result.status !== 200) issues.push({ type: 'asset', url, status: result.status, sources: [...media.get(url)] });
});
const report = { checkedAt: new Date().toISOString(), base, pages: paths.length, internalLinks: links.size, assets: media.size, issues, redirects };
await fs.writeFile('../seo/crawl-audit.json', JSON.stringify(report, null, 2));
console.log(JSON.stringify({ pages: report.pages, internalLinks: report.internalLinks, assets: report.assets, issues: issues.map(({ type, url, status }) => ({ type, url, status })), redirects: redirects.map(({ url, to }) => ({ url, to })) }, null, 2));
if (process.env.STRICT_AUDIT === '1' && issues.length) process.exitCode = 1;