@operstack/audit 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/visibility.mjs +34 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operstack/audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "SEO, AEO and GEO audit generator: collect public signals from a site into audit.json, let an analyst or an agent fill the narrative, render the OperStack report as HTML and PDF.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"LICENSE"
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
|
-
"test": "node src/test-scores.mjs && node src/test-fix.mjs && node src/test-foundation.mjs && node src/test-narrative.mjs && node src/test-prompts.mjs && node src/test-sitemap.mjs && node src/test-overall.mjs && node src/test-blocked.mjs && node src/test-unmeasured.mjs && node src/test-rendered.mjs && node src/test-sample.mjs && node src/test-demand.mjs && node src/test-agent-surface.mjs && node src/test-agency.mjs && node src/test-i18n.mjs examples/sample-audit.json && node bin/audit.mjs check examples/sample-audit.json && node bin/audit.mjs render examples/sample-audit.json --out /tmp/operstack-audit-sample.html && node src/test-ru-sources.mjs && node src/test-sample-fairness.mjs && node test-engines.mjs",
|
|
32
|
+
"test": "node src/test-scores.mjs && node src/test-fix.mjs && node src/test-foundation.mjs && node src/test-narrative.mjs && node src/test-prompts.mjs && node src/test-sitemap.mjs && node src/test-overall.mjs && node src/test-blocked.mjs && node src/test-unmeasured.mjs && node src/test-rendered.mjs && node src/test-sample.mjs && node src/test-demand.mjs && node src/test-agent-surface.mjs && node src/test-agency.mjs && node src/test-i18n.mjs examples/sample-audit.json && node bin/audit.mjs check examples/sample-audit.json && node bin/audit.mjs render examples/sample-audit.json --out /tmp/operstack-audit-sample.html && node src/test-ru-sources.mjs && node src/test-sample-fairness.mjs && node test-engines.mjs && node test-seo-basics.mjs",
|
|
33
33
|
"sample": "node bin/audit.mjs render examples/sample-audit.json --pdf",
|
|
34
34
|
"verify:live": "node src/verify-live.mjs"
|
|
35
35
|
},
|
package/src/visibility.mjs
CHANGED
|
@@ -99,6 +99,7 @@ export const MESSAGES = {
|
|
|
99
99
|
rulesUnknown: ' Its robots.txt could not be read either, so there is nothing to say about AI crawlers: that is not counted for or against the site.',
|
|
100
100
|
howToCheckBots: ' To know for certain, open the server log and look for GPTBot, ClaudeBot and PerplexityBot, or ask whoever runs the protection which bots are on the allowlist.',
|
|
101
101
|
area: { access: 'Can AI crawlers read it', index: 'Is there a map for agents (llms.txt)', entity: 'Is the entity clear (schema)', content: 'Is there something to quote', trust: 'Can it be dated and trusted' },
|
|
102
|
+
seo: { title: 'Title of 10 to 70 characters', description: 'Description of 50 to 170 characters', h1: 'An H1 on the page', canonical: 'A canonical address', og: 'An og:title for link previews', indexable: 'Not closed to indexing' },
|
|
102
103
|
llmsUnreadable: (status) => `The site did not let this check read llms.txt${status ? ` (HTTP ${status})` : ''}, so whether the site has a map for agents could not be measured. It is not counted for or against the site: a file we were refused is not the same as a file that is not there.`,
|
|
103
104
|
robotsMissing: 'There is no robots.txt on the site. By the standard that means nothing is disallowed, so every AI crawler may read it. A file is not required; add one only when something needs closing off.',
|
|
104
105
|
sampleShort: (read, wanted) => `Only ${read} of ${wanted} pages answered in time, twice in a row, so there was not enough of the site to score. We do not show a number here: a score that goes up because we read less is a score nobody can defend. The site may simply be slow right now; run the check again in a minute.`,
|
|
@@ -150,6 +151,7 @@ export const MESSAGES = {
|
|
|
150
151
|
rulesUnknown: ' Файл robots.txt прочитать тоже не вышло, поэтому про роботов ИИ сказать нечего, и в плюс или в минус сайту это не зачтено.',
|
|
151
152
|
howToCheckBots: ' Чтобы знать точно, посмотрите в журнале сервера, приходят ли GPTBot, ClaudeBot и PerplexityBot, либо спросите у тех, кто настраивал защиту, кто у неё в белом списке.',
|
|
152
153
|
area: { access: 'Могут ли роботы ИИ прочитать сайт', index: 'Есть ли карта для агентов (llms.txt)', entity: 'Понятно ли, кто вы (разметка)', content: 'Есть ли что процитировать', trust: 'Можно ли датировать и доверять' },
|
|
154
|
+
seo: { title: 'Заголовок от 10 до 70 знаков', description: 'Описание от 50 до 170 знаков', h1: 'На странице есть H1', canonical: 'Указан канонический адрес', og: 'Есть og:title для превью ссылки', indexable: 'Не закрыта от индексации' },
|
|
153
155
|
llmsUnreadable: (status) => `Сайт не дал этой проверке прочитать llms.txt${status ? ` (код ${status})` : ''}, поэтому есть ли у сайта карта для агентов, измерить не вышло. В плюс или в минус это не зачтено: файл, который нам не отдали, это не то же самое, что файла нет.`,
|
|
154
156
|
robotsMissing: 'Файла robots.txt на сайте нет. По стандарту это значит, что не запрещено ничего, то есть читать сайт может любой робот ИИ. Заводить файл необязательно: он нужен, только когда есть что закрывать.',
|
|
155
157
|
sampleShort: (read, wanted) => `Из ${wanted} страниц ответили ${read}, и со второй попытки тоже, поэтому считать балл не по чему. Числа здесь не будет: балл, который растёт оттого, что мы прочитали меньше, защитить нельзя. Возможно, сайт сейчас просто медленный: запустите проверку через минуту.`,
|
|
@@ -402,6 +404,37 @@ export function engineReadiness(verdicts, parts, T, label = (v) => v.label) {
|
|
|
402
404
|
});
|
|
403
405
|
}
|
|
404
406
|
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Основы SEO: второе число рядом с ИИ-видимостью.
|
|
410
|
+
*
|
|
411
|
+
* Простым языком. У большинства сайтов с обычным поиском всё в порядке, а для нейросетей они
|
|
412
|
+
* почти невидимы. Пока на экране одно число, этого разрыва не видно, и человек не понимает,
|
|
413
|
+
* почему ему вообще стоит волноваться. Два числа рядом объясняют это без слов: «для Google
|
|
414
|
+
* сайт в порядке, для ChatGPT нет».
|
|
415
|
+
*
|
|
416
|
+
* Считается ТОЛЬКО из того, что уже прочитано на тех же десяти страницах, ни одного нового
|
|
417
|
+
* запроса: заголовок, описание, H1, canonical, og:title и запрет индексации. Поэтому и
|
|
418
|
+
* называется «основы», а не «SEO»: полный аудит смотрит на скорость, ссылки и позиции, и этого
|
|
419
|
+
* здесь нет. Называть число громче, чем оно есть, это ровно то, за что мы критикуем конкурента.
|
|
420
|
+
*
|
|
421
|
+
* На балл ИИ-видимости не влияет ничем: это отдельное число, оно ни в плюс, ни в минус.
|
|
422
|
+
*/
|
|
423
|
+
export function seoBasics(pages, T) {
|
|
424
|
+
const rules = [
|
|
425
|
+
{ id: 'title', ok: (p) => p.title.length >= 10 && p.title.length <= 70 },
|
|
426
|
+
{ id: 'description', ok: (p) => p.description.length >= 50 && p.description.length <= 170 },
|
|
427
|
+
{ id: 'h1', ok: (p) => p.h1.length > 0 },
|
|
428
|
+
{ id: 'canonical', ok: (p) => p.canonical.length > 0 },
|
|
429
|
+
{ id: 'og', ok: (p) => p.ogTitle },
|
|
430
|
+
{ id: 'indexable', ok: (p) => !/noindex/.test(p.robotsMeta) },
|
|
431
|
+
];
|
|
432
|
+
const checks = rules.map((r) => ({ id: r.id, label: T.seo[r.id], passed: pages.filter(r.ok).length, total: pages.length }));
|
|
433
|
+
const passed = checks.reduce((a, c) => a + c.passed, 0);
|
|
434
|
+
const total = checks.reduce((a, c) => a + c.total, 0);
|
|
435
|
+
return { score: total ? Math.round((passed / total) * 100) : null, checks };
|
|
436
|
+
}
|
|
437
|
+
|
|
405
438
|
async function readSitemap(origin, robotsText, timeoutFn = () => 3000) {
|
|
406
439
|
const fromRobots = [...(robotsText || '').matchAll(/^sitemap:\s*(\S+)/gim)].map((m) => m[1]);
|
|
407
440
|
const candidates = fromRobots.length
|
|
@@ -816,6 +849,7 @@ export async function checkVisibility(input, { budgetMs = 8500, lang = 'en', sam
|
|
|
816
849
|
})),
|
|
817
850
|
sitemap: { found: sitemap.found, count: sitemap.count, lastmod: sitemap.lastmod, unchecked: Boolean(!sitemap.found && (sitemap.timedOut || sitemap.skipped)) },
|
|
818
851
|
crawlers: verdicts.map((v) => ({ label: agentLabel(v), kind: v.kind, verdict: v.verdict })),
|
|
852
|
+
seo: seoBasics(pages, T),
|
|
819
853
|
engines: engineReadiness(verdicts, {
|
|
820
854
|
access: null, // считается внутри по роботам каждого движка
|
|
821
855
|
index: llmsRead ? index / 15 : null,
|