seo 0.1.0 → 0.2.1
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/PRIVACY.md +2 -2
- package/README.md +138 -42
- package/TERMS.md +2 -2
- package/TRADEMARKS.md +9 -5
- package/dist/cli.js +69 -52
- package/dist/cli.js.map +1 -1
- package/dist/{dist-CSY2_p-j.js → dist-CTBhWxn6.js} +49 -49
- package/dist/dist-CTBhWxn6.js.map +1 -0
- package/dist/{dist-FeAUNn9A.js → dist-eiewVfiS.js} +1 -1
- package/dist/index.d.ts +186 -94
- package/dist/index.js +843 -415
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +18 -1
- package/dist/mcp.js +6532 -4665
- package/dist/mcp.js.map +1 -1
- package/dist/{package-uzXzqDW6.js → package-BTOI9tde.js} +2 -2
- package/dist/{package-uzXzqDW6.js.map → package-BTOI9tde.js.map} +1 -1
- package/evals/README.md +117 -0
- package/evals/affected-urls.json +57 -0
- package/evals/ai-readiness.json +84 -0
- package/evals/ai-referrals.json +71 -0
- package/evals/ai-search-scorecard.json +70 -0
- package/evals/audit-page.json +57 -0
- package/evals/audit-urls.json +58 -0
- package/evals/cannibalisation.json +57 -0
- package/evals/community-intent.json +57 -0
- package/evals/compare-crawls.json +56 -0
- package/evals/content-optimization.json +57 -0
- package/evals/crawl-diff.json +58 -0
- package/evals/crawl-history.json +44 -0
- package/evals/crawl-report.json +44 -0
- package/evals/crawler-rules.json +44 -0
- package/evals/ctr-underperformers.json +57 -0
- package/evals/decaying-pages.json +57 -0
- package/evals/entity-readiness.json +57 -0
- package/evals/explain-crawl-issue.json +44 -0
- package/evals/generate-llms-txt.json +44 -0
- package/evals/geo-gaps.json +57 -0
- package/evals/index-coverage-plan.json +58 -0
- package/evals/index-coverage.json +70 -0
- package/evals/index-monitor.json +58 -0
- package/evals/index-watch.json +68 -0
- package/evals/internal-links.json +57 -0
- package/evals/link-recovery.json +58 -0
- package/evals/llms-txt-audit.json +44 -0
- package/evals/measure-change.json +58 -0
- package/evals/monthly-action-plan.json +56 -0
- package/evals/monthly-report.json +58 -0
- package/evals/narrative-report.json +58 -0
- package/evals/okf-build.json +44 -0
- package/evals/okf-validate.json +44 -0
- package/evals/page-opportunities.json +57 -0
- package/evals/performance-audit.json +57 -0
- package/evals/pseo-audit.json +84 -0
- package/evals/query-clusters.json +57 -0
- package/evals/quick-wins.json +86 -0
- package/evals/redirect-trace.json +57 -0
- package/evals/refresh-priorities.json +57 -0
- package/evals/search-performance-overview.json +86 -0
- package/evals/second-page.json +58 -0
- package/evals/segment-impact.json +57 -0
- package/evals/seo-fix-queue.json +85 -0
- package/evals/seo-geo-readiness.json +84 -0
- package/evals/seo-site-audit.json +85 -0
- package/evals/seo-to-ai-query.json +57 -0
- package/evals/seo.json +87 -0
- package/evals/setup-check.json +56 -0
- package/evals/site-crawl.json +44 -0
- package/evals/striking-distance.json +85 -0
- package/evals/technical-watch.json +57 -0
- package/evals/top-fixes.json +44 -0
- package/evals/traffic-anomaly.json +57 -0
- package/evals/update-correlation.json +57 -0
- package/evals/update-postmortem.json +57 -0
- package/package.json +5 -3
- package/skills/README.md +50 -61
- package/skills/seo/SKILL.md +88 -63
- package/skills/seo/agents/openai.yaml +2 -2
- package/dist/dist-CSY2_p-j.js.map +0 -1
- package/skills/affected-urls/SKILL.md +0 -39
- package/skills/affected-urls/agents/openai.yaml +0 -4
- package/skills/ai-readiness/SKILL.md +0 -34
- package/skills/ai-readiness/agents/openai.yaml +0 -4
- package/skills/ai-referrals/SKILL.md +0 -31
- package/skills/ai-referrals/agents/openai.yaml +0 -4
- package/skills/audit-page/SKILL.md +0 -50
- package/skills/audit-page/agents/openai.yaml +0 -4
- package/skills/audit-urls/SKILL.md +0 -39
- package/skills/audit-urls/agents/openai.yaml +0 -4
- package/skills/cannibalisation/SKILL.md +0 -31
- package/skills/cannibalisation/agents/openai.yaml +0 -4
- package/skills/community-intent/SKILL.md +0 -39
- package/skills/community-intent/agents/openai.yaml +0 -4
- package/skills/compare-crawls/SKILL.md +0 -40
- package/skills/compare-crawls/agents/openai.yaml +0 -4
- package/skills/content-optimization/SKILL.md +0 -31
- package/skills/content-optimization/agents/openai.yaml +0 -4
- package/skills/crawl-diff/SKILL.md +0 -51
- package/skills/crawl-diff/agents/openai.yaml +0 -4
- package/skills/crawl-history/SKILL.md +0 -37
- package/skills/crawl-history/agents/openai.yaml +0 -4
- package/skills/crawl-report/SKILL.md +0 -37
- package/skills/crawl-report/agents/openai.yaml +0 -4
- package/skills/crawler-rules/SKILL.md +0 -34
- package/skills/crawler-rules/agents/openai.yaml +0 -4
- package/skills/ctr-underperformers/SKILL.md +0 -31
- package/skills/ctr-underperformers/agents/openai.yaml +0 -4
- package/skills/decaying-pages/SKILL.md +0 -31
- package/skills/decaying-pages/agents/openai.yaml +0 -4
- package/skills/entity-readiness/SKILL.md +0 -34
- package/skills/entity-readiness/agents/openai.yaml +0 -4
- package/skills/explain-crawl-issue/SKILL.md +0 -32
- package/skills/explain-crawl-issue/agents/openai.yaml +0 -4
- package/skills/generate-llms-txt/SKILL.md +0 -37
- package/skills/generate-llms-txt/agents/openai.yaml +0 -4
- package/skills/geo-gaps/SKILL.md +0 -37
- package/skills/geo-gaps/agents/openai.yaml +0 -4
- package/skills/index-coverage/SKILL.md +0 -84
- package/skills/index-coverage/agents/openai.yaml +0 -4
- package/skills/index-coverage-plan/SKILL.md +0 -50
- package/skills/index-coverage-plan/agents/openai.yaml +0 -4
- package/skills/index-monitor/SKILL.md +0 -49
- package/skills/index-monitor/agents/openai.yaml +0 -4
- package/skills/index-watch/SKILL.md +0 -77
- package/skills/index-watch/agents/openai.yaml +0 -4
- package/skills/internal-links/SKILL.md +0 -31
- package/skills/internal-links/agents/openai.yaml +0 -4
- package/skills/link-recovery/SKILL.md +0 -51
- package/skills/link-recovery/agents/openai.yaml +0 -4
- package/skills/llms-txt-audit/SKILL.md +0 -34
- package/skills/llms-txt-audit/agents/openai.yaml +0 -4
- package/skills/measure-change/SKILL.md +0 -52
- package/skills/measure-change/agents/openai.yaml +0 -4
- package/skills/monthly-action-plan/SKILL.md +0 -50
- package/skills/monthly-action-plan/agents/openai.yaml +0 -4
- package/skills/monthly-report/SKILL.md +0 -50
- package/skills/monthly-report/agents/openai.yaml +0 -4
- package/skills/narrative-report/SKILL.md +0 -51
- package/skills/narrative-report/agents/openai.yaml +0 -4
- package/skills/okf-build/SKILL.md +0 -39
- package/skills/okf-build/agents/openai.yaml +0 -4
- package/skills/okf-validate/SKILL.md +0 -43
- package/skills/okf-validate/agents/openai.yaml +0 -4
- package/skills/page-opportunities/SKILL.md +0 -31
- package/skills/page-opportunities/agents/openai.yaml +0 -4
- package/skills/performance/SKILL.md +0 -50
- package/skills/performance/agents/openai.yaml +0 -4
- package/skills/performance-audit/SKILL.md +0 -31
- package/skills/performance-audit/agents/openai.yaml +0 -4
- package/skills/pseo-audit/SKILL.md +0 -103
- package/skills/pseo-audit/agents/openai.yaml +0 -4
- package/skills/query-clusters/SKILL.md +0 -31
- package/skills/query-clusters/agents/openai.yaml +0 -4
- package/skills/quick-wins/SKILL.md +0 -31
- package/skills/quick-wins/agents/openai.yaml +0 -4
- package/skills/redirect-trace/SKILL.md +0 -51
- package/skills/redirect-trace/agents/openai.yaml +0 -4
- package/skills/refresh-priorities/SKILL.md +0 -48
- package/skills/refresh-priorities/agents/openai.yaml +0 -4
- package/skills/search-performance-overview/SKILL.md +0 -48
- package/skills/search-performance-overview/agents/openai.yaml +0 -4
- package/skills/second-page/SKILL.md +0 -50
- package/skills/second-page/agents/openai.yaml +0 -4
- package/skills/segment-impact/SKILL.md +0 -50
- package/skills/segment-impact/agents/openai.yaml +0 -4
- package/skills/seo-fix-queue/SKILL.md +0 -50
- package/skills/seo-fix-queue/agents/openai.yaml +0 -4
- package/skills/seo-geo-readiness/SKILL.md +0 -60
- package/skills/seo-geo-readiness/agents/openai.yaml +0 -4
- package/skills/seo-site-audit/SKILL.md +0 -59
- package/skills/seo-site-audit/agents/openai.yaml +0 -4
- package/skills/seo-to-ai-query/SKILL.md +0 -31
- package/skills/seo-to-ai-query/agents/openai.yaml +0 -4
- package/skills/setup-check/SKILL.md +0 -49
- package/skills/setup-check/agents/openai.yaml +0 -4
- package/skills/site-crawl/SKILL.md +0 -39
- package/skills/site-crawl/agents/openai.yaml +0 -4
- package/skills/striking-distance/SKILL.md +0 -52
- package/skills/striking-distance/agents/openai.yaml +0 -4
- package/skills/technical-watch/SKILL.md +0 -49
- package/skills/technical-watch/agents/openai.yaml +0 -4
- package/skills/top-fixes/SKILL.md +0 -38
- package/skills/top-fixes/agents/openai.yaml +0 -4
- package/skills/traffic-anomaly/SKILL.md +0 -52
- package/skills/traffic-anomaly/agents/openai.yaml +0 -4
- package/skills/update-correlation/SKILL.md +0 -51
- package/skills/update-correlation/agents/openai.yaml +0 -4
- package/skills/update-postmortem/SKILL.md +0 -49
- package/skills/update-postmortem/agents/openai.yaml +0 -4
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
|
|
2
|
-
import { n as load, t as version } from "./package-
|
|
2
|
+
import { n as load, t as version } from "./package-BTOI9tde.js";
|
|
3
3
|
import { Headers, fetch as fetch$1 } from "undici";
|
|
4
4
|
import { chmodSync, createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
@@ -888,7 +888,7 @@ function oauthCallbackPage() {
|
|
|
888
888
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
889
889
|
<meta name="color-scheme" content="light dark">
|
|
890
890
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'">
|
|
891
|
-
<title>
|
|
891
|
+
<title>seo is connected</title>
|
|
892
892
|
<style>
|
|
893
893
|
:root {
|
|
894
894
|
color-scheme: light dark;
|
|
@@ -1027,7 +1027,7 @@ function oauthCallbackPage() {
|
|
|
1027
1027
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
1028
1028
|
<path fill="currentColor" d="M12,8c-2.2,0-4,1.8-4,4s1.8,4,4,4s4-1.8,4-4C16,9.8,14.2,8,12,8z M3,9L3,9c0.6,0,1-0.4,1-1V5c0-0.6,0.4-1,1-1h3 c0.6,0,1-0.4,1-1S8.6,2,8,2H5C3.3,2,2,3.3,2,5v3C2,8.6,2.4,9,3,9z M8,20H5c-0.6,0-1-0.4-1-1v-3c0-0.6-0.4-1-1-1s-1,0.4-1,1v3 c0,1.7,1.3,3,3,3h3c0.6,0,1-0.4,1-1S8.6,20,8,20z M21,15L21,15c-0.6,0-1,0.4-1,1v3c0,0.6-0.4,1-1,1h-3c-0.6,0-1,0.4-1,1s0.4,1,1,1h3 c1.7,0,3-1.3,3-3v-3C22,15.4,21.6,15,21,15z M19,2h-3c-0.6,0-1,0.4-1,1s0.4,1,1,1h3c0.6,0,1,0.4,1,1v3c0,0.6,0.4,1,1,1s1-0.4,1-1V5 C22,3.3,20.7,2,19,2z" />
|
|
1029
1029
|
</svg>
|
|
1030
|
-
|
|
1030
|
+
seo
|
|
1031
1031
|
</span>
|
|
1032
1032
|
</div>
|
|
1033
1033
|
</header>
|
|
@@ -1036,7 +1036,7 @@ function oauthCallbackPage() {
|
|
|
1036
1036
|
<div class="frame content">
|
|
1037
1037
|
<p class="status"><span class="status-mark" aria-hidden="true"></span>Google account connected</p>
|
|
1038
1038
|
<h1>You can start your first SEO report.</h1>
|
|
1039
|
-
<p class="lead">Go back to your terminal to continue
|
|
1039
|
+
<p class="lead">Go back to your terminal to continue, and the CLI will help you choose a site and save a project profile before future reports.</p>
|
|
1040
1040
|
</div>
|
|
1041
1041
|
</section>
|
|
1042
1042
|
<div class="frame details">
|
|
@@ -1046,7 +1046,7 @@ function oauthCallbackPage() {
|
|
|
1046
1046
|
</section>
|
|
1047
1047
|
<section class="detail">
|
|
1048
1048
|
<h2>Read-only access</h2>
|
|
1049
|
-
<p>
|
|
1049
|
+
<p>The seo CLI can read the Google data you choose. It cannot change your site or Google account.</p>
|
|
1050
1050
|
</section>
|
|
1051
1051
|
</div>
|
|
1052
1052
|
</main>
|
|
@@ -6644,7 +6644,7 @@ function matchesFilters(issue, filters) {
|
|
|
6644
6644
|
}
|
|
6645
6645
|
//#endregion
|
|
6646
6646
|
//#region packages/core/src/analyze/crawler/ai-readiness.ts
|
|
6647
|
-
function pct$
|
|
6647
|
+
function pct$3(part, total) {
|
|
6648
6648
|
if (total <= 0) return 0;
|
|
6649
6649
|
return Math.round(part / total * 100);
|
|
6650
6650
|
}
|
|
@@ -6674,14 +6674,14 @@ function check(input) {
|
|
|
6674
6674
|
status: evaluated ? status(score, maxScore) : "unknown"
|
|
6675
6675
|
};
|
|
6676
6676
|
}
|
|
6677
|
-
function indexablePages(report) {
|
|
6677
|
+
function indexablePages$1(report) {
|
|
6678
6678
|
return report.pages.filter((page) => page.indexable && page.status < 400);
|
|
6679
6679
|
}
|
|
6680
|
-
function sampleUrls(pages, predicate) {
|
|
6680
|
+
function sampleUrls$1(pages, predicate) {
|
|
6681
6681
|
return pages.filter(predicate).slice(0, 10).map((page) => page.finalUrl);
|
|
6682
6682
|
}
|
|
6683
6683
|
function aiReadiness(report) {
|
|
6684
|
-
const pages = indexablePages(report);
|
|
6684
|
+
const pages = indexablePages$1(report);
|
|
6685
6685
|
const pageCount = pages.length;
|
|
6686
6686
|
const hasIndexablePages = pageCount > 0;
|
|
6687
6687
|
const pageLimitReached = report.summary.pageLimitReached;
|
|
@@ -6721,7 +6721,7 @@ function aiReadiness(report) {
|
|
|
6721
6721
|
section: "agent-access",
|
|
6722
6722
|
maxScore: 20,
|
|
6723
6723
|
evaluated: botAccessEvaluated,
|
|
6724
|
-
score: robotsUnavailable ? 0 : botAccess.length ? blockedBots.length ? scoreFromPercent(pct$
|
|
6724
|
+
score: robotsUnavailable ? 0 : botAccess.length ? blockedBots.length ? scoreFromPercent(pct$3(botAccess.length - blockedBots.length, botAccess.length), 20) : 20 : 8,
|
|
6725
6725
|
title: robotsUnavailable ? "robots.txt crawler-policy evidence is unavailable" : !hasBotAccessEvidence ? "robots.txt crawler-policy evidence was not collected" : blockedBots.length ? "robots.txt blocks selected crawler tokens at the start URL" : "robots.txt allows selected crawler tokens at the start URL",
|
|
6726
6726
|
plainEnglish: robotsUnavailable ? "The robots.txt request failed, so this report cannot say whether selected crawler tokens are allowed at the start URL." : !hasBotAccessEvidence ? "This crawl does not include per-bot robots.txt policy data for the start URL." : blockedBots.length ? `robots.txt policy blocks ${blockedBots.length} selected crawler ${blockedBots.length === 1 ? "token" : "tokens"} at the start URL.` : "robots.txt policy does not block the selected crawler tokens at the start URL. This does not verify actual or site-wide fetchability.",
|
|
6727
6727
|
action: robotsUnavailable ? "Restore a stable robots.txt response, then rerun this report before making crawler-policy claims." : !hasBotAccessEvidence ? "Rerun the crawl with top-level per-bot robots.txt evidence before making crawler-policy claims." : blockedBots.length ? "Only block AI crawlers intentionally. If discovery matters, remove accidental Disallow rules for the blocked user agents." : "Keep robots.txt explicit and intentional so humans and agents can see what is allowed.",
|
|
@@ -6790,9 +6790,9 @@ function aiReadiness(report) {
|
|
|
6790
6790
|
evaluated: hasIndexablePages,
|
|
6791
6791
|
score: 0,
|
|
6792
6792
|
title: "Short opening paragraphs are an unscored observation",
|
|
6793
|
-
plainEnglish: hasIndexablePages ? `${pct$
|
|
6793
|
+
plainEnglish: hasIndexablePages ? `${pct$3(answerablePages.length, pageCount)}% of indexable pages have at least one 25-word paragraph near the start. This does not establish content quality, citation likelihood, or Google AI eligibility.` : "No indexable 2xx pages were available, so opening-paragraph coverage was not evaluated.",
|
|
6794
6794
|
action: "Write for the reader and the page intent. Do not create artificial answer blocks or chunk content solely for AI Search.",
|
|
6795
|
-
urls: sampleUrls(pages, (page) => !page.geo?.answerable),
|
|
6795
|
+
urls: sampleUrls$1(pages, (page) => !page.geo?.answerable),
|
|
6796
6796
|
evidence: {
|
|
6797
6797
|
observedPages: answerablePages.length,
|
|
6798
6798
|
evaluatedPages: pageCount,
|
|
@@ -6808,7 +6808,7 @@ function aiReadiness(report) {
|
|
|
6808
6808
|
evaluated: hasIndexablePages,
|
|
6809
6809
|
score: 0,
|
|
6810
6810
|
title: "Semantic HTML coverage observed",
|
|
6811
|
-
plainEnglish: hasIndexablePages ? `${pct$
|
|
6811
|
+
plainEnglish: hasIndexablePages ? `${pct$3(semanticPages.length, pageCount)}% of indexable pages use semantic structure.` : "No indexable 2xx pages were available, so semantic HTML coverage was not evaluated.",
|
|
6812
6812
|
action: "Use real headings, lists, tables, nav, main, article, and section elements instead of layout-only divs."
|
|
6813
6813
|
}),
|
|
6814
6814
|
check({
|
|
@@ -6818,7 +6818,7 @@ function aiReadiness(report) {
|
|
|
6818
6818
|
evaluated: hasIndexablePages,
|
|
6819
6819
|
score: 0,
|
|
6820
6820
|
title: "Title, H1, and description coverage observed",
|
|
6821
|
-
plainEnglish: hasIndexablePages ? `${pct$
|
|
6821
|
+
plainEnglish: hasIndexablePages ? `${pct$3(titlePages.length, pageCount)}% of indexable pages have title/H1 coverage and ${pct$3(metaPages.length, pageCount)}% have meta descriptions.` : "No indexable 2xx pages were available, so title, H1, and description coverage was not evaluated.",
|
|
6822
6822
|
action: "Write a unique title and descriptive H1. Add a useful meta description when a summary would help searchers understand the page."
|
|
6823
6823
|
}),
|
|
6824
6824
|
check({
|
|
@@ -6828,7 +6828,7 @@ function aiReadiness(report) {
|
|
|
6828
6828
|
evaluated: hasIndexablePages,
|
|
6829
6829
|
score: 0,
|
|
6830
6830
|
title: "HTML language declaration coverage observed",
|
|
6831
|
-
plainEnglish: hasIndexablePages ? `${pct$
|
|
6831
|
+
plainEnglish: hasIndexablePages ? `${pct$3(langPages.length, pageCount)}% of indexable pages declare a language.` : "No indexable 2xx pages were available, so language declaration coverage was not evaluated.",
|
|
6832
6832
|
action: "Set the html lang attribute so crawlers and assistive tech understand the page language."
|
|
6833
6833
|
})
|
|
6834
6834
|
]),
|
|
@@ -6839,9 +6839,9 @@ function aiReadiness(report) {
|
|
|
6839
6839
|
evaluated: hasIndexablePages,
|
|
6840
6840
|
score: 0,
|
|
6841
6841
|
title: "Entity-signal coverage observed",
|
|
6842
|
-
plainEnglish: hasIndexablePages ? `${pct$
|
|
6842
|
+
plainEnglish: hasIndexablePages ? `${pct$3(entityPages.length, pageCount)}% of indexable pages include entity signals such as Organization, Person, Product, sameAs, or social profile links.` : "No indexable 2xx pages were available, so entity-signal coverage was not evaluated.",
|
|
6843
6843
|
action: "Make the homepage and key pages explicit about the brand, product, people, and official profiles using accurate schema and links.",
|
|
6844
|
-
urls: sampleUrls(pages, (page) => !entityPages.includes(page))
|
|
6844
|
+
urls: sampleUrls$1(pages, (page) => !entityPages.includes(page))
|
|
6845
6845
|
}), check({
|
|
6846
6846
|
id: "authors-dates",
|
|
6847
6847
|
section: "entity-signals",
|
|
@@ -6883,9 +6883,9 @@ function aiReadiness(report) {
|
|
|
6883
6883
|
section: "technical-ux",
|
|
6884
6884
|
maxScore: 12,
|
|
6885
6885
|
evaluated: report.pages.length > 0,
|
|
6886
|
-
score: scoreFromPercent(pct$
|
|
6886
|
+
score: scoreFromPercent(pct$3(pages.length, report.pages.length || 1), 12),
|
|
6887
6887
|
title: report.pages.length ? "Crawled-page indexability observed" : "Crawled-page indexability was not evaluated",
|
|
6888
|
-
plainEnglish: report.pages.length ? `${pct$
|
|
6888
|
+
plainEnglish: report.pages.length ? `${pct$3(pages.length, report.pages.length)}% of crawled pages are indexable 2xx pages.` : "No crawled pages were available, so status and indexability were not evaluated.",
|
|
6889
6889
|
action: "Fix 4xx/5xx pages, accidental noindex, and blocked internal URLs before expanding AI discovery work."
|
|
6890
6890
|
})
|
|
6891
6891
|
]),
|
|
@@ -6907,35 +6907,815 @@ function aiReadiness(report) {
|
|
|
6907
6907
|
action: "Link important pages from navigational, hub, or contextual links so crawlers can find them naturally."
|
|
6908
6908
|
})])
|
|
6909
6909
|
];
|
|
6910
|
-
const checks = sections.flatMap((item) => item.checks);
|
|
6911
|
-
const dataStatus = report.status === "completed" && !robotsEvidenceIncomplete ? "complete" : "partial";
|
|
6912
|
-
const topActions = checks.filter((item) => item.evaluated && (item.status === "warning" || item.status === "fail") && [
|
|
6913
|
-
"robots-ai-bots",
|
|
6914
|
-
"valid-json-ld",
|
|
6915
|
-
"status-indexability",
|
|
6916
|
-
"crawl-depth"
|
|
6917
|
-
].includes(item.id)).sort((a, b) => Number(b.status === "fail") - Number(a.status === "fail") || a.id.localeCompare(b.id)).slice(0, 8);
|
|
6910
|
+
const checks = sections.flatMap((item) => item.checks);
|
|
6911
|
+
const dataStatus = report.status === "completed" && !robotsEvidenceIncomplete ? "complete" : "partial";
|
|
6912
|
+
const topActions = checks.filter((item) => item.evaluated && (item.status === "warning" || item.status === "fail") && [
|
|
6913
|
+
"robots-ai-bots",
|
|
6914
|
+
"valid-json-ld",
|
|
6915
|
+
"status-indexability",
|
|
6916
|
+
"crawl-depth"
|
|
6917
|
+
].includes(item.id)).sort((a, b) => Number(b.status === "fail") - Number(a.status === "fail") || a.id.localeCompare(b.id)).slice(0, 8);
|
|
6918
|
+
return {
|
|
6919
|
+
reportId: report.id,
|
|
6920
|
+
url: report.config.url,
|
|
6921
|
+
generatedAt: report.generatedAt,
|
|
6922
|
+
dataStatus,
|
|
6923
|
+
assessment: "evidence-only",
|
|
6924
|
+
headline: dataStatus === "partial" ? "AI-search evidence is incomplete. Fix the collection gaps before drawing site-wide conclusions." : topActions.length ? `${topActions.length} evidence-backed technical ${topActions.length === 1 ? "action remains" : "actions remain"}. Optional observations are not treated as ranking or citation factors.` : "No evidence-backed technical action was found in this crawl. This is not a visibility or citation verdict.",
|
|
6925
|
+
sections,
|
|
6926
|
+
checks,
|
|
6927
|
+
topActions,
|
|
6928
|
+
botAccess,
|
|
6929
|
+
agentResources: report.ai?.agentResources ?? [],
|
|
6930
|
+
caveats: [
|
|
6931
|
+
...report.caveats,
|
|
6932
|
+
"This report deliberately has no aggregate readiness score. Google documents no separate technical requirements for its generative AI Search features.",
|
|
6933
|
+
"llms.txt is treated as optional agent-discovery metadata, not a Google Search ranking or visibility factor.",
|
|
6934
|
+
"Paragraph length and placement are observations only. Google does not require content chunking or special answer blocks for generative AI features.",
|
|
6935
|
+
"Snippet controls reflect page-level publisher directives only. No detected restriction does not guarantee selection, visibility, or a displayed snippet.",
|
|
6936
|
+
...robotsTxt ? robotsUnavailable ? ["robots.txt was unavailable, so crawler access and sitemap declaration checks are inconclusive."] : !hasBotAccessEvidence ? ["robots.txt was collected without per-bot policy evidence, so the crawler-policy check is inconclusive."] : [] : ["This crawl report does not include top-level robots.txt AI bot data."],
|
|
6937
|
+
"robots.txt checks describe policy for the configured start URL only. They do not verify actual crawler requests or site-wide access."
|
|
6938
|
+
]
|
|
6939
|
+
};
|
|
6940
|
+
}
|
|
6941
|
+
//#endregion
|
|
6942
|
+
//#region packages/core/src/analyze/crawler/entity-readiness.ts
|
|
6943
|
+
function unique$3(values) {
|
|
6944
|
+
return [...new Set(values.filter((value) => Boolean(value)))];
|
|
6945
|
+
}
|
|
6946
|
+
function pct$2(part, total) {
|
|
6947
|
+
return total ? Math.round(part / total * 100) : 0;
|
|
6948
|
+
}
|
|
6949
|
+
function entityReadiness(report) {
|
|
6950
|
+
const pages = report.pages.filter((page) => page.indexable && page.status < 400);
|
|
6951
|
+
const total = pages.length || 1;
|
|
6952
|
+
const schemaTypes = {};
|
|
6953
|
+
for (const page of pages) for (const type of page.schemaTypes ?? []) schemaTypes[type] = (schemaTypes[type] ?? 0) + 1;
|
|
6954
|
+
const entitySchemaPages = pages.filter((page) => page.schemaTypes?.some((type) => /^(Organization|LocalBusiness|Person|Product|WebSite)$/i.test(type)));
|
|
6955
|
+
const sameAsEvidence = pages.flatMap((page) => page.schemaSameAsEvidence ?? []).filter((evidence) => evidence.subjectTypes.some((type) => /^(Organization|LocalBusiness|Person)$/i.test(type)));
|
|
6956
|
+
const siteSameAsEvidence = sameAsEvidence.filter((evidence) => evidence.subjectTypes.some((type) => /^(Organization|LocalBusiness)$/i.test(type)));
|
|
6957
|
+
const sameAs = unique$3(sameAsEvidence.map((evidence) => evidence.url));
|
|
6958
|
+
const sameAsByType = {};
|
|
6959
|
+
for (const evidence of sameAsEvidence) for (const type of evidence.subjectTypes) {
|
|
6960
|
+
if (!/^(Organization|LocalBusiness|Person)$/i.test(type)) continue;
|
|
6961
|
+
sameAsByType[type] = unique$3([...sameAsByType[type] ?? [], evidence.url]);
|
|
6962
|
+
}
|
|
6963
|
+
const socialProfiles = unique$3(pages.flatMap((page) => page.socialProfileLinks ?? []));
|
|
6964
|
+
const authors = unique$3(pages.map((page) => page.author));
|
|
6965
|
+
const authorPages = pages.filter((page) => page.author || page.geo?.hasAuthor);
|
|
6966
|
+
const datePages = pages.filter((page) => page.hasDate || page.geo?.hasDate);
|
|
6967
|
+
const titlePages = pages.filter((page) => page.title && page.h1);
|
|
6968
|
+
const checks = [
|
|
6969
|
+
{
|
|
6970
|
+
coverage: pct$2(entitySchemaPages.length, total),
|
|
6971
|
+
check: {
|
|
6972
|
+
id: "entity-schema",
|
|
6973
|
+
title: "Entity schema is present",
|
|
6974
|
+
plainEnglish: `${pct$2(entitySchemaPages.length, total)}% of evaluated indexable pages include Organization, LocalBusiness, Person, Product, or WebSite schema.`,
|
|
6975
|
+
action: "Add accurate entity schema to the homepage and key product, author, organization, and local pages.",
|
|
6976
|
+
evidence: { schemaTypes },
|
|
6977
|
+
urls: pages.filter((page) => !entitySchemaPages.includes(page)).slice(0, 10).map((page) => page.finalUrl)
|
|
6978
|
+
}
|
|
6979
|
+
},
|
|
6980
|
+
{
|
|
6981
|
+
coverage: siteSameAsEvidence.length ? 100 : 0,
|
|
6982
|
+
check: {
|
|
6983
|
+
id: "same-as",
|
|
6984
|
+
title: "Site entity profiles are connected",
|
|
6985
|
+
plainEnglish: siteSameAsEvidence.length ? "The crawl found sameAs links attached to Organization or LocalBusiness structured data." : sameAs.length ? "The crawl found Person sameAs links, but no Organization or LocalBusiness profile evidence for the site entity." : socialProfiles.length ? "The crawl found social-domain links, but they are not enough to prove official profile ownership." : "The crawl did not find entity-scoped sameAs evidence.",
|
|
6986
|
+
action: "Connect official profiles with sameAs on Organization, Person, or LocalBusiness schema. Use only profiles you control.",
|
|
6987
|
+
evidence: {
|
|
6988
|
+
sameAs,
|
|
6989
|
+
siteSameAs: unique$3(siteSameAsEvidence.map((evidence) => evidence.url)),
|
|
6990
|
+
sameAsEvidence,
|
|
6991
|
+
unclassifiedSocialLinks: socialProfiles
|
|
6992
|
+
}
|
|
6993
|
+
}
|
|
6994
|
+
},
|
|
6995
|
+
{
|
|
6996
|
+
coverage: Math.round((pct$2(authorPages.length, total) + pct$2(datePages.length, total)) / 2),
|
|
6997
|
+
check: {
|
|
6998
|
+
id: "authority-freshness",
|
|
6999
|
+
title: "Author and date signals observed",
|
|
7000
|
+
plainEnglish: `${pct$2(authorPages.length, total)}% of evaluated indexable pages expose an author signal and ${pct$2(datePages.length, total)}% expose a date signal.`,
|
|
7001
|
+
action: "For editorial content, identify authors and show accurate published or modified dates when that information is useful to readers.",
|
|
7002
|
+
evidence: {
|
|
7003
|
+
authorCoverage: pct$2(authorPages.length, total),
|
|
7004
|
+
dateCoverage: pct$2(datePages.length, total),
|
|
7005
|
+
authors
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
7008
|
+
},
|
|
7009
|
+
{
|
|
7010
|
+
coverage: pct$2(titlePages.length, total),
|
|
7011
|
+
check: {
|
|
7012
|
+
id: "entity-naming",
|
|
7013
|
+
title: "Title and H1 coverage observed",
|
|
7014
|
+
plainEnglish: `${pct$2(titlePages.length, total)}% of evaluated indexable pages have both a title and H1.`,
|
|
7015
|
+
action: "Review pages missing either signal. Keep brand, product, and person names accurate wherever they appear."
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
7018
|
+
].map(({ coverage, check }) => ({
|
|
7019
|
+
...check,
|
|
7020
|
+
status: "info",
|
|
7021
|
+
evidence: {
|
|
7022
|
+
...check.evidence,
|
|
7023
|
+
observedCoveragePercent: coverage
|
|
7024
|
+
}
|
|
7025
|
+
}));
|
|
7026
|
+
const dataStatus = report.status === "completed" && pages.length > 0 ? "complete" : "partial";
|
|
7027
|
+
return {
|
|
7028
|
+
reportId: report.id,
|
|
7029
|
+
url: report.config.url,
|
|
7030
|
+
generatedAt: report.generatedAt,
|
|
7031
|
+
dataStatus,
|
|
7032
|
+
evaluatedPages: pages.length,
|
|
7033
|
+
crawlPages: report.pages.length,
|
|
7034
|
+
assessment: "evidence-only",
|
|
7035
|
+
headline: dataStatus === "partial" ? "Entity evidence is incomplete; treat these findings as scoped to the evaluated pages, not the whole site." : "Entity signals are reported as observations, not a ranking, visibility, or machine-understanding score.",
|
|
7036
|
+
caveats: [
|
|
7037
|
+
...report.caveats,
|
|
7038
|
+
"This report deliberately has no aggregate entity score. Add schema, authors, dates, and sameAs only where they accurately fit the page and a documented use case.",
|
|
7039
|
+
...dataStatus === "partial" ? [`Entity coverage is based on ${pages.length} evaluated indexable page${pages.length === 1 ? "" : "s"} from a ${report.status} crawl.`] : [],
|
|
7040
|
+
...socialProfiles.length && !sameAs.length ? ["Social-domain links are unclassified navigation evidence, not proof that the linked profiles are official."] : []
|
|
7041
|
+
],
|
|
7042
|
+
checks,
|
|
7043
|
+
entities: {
|
|
7044
|
+
schemaTypes,
|
|
7045
|
+
sameAs,
|
|
7046
|
+
sameAsByType,
|
|
7047
|
+
socialProfiles,
|
|
7048
|
+
authors
|
|
7049
|
+
}
|
|
7050
|
+
};
|
|
7051
|
+
}
|
|
7052
|
+
//#endregion
|
|
7053
|
+
//#region packages/core/src/analyze/crawler/geo-gaps.ts
|
|
7054
|
+
const ELIGIBILITY_RULES = /* @__PURE__ */ new Set([
|
|
7055
|
+
"connection_error",
|
|
7056
|
+
"client_error",
|
|
7057
|
+
"server_error",
|
|
7058
|
+
"robots_blocked",
|
|
7059
|
+
"noindex",
|
|
7060
|
+
"x_robots_noindex",
|
|
7061
|
+
"canonicalized_page"
|
|
7062
|
+
]);
|
|
7063
|
+
function matchingGeoGaps(report) {
|
|
7064
|
+
const eligibilityIssuesByUrl = /* @__PURE__ */ new Map();
|
|
7065
|
+
for (const issue of report.issues) {
|
|
7066
|
+
if (!ELIGIBILITY_RULES.has(issue.ruleId)) continue;
|
|
7067
|
+
const issues = eligibilityIssuesByUrl.get(issue.url) ?? [];
|
|
7068
|
+
issues.push({
|
|
7069
|
+
ruleId: issue.ruleId,
|
|
7070
|
+
title: issue.title,
|
|
7071
|
+
severity: issue.severity
|
|
7072
|
+
});
|
|
7073
|
+
eligibilityIssuesByUrl.set(issue.url, issues);
|
|
7074
|
+
}
|
|
7075
|
+
return report.pages.map((page) => {
|
|
7076
|
+
const geo = page.geo ?? {
|
|
7077
|
+
semanticHtml: false,
|
|
7078
|
+
structuredData: false,
|
|
7079
|
+
hasAuthor: false,
|
|
7080
|
+
hasDate: false,
|
|
7081
|
+
questionHeadings: 0,
|
|
7082
|
+
structuredBlocks: 0,
|
|
7083
|
+
answerable: false
|
|
7084
|
+
};
|
|
7085
|
+
const snippetEligibility = effectiveSnippetControl({
|
|
7086
|
+
metaRobots: page.metaRobots,
|
|
7087
|
+
xRobotsTag: page.xRobotsTag
|
|
7088
|
+
});
|
|
7089
|
+
return {
|
|
7090
|
+
url: page.finalUrl,
|
|
7091
|
+
issueCount: eligibilityIssuesByUrl.get(page.url)?.length ?? 0,
|
|
7092
|
+
issues: eligibilityIssuesByUrl.get(page.url) ?? [],
|
|
7093
|
+
searchEligibility: {
|
|
7094
|
+
successfulHtmlResponse: page.status >= 200 && page.status < 300 && page.contentType?.toLowerCase().includes("text/html") === true,
|
|
7095
|
+
crawlAllowed: page.robotsTxt?.allowed ?? null,
|
|
7096
|
+
indexableCandidate: page.indexable,
|
|
7097
|
+
declaredIndexability: page.declaredIndexability,
|
|
7098
|
+
indexability: page.indexability,
|
|
7099
|
+
snippetEligibility
|
|
7100
|
+
},
|
|
7101
|
+
observations: {
|
|
7102
|
+
semanticHtml: geo.semanticHtml,
|
|
7103
|
+
structuredData: geo.structuredData,
|
|
7104
|
+
hasAuthor: geo.hasAuthor,
|
|
7105
|
+
hasDate: geo.hasDate,
|
|
7106
|
+
questionHeadings: geo.questionHeadings,
|
|
7107
|
+
structuredBlocks: geo.structuredBlocks,
|
|
7108
|
+
answerable: geo.answerable
|
|
7109
|
+
}
|
|
7110
|
+
};
|
|
7111
|
+
}).filter((gap) => gap.issueCount > 0 || !gap.searchEligibility.successfulHtmlResponse || gap.searchEligibility.crawlAllowed === false || !gap.searchEligibility.indexableCandidate || gap.searchEligibility.snippetEligibility.status !== "not-restricted").sort((a, b) => b.issueCount - a.issueCount || a.url.localeCompare(b.url));
|
|
7112
|
+
}
|
|
7113
|
+
function partialReasons(report) {
|
|
7114
|
+
const reasons = [];
|
|
7115
|
+
if (report.pages.length === 0) reasons.push("no-pages-evaluated");
|
|
7116
|
+
if (report.status !== "completed") reasons.push("crawl-not-complete");
|
|
7117
|
+
if (report.requestEvidenceStatus === "partial") reasons.push("request-evidence-partial");
|
|
7118
|
+
if (report.requestEvidenceStatus === "unavailable") reasons.push("request-evidence-unavailable");
|
|
7119
|
+
if (report.summary.pageLimitReached) reasons.push("page-limit-reached");
|
|
7120
|
+
if (report.summary.skippedUrls > 0) reasons.push("skipped-urls");
|
|
7121
|
+
if (report.summary.failedRequests > 0) reasons.push("failed-requests");
|
|
7122
|
+
if (report.summary.extractionFailures > 0) reasons.push("extraction-failures");
|
|
7123
|
+
return reasons;
|
|
7124
|
+
}
|
|
7125
|
+
function geoGapsReport(report, filters = {}) {
|
|
7126
|
+
const matches = matchingGeoGaps(report);
|
|
7127
|
+
const limit = filters.limit ?? 50;
|
|
7128
|
+
const eligibilityGaps = matches.slice(0, limit);
|
|
7129
|
+
const reasons = partialReasons(report);
|
|
7130
|
+
const truncated = eligibilityGaps.length < matches.length;
|
|
7131
|
+
return {
|
|
7132
|
+
schemaVersion: 1,
|
|
7133
|
+
dataStatus: report.pages.length ? reasons.length ? "partial" : "complete" : "unavailable",
|
|
7134
|
+
source: {
|
|
7135
|
+
provider: "seo-crawl",
|
|
7136
|
+
reportId: report.id,
|
|
7137
|
+
definitionId: report.definitionId,
|
|
7138
|
+
generatedAt: report.generatedAt,
|
|
7139
|
+
startUrl: report.config.url,
|
|
7140
|
+
crawlStatus: report.status,
|
|
7141
|
+
requestEvidenceStatus: report.requestEvidenceStatus,
|
|
7142
|
+
configuredMaxPages: report.config.maxPages,
|
|
7143
|
+
pageLimitReached: report.summary.pageLimitReached,
|
|
7144
|
+
discoveredUrls: report.summary.discoveredUrls,
|
|
7145
|
+
queuedUrls: report.summary.queuedUrls,
|
|
7146
|
+
crawledUrls: report.summary.crawledUrls,
|
|
7147
|
+
skippedUrls: report.summary.skippedUrls,
|
|
7148
|
+
failedRequests: report.summary.failedRequests,
|
|
7149
|
+
extractionFailures: report.summary.extractionFailures,
|
|
7150
|
+
partialReasons: reasons
|
|
7151
|
+
},
|
|
7152
|
+
selection: {
|
|
7153
|
+
evaluatedPages: report.pages.length,
|
|
7154
|
+
totalMatchedPages: matches.length,
|
|
7155
|
+
returnedPages: eligibilityGaps.length,
|
|
7156
|
+
limit,
|
|
7157
|
+
truncated
|
|
7158
|
+
},
|
|
7159
|
+
eligibilityGaps,
|
|
7160
|
+
warnings: report.warnings,
|
|
7161
|
+
caveats: [
|
|
7162
|
+
...report.caveats,
|
|
7163
|
+
...truncated ? [`${matches.length - eligibilityGaps.length} matched pages were omitted by the output limit.`] : [],
|
|
7164
|
+
"No detected restriction among evaluated pages does not prove indexing, selection, visibility, or citation."
|
|
7165
|
+
]
|
|
7166
|
+
};
|
|
7167
|
+
}
|
|
7168
|
+
function geoGaps(report, filters = {}) {
|
|
7169
|
+
return geoGapsReport(report, filters).eligibilityGaps;
|
|
7170
|
+
}
|
|
7171
|
+
//#endregion
|
|
7172
|
+
//#region packages/core/src/analyze/crawler/llms.ts
|
|
7173
|
+
function estimatedTokens(value) {
|
|
7174
|
+
return Math.ceil(value.length / 4);
|
|
7175
|
+
}
|
|
7176
|
+
function originHost(url) {
|
|
7177
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
7178
|
+
}
|
|
7179
|
+
function cleanTitle(page) {
|
|
7180
|
+
return page.title ?? page.h1 ?? (new URL(page.finalUrl).pathname || page.finalUrl);
|
|
7181
|
+
}
|
|
7182
|
+
function sectionForPage(page) {
|
|
7183
|
+
const path = new URL(page.finalUrl).pathname.toLowerCase();
|
|
7184
|
+
if (/\/(docs|documentation|guide|guides|help|kb|learn)\b/.test(path)) return "Docs and guides";
|
|
7185
|
+
if (/\/(blog|articles|news|resources)\b/.test(path)) return "Articles";
|
|
7186
|
+
if (/\/(product|products|features|tools|solutions)\b/.test(path)) return "Products and tools";
|
|
7187
|
+
if (/\/(pricing|plans)\b/.test(path)) return "Pricing";
|
|
7188
|
+
if (/\/(about|company|team|contact)\b/.test(path)) return "Company";
|
|
7189
|
+
return "Core pages";
|
|
7190
|
+
}
|
|
7191
|
+
function defaultExclude(url) {
|
|
7192
|
+
return /\/(login|logout|cart|checkout|account|admin|wp-admin|search|tag|privacy|terms|cookie|legal)\b/i.test(new URL(url).pathname);
|
|
7193
|
+
}
|
|
7194
|
+
function matchesPattern$1(value, patterns = []) {
|
|
7195
|
+
return patterns.some((pattern) => {
|
|
7196
|
+
if (!pattern.includes("*")) return value.includes(pattern);
|
|
7197
|
+
const parts = pattern.split("*");
|
|
7198
|
+
let cursor = 0;
|
|
7199
|
+
for (const part of parts) {
|
|
7200
|
+
if (!part) continue;
|
|
7201
|
+
const found = value.slice(cursor).indexOf(part);
|
|
7202
|
+
if (found === -1) return false;
|
|
7203
|
+
cursor += found + part.length;
|
|
7204
|
+
}
|
|
7205
|
+
return true;
|
|
7206
|
+
});
|
|
7207
|
+
}
|
|
7208
|
+
function candidatePages(report, options = {}) {
|
|
7209
|
+
return report.pages.filter((page) => page.indexable && page.status < 400).filter((page) => !defaultExclude(page.finalUrl)).filter((page) => !matchesPattern$1(page.finalUrl, options.exclude)).sort((a, b) => (b.internalLinkAuthorityScore ?? 0) - (a.internalLinkAuthorityScore ?? 0) || (b.wordCount ?? 0) - (a.wordCount ?? 0) || a.finalUrl.localeCompare(b.finalUrl));
|
|
7210
|
+
}
|
|
7211
|
+
function auditLlmsTxt(report) {
|
|
7212
|
+
const llmsTxt = report.ai?.llmsTxt;
|
|
7213
|
+
const pages = candidatePages(report);
|
|
7214
|
+
const issues = [];
|
|
7215
|
+
if (pages.length < 3) issues.push({
|
|
7216
|
+
id: "thin-llms-inventory",
|
|
7217
|
+
severity: "low",
|
|
7218
|
+
title: "The crawl has a small llms.txt inventory",
|
|
7219
|
+
plainEnglish: "There are very few indexable candidate pages to include in llms.txt.",
|
|
7220
|
+
action: "If you choose to generate the optional file, run a deeper crawl or sitemap crawl first.",
|
|
7221
|
+
evidence: { candidatePages: pages.length }
|
|
7222
|
+
});
|
|
7223
|
+
return {
|
|
7224
|
+
reportId: report.id,
|
|
7225
|
+
url: report.config.url,
|
|
7226
|
+
exists: Boolean(llmsTxt?.exists),
|
|
7227
|
+
llmsTxtUrl: llmsTxt?.url ?? new URL("/llms.txt", report.config.url).toString(),
|
|
7228
|
+
status: llmsTxt?.status,
|
|
7229
|
+
optional: true,
|
|
7230
|
+
googleSearchImpact: "none",
|
|
7231
|
+
guidanceUrl: "https://developers.google.com/search/updates#clarifying-guidance-on-llms-txt-files",
|
|
7232
|
+
headline: llmsTxt?.exists ? "An optional llms.txt file is present. It has no Google Search visibility impact." : "No llms.txt file was found. This is not an SEO issue and requires no action.",
|
|
7233
|
+
issues,
|
|
7234
|
+
recommendedPages: pages.slice(0, 25).map((page) => ({
|
|
7235
|
+
url: page.finalUrl,
|
|
7236
|
+
title: page.title,
|
|
7237
|
+
section: sectionForPage(page),
|
|
7238
|
+
reason: (page.internalLinkAuthorityScore ?? 0) > 0 ? "High internal link authority in this crawl." : "Indexable page with useful content signals."
|
|
7239
|
+
}))
|
|
7240
|
+
};
|
|
7241
|
+
}
|
|
7242
|
+
function generateLlmsTxt(report, options = {}) {
|
|
7243
|
+
const maxUrls = options.maxUrls ?? 250;
|
|
7244
|
+
const tokenBudget = options.tokenBudget ?? 12e3;
|
|
7245
|
+
const title = options.title ?? originHost(report.config.url);
|
|
7246
|
+
const description = options.description ?? `Curated entry points for agents reading ${originHost(report.config.url)}.`;
|
|
7247
|
+
const pages = candidatePages(report, options).slice(0, maxUrls);
|
|
7248
|
+
const sections = /* @__PURE__ */ new Map();
|
|
7249
|
+
for (const page of pages) {
|
|
7250
|
+
const section = sectionForPage(page);
|
|
7251
|
+
sections.set(section, [...sections.get(section) ?? [], page]);
|
|
7252
|
+
}
|
|
7253
|
+
const lines = [
|
|
7254
|
+
`# ${title}`,
|
|
7255
|
+
"",
|
|
7256
|
+
`> ${description}`,
|
|
7257
|
+
""
|
|
7258
|
+
];
|
|
7259
|
+
let includedUrls = 0;
|
|
7260
|
+
const counts = {};
|
|
7261
|
+
for (const [sectionName, sectionPages] of sections) {
|
|
7262
|
+
const sectionLines = [`## ${sectionName}`, ""];
|
|
7263
|
+
let sectionCount = 0;
|
|
7264
|
+
for (const page of sectionPages) {
|
|
7265
|
+
const title = cleanTitle(page).replace(/\]/g, "\\]");
|
|
7266
|
+
const note = page.metaDescription ? ` - ${page.metaDescription.replace(/\s+/g, " ").slice(0, 160)}` : "";
|
|
7267
|
+
const line = `- [${title}](${page.finalUrl})${note}`;
|
|
7268
|
+
if (estimatedTokens([
|
|
7269
|
+
...lines,
|
|
7270
|
+
...sectionLines,
|
|
7271
|
+
line,
|
|
7272
|
+
""
|
|
7273
|
+
].join("\n")) > tokenBudget && includedUrls > 0) break;
|
|
7274
|
+
sectionLines.push(line);
|
|
7275
|
+
sectionCount += 1;
|
|
7276
|
+
includedUrls += 1;
|
|
7277
|
+
}
|
|
7278
|
+
if (!sectionCount) break;
|
|
7279
|
+
sectionLines.push("");
|
|
7280
|
+
lines.push(...sectionLines);
|
|
7281
|
+
counts[sectionName] = sectionCount;
|
|
7282
|
+
if (sectionCount < sectionPages.length) break;
|
|
7283
|
+
}
|
|
7284
|
+
lines.push("## Notes", "", "- Optional metadata for agents and services that explicitly support llms.txt.", "- [Google Search guidance](https://developers.google.com/search/updates#clarifying-guidance-on-llms-txt-files) says llms.txt does not affect visibility or rankings.", `- Generated from crawl report ${report.id}.`, `- Crawl generated at ${report.generatedAt}.`);
|
|
7285
|
+
if (report.caveats.length) lines.push(...report.caveats.map((caveat) => `- Caveat: ${caveat}`));
|
|
7286
|
+
const content = `${lines.join("\n")}\n`;
|
|
7287
|
+
return {
|
|
7288
|
+
content,
|
|
7289
|
+
includedUrls,
|
|
7290
|
+
estimatedTokens: estimatedTokens(content),
|
|
7291
|
+
sections: counts
|
|
7292
|
+
};
|
|
7293
|
+
}
|
|
7294
|
+
//#endregion
|
|
7295
|
+
//#region packages/core/src/analyze/crawler/ai-search-scorecard.ts
|
|
7296
|
+
const AI_SEARCH_SCORECARD_METHODOLOGY_ID = "seo-ai-search-scorecard";
|
|
7297
|
+
const AI_SEARCH_SCORECARD_METHODOLOGY_VERSION = 1;
|
|
7298
|
+
/**
|
|
7299
|
+
* Status credit is this tool's own heuristic weighting. It is not a Google or
|
|
7300
|
+
* AI-engine requirement. A `warn` earns half credit because the observed
|
|
7301
|
+
* evidence is mixed, not because a partial state maps to a documented rule.
|
|
7302
|
+
*/
|
|
7303
|
+
const SCORECARD_STATUS_CREDIT = {
|
|
7304
|
+
pass: 1,
|
|
7305
|
+
warn: .5,
|
|
7306
|
+
fail: 0
|
|
7307
|
+
};
|
|
7308
|
+
/**
|
|
7309
|
+
* Fixed check order and per-check weights. Ordering is stable so the same crawl
|
|
7310
|
+
* always serializes to the same bytes. Weights sum to 100 for readability; the
|
|
7311
|
+
* score itself is normalized against the weight of the checks that had known
|
|
7312
|
+
* evidence, so unknown checks never change the denominator silently.
|
|
7313
|
+
*/
|
|
7314
|
+
const SCORECARD_CHECK_WEIGHTS = {
|
|
7315
|
+
"ai-bot-access": 20,
|
|
7316
|
+
https: 10,
|
|
7317
|
+
"indexable-pages": 15,
|
|
7318
|
+
"structured-data": 15,
|
|
7319
|
+
"valid-json-ld": 10,
|
|
7320
|
+
"entity-identity": 20,
|
|
7321
|
+
"answerable-content": 10
|
|
7322
|
+
};
|
|
7323
|
+
const SCORECARD_CHECK_ORDER = [
|
|
7324
|
+
"ai-bot-access",
|
|
7325
|
+
"https",
|
|
7326
|
+
"indexable-pages",
|
|
7327
|
+
"structured-data",
|
|
7328
|
+
"valid-json-ld",
|
|
7329
|
+
"entity-identity",
|
|
7330
|
+
"answerable-content"
|
|
7331
|
+
];
|
|
7332
|
+
const ENTITY_SCHEMA_PATTERN = /^(Organization|LocalBusiness|Person|Product|WebSite)$/i;
|
|
7333
|
+
const SITE_ENTITY_TYPE_PATTERN = /^(Organization|LocalBusiness)$/i;
|
|
7334
|
+
function indexablePages(report) {
|
|
7335
|
+
return report.pages.filter((page) => page.indexable && page.status < 400);
|
|
7336
|
+
}
|
|
7337
|
+
function pct$1(part, total) {
|
|
7338
|
+
return total > 0 ? Math.round(part / total * 100) : 0;
|
|
7339
|
+
}
|
|
7340
|
+
function sampleUrls(pages) {
|
|
7341
|
+
return pages.slice(0, 10).map((page) => page.finalUrl);
|
|
7342
|
+
}
|
|
7343
|
+
function coverageStatus(covered, total, passRatio) {
|
|
7344
|
+
if (total === 0) return "unknown";
|
|
7345
|
+
if (covered / total >= passRatio) return "pass";
|
|
7346
|
+
if (covered > 0) return "warn";
|
|
7347
|
+
return "fail";
|
|
7348
|
+
}
|
|
7349
|
+
function botAccessCheck(report) {
|
|
7350
|
+
const base = {
|
|
7351
|
+
id: "ai-bot-access",
|
|
7352
|
+
label: "AI crawler access in the start-URL robots policy",
|
|
7353
|
+
weight: SCORECARD_CHECK_WEIGHTS["ai-bot-access"]
|
|
7354
|
+
};
|
|
7355
|
+
const robotsTxt = report.ai?.robotsTxt;
|
|
7356
|
+
const startUrl = report.config.url;
|
|
7357
|
+
const verification = `Fetch ${robotsTxt?.url ?? `${new URL(startUrl).origin}/robots.txt`} and confirm every Disallow rule for the listed AI crawler tokens is intentional.`;
|
|
7358
|
+
if (!robotsTxt) return {
|
|
7359
|
+
...base,
|
|
7360
|
+
status: "unknown",
|
|
7361
|
+
reason: "This crawl did not collect top-level robots.txt policy evidence for AI crawler tokens.",
|
|
7362
|
+
observed: {
|
|
7363
|
+
robotsEvidence: "not-collected",
|
|
7364
|
+
startUrl
|
|
7365
|
+
},
|
|
7366
|
+
finding: "AI crawler access could not be evaluated because per-bot robots.txt evidence was not collected.",
|
|
7367
|
+
verification
|
|
7368
|
+
};
|
|
7369
|
+
const unavailable = robotsTxt.availability === "rate-limited" || robotsTxt.availability === "unreachable" || robotsTxt.availability === "access-blocked";
|
|
7370
|
+
const botAccess = robotsTxt.botAccess ?? [];
|
|
7371
|
+
if (unavailable || botAccess.length === 0) return {
|
|
7372
|
+
...base,
|
|
7373
|
+
status: "unknown",
|
|
7374
|
+
reason: unavailable ? `robots.txt was ${robotsTxt.availability}, so the AI crawler policy could not be read.` : "No AI crawler tokens were evaluated against the start-URL robots policy.",
|
|
7375
|
+
observed: {
|
|
7376
|
+
availability: robotsTxt.availability,
|
|
7377
|
+
status: robotsTxt.status,
|
|
7378
|
+
error: robotsTxt.error,
|
|
7379
|
+
botAccess,
|
|
7380
|
+
startUrl
|
|
7381
|
+
},
|
|
7382
|
+
finding: "AI crawler access is inconclusive from this crawl. Restore a readable robots.txt response and rerun before drawing a conclusion.",
|
|
7383
|
+
verification
|
|
7384
|
+
};
|
|
7385
|
+
const blocked = botAccess.filter((bot) => bot.allowed === false);
|
|
7386
|
+
const blockedUserAgents = blocked.map((bot) => bot.userAgent);
|
|
7387
|
+
const status = blocked.length === 0 ? "pass" : blocked.length === botAccess.length ? "fail" : "warn";
|
|
7388
|
+
const finding = status === "pass" ? "The start-URL robots policy does not block any evaluated AI crawler token. This describes policy at the entry point, not proven fetchability of every URL." : status === "fail" ? `The start-URL robots policy blocks all ${botAccess.length} evaluated AI crawler tokens. This may be intentional; confirm before treating it as a defect.` : `The start-URL robots policy blocks ${blocked.length} of ${botAccess.length} evaluated AI crawler tokens (${blockedUserAgents.join(", ")}). This may be intentional; confirm before treating it as a defect.`;
|
|
7389
|
+
return {
|
|
7390
|
+
...base,
|
|
7391
|
+
status,
|
|
7392
|
+
observed: {
|
|
7393
|
+
availability: robotsTxt.availability,
|
|
7394
|
+
status: robotsTxt.status,
|
|
7395
|
+
evaluatedTokens: botAccess.length,
|
|
7396
|
+
blockedTokens: blocked.length,
|
|
7397
|
+
blockedUserAgents,
|
|
7398
|
+
botAccess,
|
|
7399
|
+
startUrl
|
|
7400
|
+
},
|
|
7401
|
+
finding,
|
|
7402
|
+
verification
|
|
7403
|
+
};
|
|
7404
|
+
}
|
|
7405
|
+
function httpsCheck(pages) {
|
|
7406
|
+
const base = {
|
|
7407
|
+
id: "https",
|
|
7408
|
+
label: "Secure transport on evaluated pages",
|
|
7409
|
+
weight: SCORECARD_CHECK_WEIGHTS.https
|
|
7410
|
+
};
|
|
7411
|
+
const verification = "Load a sample of the returned pages and confirm each is served over HTTPS with no mixed-content downgrade.";
|
|
7412
|
+
if (pages.length === 0) return {
|
|
7413
|
+
...base,
|
|
7414
|
+
status: "unknown",
|
|
7415
|
+
reason: "No indexable 2xx pages were evaluated in this crawl.",
|
|
7416
|
+
observed: { evaluatedPages: 0 },
|
|
7417
|
+
finding: "HTTPS coverage could not be evaluated because no indexable 2xx pages were available.",
|
|
7418
|
+
verification
|
|
7419
|
+
};
|
|
7420
|
+
const httpsPages = pages.filter((page) => page.isHttps === true);
|
|
7421
|
+
const status = coverageStatus(httpsPages.length, pages.length, 1);
|
|
7422
|
+
return {
|
|
7423
|
+
...base,
|
|
7424
|
+
status,
|
|
7425
|
+
observed: {
|
|
7426
|
+
evaluatedPages: pages.length,
|
|
7427
|
+
httpsPages: httpsPages.length,
|
|
7428
|
+
coveragePercent: pct$1(httpsPages.length, pages.length),
|
|
7429
|
+
insecureSample: sampleUrls(pages.filter((page) => page.isHttps !== true))
|
|
7430
|
+
},
|
|
7431
|
+
finding: status === "pass" ? "Every evaluated indexable page was served over HTTPS." : `${pct$1(httpsPages.length, pages.length)}% of evaluated indexable pages were served over HTTPS.`,
|
|
7432
|
+
verification
|
|
7433
|
+
};
|
|
7434
|
+
}
|
|
7435
|
+
function indexablePagesCheck(report) {
|
|
7436
|
+
const base = {
|
|
7437
|
+
id: "indexable-pages",
|
|
7438
|
+
label: "Indexable 2xx share of crawled pages",
|
|
7439
|
+
weight: SCORECARD_CHECK_WEIGHTS["indexable-pages"]
|
|
7440
|
+
};
|
|
7441
|
+
const verification = "Review the non-indexable crawled URLs and confirm each noindex, redirect, canonical, or error state is intended.";
|
|
7442
|
+
const crawled = report.pages.length;
|
|
7443
|
+
if (crawled === 0) return {
|
|
7444
|
+
...base,
|
|
7445
|
+
status: "unknown",
|
|
7446
|
+
reason: "No pages were crawled, so indexability could not be evaluated.",
|
|
7447
|
+
observed: { crawledPages: 0 },
|
|
7448
|
+
finding: "Indexability could not be evaluated because no pages were crawled.",
|
|
7449
|
+
verification
|
|
7450
|
+
};
|
|
7451
|
+
const indexable = indexablePages(report).length;
|
|
7452
|
+
const status = coverageStatus(indexable, crawled, .85);
|
|
7453
|
+
return {
|
|
7454
|
+
...base,
|
|
7455
|
+
status,
|
|
7456
|
+
observed: {
|
|
7457
|
+
crawledPages: crawled,
|
|
7458
|
+
indexablePages: indexable,
|
|
7459
|
+
coveragePercent: pct$1(indexable, crawled)
|
|
7460
|
+
},
|
|
7461
|
+
finding: `${pct$1(indexable, crawled)}% of crawled pages are indexable 2xx pages. Non-indexable states can be intentional; this check flags share, not intent.`,
|
|
7462
|
+
verification
|
|
7463
|
+
};
|
|
7464
|
+
}
|
|
7465
|
+
function structuredDataCheck(pages) {
|
|
7466
|
+
const base = {
|
|
7467
|
+
id: "structured-data",
|
|
7468
|
+
label: "Structured-data coverage on evaluated pages",
|
|
7469
|
+
weight: SCORECARD_CHECK_WEIGHTS["structured-data"]
|
|
7470
|
+
};
|
|
7471
|
+
const verification = "Open a sample of the pages without structured data and confirm markup exists only where it accurately represents the visible page.";
|
|
7472
|
+
if (pages.length === 0) return {
|
|
7473
|
+
...base,
|
|
7474
|
+
status: "unknown",
|
|
7475
|
+
reason: "No indexable 2xx pages were evaluated in this crawl.",
|
|
7476
|
+
observed: { evaluatedPages: 0 },
|
|
7477
|
+
finding: "Structured-data coverage could not be evaluated because no indexable 2xx pages were available.",
|
|
7478
|
+
verification
|
|
7479
|
+
};
|
|
7480
|
+
const withStructuredData = pages.filter((page) => (page.structuredDataFormats?.length ?? 0) > 0 || (page.schemaTypes?.length ?? 0) > 0);
|
|
7481
|
+
const status = coverageStatus(withStructuredData.length, pages.length, .5);
|
|
7482
|
+
return {
|
|
7483
|
+
...base,
|
|
7484
|
+
status,
|
|
7485
|
+
observed: {
|
|
7486
|
+
evaluatedPages: pages.length,
|
|
7487
|
+
pagesWithStructuredData: withStructuredData.length,
|
|
7488
|
+
coveragePercent: pct$1(withStructuredData.length, pages.length),
|
|
7489
|
+
missingSample: sampleUrls(pages.filter((page) => !withStructuredData.includes(page)))
|
|
7490
|
+
},
|
|
7491
|
+
finding: `${pct$1(withStructuredData.length, pages.length)}% of evaluated indexable pages expose structured data. Coverage is a convenience signal for agents, not a documented ranking or citation requirement.`,
|
|
7492
|
+
verification
|
|
7493
|
+
};
|
|
7494
|
+
}
|
|
7495
|
+
function validJsonLdCheck(report) {
|
|
7496
|
+
const base = {
|
|
7497
|
+
id: "valid-json-ld",
|
|
7498
|
+
label: "JSON-LD parses cleanly where present",
|
|
7499
|
+
weight: SCORECARD_CHECK_WEIGHTS["valid-json-ld"]
|
|
7500
|
+
};
|
|
7501
|
+
const verification = "Validate the flagged pages with a JSON-LD or Rich Results parser and fix any syntax errors.";
|
|
7502
|
+
const jsonLdPages = report.pages.filter((page) => page.structuredDataFormats?.includes("json-ld") || (page.invalidJsonLdCount ?? 0) > 0);
|
|
7503
|
+
if (jsonLdPages.length === 0) return {
|
|
7504
|
+
...base,
|
|
7505
|
+
status: "unknown",
|
|
7506
|
+
reason: "No JSON-LD was observed, so its syntax could not be evaluated.",
|
|
7507
|
+
observed: { jsonLdPages: 0 },
|
|
7508
|
+
finding: "JSON-LD validity could not be evaluated because no JSON-LD was observed.",
|
|
7509
|
+
verification
|
|
7510
|
+
};
|
|
7511
|
+
const invalidPages = jsonLdPages.filter((page) => (page.invalidJsonLdCount ?? 0) > 0);
|
|
7512
|
+
const status = invalidPages.length === 0 ? "pass" : "fail";
|
|
7513
|
+
return {
|
|
7514
|
+
...base,
|
|
7515
|
+
status,
|
|
7516
|
+
observed: {
|
|
7517
|
+
jsonLdPages: jsonLdPages.length,
|
|
7518
|
+
invalidJsonLdPages: invalidPages.length,
|
|
7519
|
+
invalidSample: sampleUrls(invalidPages)
|
|
7520
|
+
},
|
|
7521
|
+
finding: status === "pass" ? `JSON-LD on ${jsonLdPages.length} observed ${jsonLdPages.length === 1 ? "page" : "pages"} parsed cleanly. Clean syntax is not proof of entity recognition or rich-result eligibility.` : `${invalidPages.length} of ${jsonLdPages.length} pages with JSON-LD contain invalid JSON-LD.`,
|
|
7522
|
+
verification
|
|
7523
|
+
};
|
|
7524
|
+
}
|
|
7525
|
+
function entityIdentityCheck(report) {
|
|
7526
|
+
const base = {
|
|
7527
|
+
id: "entity-identity",
|
|
7528
|
+
label: "Entity identity connected with sameAs",
|
|
7529
|
+
weight: SCORECARD_CHECK_WEIGHTS["entity-identity"]
|
|
7530
|
+
};
|
|
7531
|
+
const verification = "Confirm the homepage and key pages carry accurate Organization, Person, or LocalBusiness schema with sameAs links to profiles you control.";
|
|
7532
|
+
const entity = entityReadiness(report);
|
|
7533
|
+
if (entity.evaluatedPages === 0) return {
|
|
7534
|
+
...base,
|
|
7535
|
+
status: "unknown",
|
|
7536
|
+
reason: "No indexable 2xx pages were evaluated in this crawl.",
|
|
7537
|
+
observed: { evaluatedPages: 0 },
|
|
7538
|
+
finding: "Entity identity could not be evaluated because no indexable 2xx pages were available.",
|
|
7539
|
+
verification
|
|
7540
|
+
};
|
|
7541
|
+
const hasEntitySchema = Object.keys(entity.entities.schemaTypes).some((type) => ENTITY_SCHEMA_PATTERN.test(type));
|
|
7542
|
+
const siteSameAs = Object.entries(entity.entities.sameAsByType).filter(([type]) => SITE_ENTITY_TYPE_PATTERN.test(type)).flatMap(([, urls]) => urls);
|
|
7543
|
+
const uniqueSiteSameAs = [...new Set(siteSameAs)].sort();
|
|
7544
|
+
const status = uniqueSiteSameAs.length ? "pass" : hasEntitySchema ? "warn" : "fail";
|
|
7545
|
+
return {
|
|
7546
|
+
...base,
|
|
7547
|
+
status,
|
|
7548
|
+
observed: {
|
|
7549
|
+
evaluatedPages: entity.evaluatedPages,
|
|
7550
|
+
schemaTypes: entity.entities.schemaTypes,
|
|
7551
|
+
siteSameAs: uniqueSiteSameAs,
|
|
7552
|
+
sameAs: entity.entities.sameAs,
|
|
7553
|
+
socialProfiles: entity.entities.socialProfiles
|
|
7554
|
+
},
|
|
7555
|
+
finding: status === "pass" ? "The crawl found sameAs links attached to Organization or LocalBusiness structured data for the site entity." : status === "warn" ? "Entity schema is present, but no Organization or LocalBusiness sameAs evidence connects the site entity to official profiles." : "No Organization, LocalBusiness, Person, Product, or WebSite schema was found on the evaluated pages.",
|
|
7556
|
+
verification
|
|
7557
|
+
};
|
|
7558
|
+
}
|
|
7559
|
+
function answerableContentCheck(pages) {
|
|
7560
|
+
const base = {
|
|
7561
|
+
id: "answerable-content",
|
|
7562
|
+
label: "Substantive opening content (heuristic)",
|
|
7563
|
+
weight: SCORECARD_CHECK_WEIGHTS["answerable-content"]
|
|
7564
|
+
};
|
|
7565
|
+
const verification = "Read a sample of the flagged pages and confirm the opening content answers the page intent for a human reader.";
|
|
7566
|
+
if (pages.length === 0) return {
|
|
7567
|
+
...base,
|
|
7568
|
+
status: "unknown",
|
|
7569
|
+
reason: "No indexable 2xx pages were evaluated in this crawl.",
|
|
7570
|
+
observed: { evaluatedPages: 0 },
|
|
7571
|
+
finding: "Opening-content coverage could not be evaluated because no indexable 2xx pages were available.",
|
|
7572
|
+
verification
|
|
7573
|
+
};
|
|
7574
|
+
const answerable = pages.filter((page) => page.geo?.answerable);
|
|
7575
|
+
const status = coverageStatus(answerable.length, pages.length, .5);
|
|
7576
|
+
return {
|
|
7577
|
+
...base,
|
|
7578
|
+
status,
|
|
7579
|
+
observed: {
|
|
7580
|
+
evaluatedPages: pages.length,
|
|
7581
|
+
answerablePages: answerable.length,
|
|
7582
|
+
coveragePercent: pct$1(answerable.length, pages.length),
|
|
7583
|
+
heuristic: "one-of-first-three-paragraphs-has-at-least-25-words",
|
|
7584
|
+
missingSample: sampleUrls(pages.filter((page) => !page.geo?.answerable))
|
|
7585
|
+
},
|
|
7586
|
+
finding: `${pct$1(answerable.length, pages.length)}% of evaluated indexable pages open with at least one substantive paragraph. This is a readability heuristic, not a content-quality or citation measure.`,
|
|
7587
|
+
verification
|
|
7588
|
+
};
|
|
7589
|
+
}
|
|
7590
|
+
function buildObservations(report) {
|
|
7591
|
+
const observations = [];
|
|
7592
|
+
const llms = auditLlmsTxt(report);
|
|
7593
|
+
observations.push({
|
|
7594
|
+
id: "llms-txt",
|
|
7595
|
+
label: "Optional llms.txt discovery file",
|
|
7596
|
+
observed: {
|
|
7597
|
+
exists: llms.exists,
|
|
7598
|
+
url: llms.llmsTxtUrl,
|
|
7599
|
+
status: llms.status,
|
|
7600
|
+
googleSearchImpact: llms.googleSearchImpact
|
|
7601
|
+
},
|
|
7602
|
+
note: "llms.txt is optional agent-discovery metadata. Google reports no positive or negative Search impact, so presence and absence are left out of the score."
|
|
7603
|
+
});
|
|
7604
|
+
const agentResources = (report.ai?.agentResources ?? []).filter((resource) => resource.exists);
|
|
7605
|
+
observations.push({
|
|
7606
|
+
id: "agent-descriptors",
|
|
7607
|
+
label: "Machine-readable agent descriptors",
|
|
7608
|
+
observed: {
|
|
7609
|
+
detected: agentResources.length,
|
|
7610
|
+
resources: report.ai?.agentResources ?? []
|
|
7611
|
+
},
|
|
7612
|
+
note: "Descriptor files such as OpenAPI or agent metadata help machine discovery only when they are real. Their absence is not a search defect, so they are unscored."
|
|
7613
|
+
});
|
|
7614
|
+
const restricted = indexablePages(report).map((page) => ({
|
|
7615
|
+
url: page.finalUrl,
|
|
7616
|
+
control: effectiveSnippetControl({
|
|
7617
|
+
metaRobots: page.metaRobots,
|
|
7618
|
+
xRobotsTag: page.xRobotsTag
|
|
7619
|
+
})
|
|
7620
|
+
})).filter(({ control }) => control.status !== "not-restricted");
|
|
7621
|
+
observations.push({
|
|
7622
|
+
id: "snippet-controls",
|
|
7623
|
+
label: "Page-level snippet directives",
|
|
7624
|
+
observed: {
|
|
7625
|
+
restrictedPages: restricted.length,
|
|
7626
|
+
restrictions: restricted.slice(0, 10)
|
|
7627
|
+
},
|
|
7628
|
+
note: "Snippet directives are intentional publisher controls. They are recorded as observations, not scored, because a restriction can be deliberate."
|
|
7629
|
+
});
|
|
7630
|
+
return observations;
|
|
7631
|
+
}
|
|
7632
|
+
function scorecardBand(score) {
|
|
7633
|
+
if (score === null) return "unscored";
|
|
7634
|
+
if (score >= 80) return "strong";
|
|
7635
|
+
if (score >= 50) return "moderate";
|
|
7636
|
+
return "weak";
|
|
7637
|
+
}
|
|
7638
|
+
function aiSearchScorecard(report) {
|
|
7639
|
+
const pages = indexablePages(report);
|
|
7640
|
+
const geo = geoGapsReport(report);
|
|
7641
|
+
const crawlComplete = geo.dataStatus === "complete";
|
|
7642
|
+
const checksById = {
|
|
7643
|
+
"ai-bot-access": botAccessCheck(report),
|
|
7644
|
+
https: httpsCheck(pages),
|
|
7645
|
+
"indexable-pages": indexablePagesCheck(report),
|
|
7646
|
+
"structured-data": structuredDataCheck(pages),
|
|
7647
|
+
"valid-json-ld": validJsonLdCheck(report),
|
|
7648
|
+
"entity-identity": entityIdentityCheck(report),
|
|
7649
|
+
"answerable-content": answerableContentCheck(pages)
|
|
7650
|
+
};
|
|
7651
|
+
const checks = SCORECARD_CHECK_ORDER.map((id) => checksById[id]);
|
|
7652
|
+
const counts = {
|
|
7653
|
+
pass: checks.filter((check) => check.status === "pass").length,
|
|
7654
|
+
warn: checks.filter((check) => check.status === "warn").length,
|
|
7655
|
+
fail: checks.filter((check) => check.status === "fail").length,
|
|
7656
|
+
unknown: checks.filter((check) => check.status === "unknown").length,
|
|
7657
|
+
scored: 0
|
|
7658
|
+
};
|
|
7659
|
+
const scoredChecks = checks.filter((check) => check.status !== "unknown");
|
|
7660
|
+
counts.scored = scoredChecks.length;
|
|
7661
|
+
const weightScored = scoredChecks.reduce((total, check) => total + check.weight, 0);
|
|
7662
|
+
const weightTotal = checks.reduce((total, check) => total + check.weight, 0);
|
|
7663
|
+
const earned = scoredChecks.reduce((total, check) => total + check.weight * SCORECARD_STATUS_CREDIT[check.status], 0);
|
|
7664
|
+
let score = weightScored > 0 ? Math.round(earned / weightScored * 100) : null;
|
|
7665
|
+
const partial = counts.unknown > 0 || !crawlComplete;
|
|
7666
|
+
if (score === 100 && partial) score = 99;
|
|
7667
|
+
const excluded = checks.filter((check) => check.status === "unknown").map((check) => ({
|
|
7668
|
+
id: check.id,
|
|
7669
|
+
reason: check.reason ?? "Evidence was unavailable for this check."
|
|
7670
|
+
}));
|
|
7671
|
+
const observations = buildObservations(report);
|
|
7672
|
+
const band = scorecardBand(score);
|
|
7673
|
+
const headline = score === null ? "AI search scorecard could not be scored because no check had known evidence in this crawl." : `AI search scorecard: ${score}/100, a heuristic summary of ${counts.scored} of ${checks.length} of this tool's own checks with known evidence.${partial ? ` Score is partial: ${counts.unknown} ${counts.unknown === 1 ? "check was" : "checks were"} unknown${crawlComplete ? "" : " and the crawl was incomplete"}, so a clean 100 is not possible.` : ""}`;
|
|
7674
|
+
const caveats = [
|
|
7675
|
+
...report.caveats,
|
|
7676
|
+
"This score is a heuristic summary of this tool's own checks. It is not a Google or AI-engine requirement, an eligibility verdict, a ranking factor, or a prediction of citations, indexing, visibility, or traffic.",
|
|
7677
|
+
"Only checks with known evidence are scored. Unknown checks are excluded and are never counted as failures.",
|
|
7678
|
+
...crawlComplete ? [] : [`The crawl was incomplete (${geo.source.partialReasons.join(", ") || "partial evidence"}), so this score is scoped to the evaluated pages and cannot be a clean 100.`],
|
|
7679
|
+
...checksById["ai-bot-access"].status === "warn" || checksById["ai-bot-access"].status === "fail" ? ["Blocking AI crawler tokens can be an intentional publisher choice. Confirm intent before treating the robots policy as a defect."] : [],
|
|
7680
|
+
"llms.txt, agent descriptors, and snippet directives are recorded as observations, not scored, because their absence or presence can be intentional."
|
|
7681
|
+
];
|
|
6918
7682
|
return {
|
|
6919
7683
|
reportId: report.id,
|
|
6920
7684
|
url: report.config.url,
|
|
6921
7685
|
generatedAt: report.generatedAt,
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
7686
|
+
methodology: {
|
|
7687
|
+
id: AI_SEARCH_SCORECARD_METHODOLOGY_ID,
|
|
7688
|
+
version: 1,
|
|
7689
|
+
summary: "A deterministic 0-100 heuristic summary of this tool's own AI-search checks over one crawl. It is not a Google or AI-engine requirement, eligibility verdict, or ranking predictor.",
|
|
7690
|
+
statusCredit: SCORECARD_STATUS_CREDIT,
|
|
7691
|
+
weights: { ...SCORECARD_CHECK_WEIGHTS },
|
|
7692
|
+
formula: "score = round(100 * sum(weight_i * credit(status_i)) / sum(weight_i)) over checks with a known status, where credit is pass 1, warn 0.5, fail 0. Unknown checks are excluded from both sums. A partial or incomplete crawl caps the score below 100."
|
|
7693
|
+
},
|
|
7694
|
+
score,
|
|
7695
|
+
scoreLabel: "heuristic-check-summary",
|
|
7696
|
+
maxScore: 100,
|
|
7697
|
+
band,
|
|
7698
|
+
partial,
|
|
7699
|
+
crawlComplete,
|
|
7700
|
+
counts,
|
|
7701
|
+
weightScored,
|
|
7702
|
+
weightTotal,
|
|
7703
|
+
excluded,
|
|
6926
7704
|
checks,
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
7705
|
+
observations,
|
|
7706
|
+
headline,
|
|
7707
|
+
caveats,
|
|
7708
|
+
source: {
|
|
7709
|
+
provider: "seo-crawl",
|
|
7710
|
+
crawlStatus: report.status,
|
|
7711
|
+
requestEvidenceStatus: report.requestEvidenceStatus,
|
|
7712
|
+
startUrl: report.config.url,
|
|
7713
|
+
configuredMaxPages: report.config.maxPages,
|
|
7714
|
+
pageLimitReached: report.summary.pageLimitReached,
|
|
7715
|
+
evaluatedPages: pages.length,
|
|
7716
|
+
crawledPages: report.pages.length,
|
|
7717
|
+
partialReasons: geo.source.partialReasons
|
|
7718
|
+
}
|
|
6939
7719
|
};
|
|
6940
7720
|
}
|
|
6941
7721
|
//#endregion
|
|
@@ -10721,133 +11501,22 @@ async function resolveTechnicalBaseline(input, dependencies = defaultDependencie
|
|
|
10721
11501
|
maxPages,
|
|
10722
11502
|
maxDepth
|
|
10723
11503
|
}));
|
|
10724
|
-
if (report.status === "failed") return {
|
|
10725
|
-
status: "unavailable",
|
|
10726
|
-
reason: unavailableReason(report)
|
|
10727
|
-
};
|
|
10728
|
-
dependencies.saveCrawlReport(report);
|
|
10729
|
-
return {
|
|
10730
|
-
status: input.refresh ? "refreshed" : "created",
|
|
10731
|
-
report,
|
|
10732
|
-
...report.status === "partial" ? { reason: "The technical crawl completed with partial coverage." } : {}
|
|
10733
|
-
};
|
|
10734
|
-
} catch (error) {
|
|
10735
|
-
return {
|
|
10736
|
-
status: "unavailable",
|
|
10737
|
-
reason: `The technical crawl could not run: ${error instanceof Error ? error.message : String(error)}`
|
|
10738
|
-
};
|
|
10739
|
-
}
|
|
10740
|
-
}
|
|
10741
|
-
//#endregion
|
|
10742
|
-
//#region packages/core/src/analyze/crawler/entity-readiness.ts
|
|
10743
|
-
function unique$3(values) {
|
|
10744
|
-
return [...new Set(values.filter((value) => Boolean(value)))];
|
|
10745
|
-
}
|
|
10746
|
-
function pct$1(part, total) {
|
|
10747
|
-
return total ? Math.round(part / total * 100) : 0;
|
|
10748
|
-
}
|
|
10749
|
-
function entityReadiness(report) {
|
|
10750
|
-
const pages = report.pages.filter((page) => page.indexable && page.status < 400);
|
|
10751
|
-
const total = pages.length || 1;
|
|
10752
|
-
const schemaTypes = {};
|
|
10753
|
-
for (const page of pages) for (const type of page.schemaTypes ?? []) schemaTypes[type] = (schemaTypes[type] ?? 0) + 1;
|
|
10754
|
-
const entitySchemaPages = pages.filter((page) => page.schemaTypes?.some((type) => /^(Organization|LocalBusiness|Person|Product|WebSite)$/i.test(type)));
|
|
10755
|
-
const sameAsEvidence = pages.flatMap((page) => page.schemaSameAsEvidence ?? []).filter((evidence) => evidence.subjectTypes.some((type) => /^(Organization|LocalBusiness|Person)$/i.test(type)));
|
|
10756
|
-
const siteSameAsEvidence = sameAsEvidence.filter((evidence) => evidence.subjectTypes.some((type) => /^(Organization|LocalBusiness)$/i.test(type)));
|
|
10757
|
-
const sameAs = unique$3(sameAsEvidence.map((evidence) => evidence.url));
|
|
10758
|
-
const sameAsByType = {};
|
|
10759
|
-
for (const evidence of sameAsEvidence) for (const type of evidence.subjectTypes) {
|
|
10760
|
-
if (!/^(Organization|LocalBusiness|Person)$/i.test(type)) continue;
|
|
10761
|
-
sameAsByType[type] = unique$3([...sameAsByType[type] ?? [], evidence.url]);
|
|
10762
|
-
}
|
|
10763
|
-
const socialProfiles = unique$3(pages.flatMap((page) => page.socialProfileLinks ?? []));
|
|
10764
|
-
const authors = unique$3(pages.map((page) => page.author));
|
|
10765
|
-
const authorPages = pages.filter((page) => page.author || page.geo?.hasAuthor);
|
|
10766
|
-
const datePages = pages.filter((page) => page.hasDate || page.geo?.hasDate);
|
|
10767
|
-
const titlePages = pages.filter((page) => page.title && page.h1);
|
|
10768
|
-
const checks = [
|
|
10769
|
-
{
|
|
10770
|
-
coverage: pct$1(entitySchemaPages.length, total),
|
|
10771
|
-
check: {
|
|
10772
|
-
id: "entity-schema",
|
|
10773
|
-
title: "Entity schema is present",
|
|
10774
|
-
plainEnglish: `${pct$1(entitySchemaPages.length, total)}% of evaluated indexable pages include Organization, LocalBusiness, Person, Product, or WebSite schema.`,
|
|
10775
|
-
action: "Add accurate entity schema to the homepage and key product, author, organization, and local pages.",
|
|
10776
|
-
evidence: { schemaTypes },
|
|
10777
|
-
urls: pages.filter((page) => !entitySchemaPages.includes(page)).slice(0, 10).map((page) => page.finalUrl)
|
|
10778
|
-
}
|
|
10779
|
-
},
|
|
10780
|
-
{
|
|
10781
|
-
coverage: siteSameAsEvidence.length ? 100 : 0,
|
|
10782
|
-
check: {
|
|
10783
|
-
id: "same-as",
|
|
10784
|
-
title: "Site entity profiles are connected",
|
|
10785
|
-
plainEnglish: siteSameAsEvidence.length ? "The crawl found sameAs links attached to Organization or LocalBusiness structured data." : sameAs.length ? "The crawl found Person sameAs links, but no Organization or LocalBusiness profile evidence for the site entity." : socialProfiles.length ? "The crawl found social-domain links, but they are not enough to prove official profile ownership." : "The crawl did not find entity-scoped sameAs evidence.",
|
|
10786
|
-
action: "Connect official profiles with sameAs on Organization, Person, or LocalBusiness schema. Use only profiles you control.",
|
|
10787
|
-
evidence: {
|
|
10788
|
-
sameAs,
|
|
10789
|
-
siteSameAs: unique$3(siteSameAsEvidence.map((evidence) => evidence.url)),
|
|
10790
|
-
sameAsEvidence,
|
|
10791
|
-
unclassifiedSocialLinks: socialProfiles
|
|
10792
|
-
}
|
|
10793
|
-
}
|
|
10794
|
-
},
|
|
10795
|
-
{
|
|
10796
|
-
coverage: Math.round((pct$1(authorPages.length, total) + pct$1(datePages.length, total)) / 2),
|
|
10797
|
-
check: {
|
|
10798
|
-
id: "authority-freshness",
|
|
10799
|
-
title: "Author and date signals observed",
|
|
10800
|
-
plainEnglish: `${pct$1(authorPages.length, total)}% of evaluated indexable pages expose an author signal and ${pct$1(datePages.length, total)}% expose a date signal.`,
|
|
10801
|
-
action: "For editorial content, identify authors and show accurate published or modified dates when that information is useful to readers.",
|
|
10802
|
-
evidence: {
|
|
10803
|
-
authorCoverage: pct$1(authorPages.length, total),
|
|
10804
|
-
dateCoverage: pct$1(datePages.length, total),
|
|
10805
|
-
authors
|
|
10806
|
-
}
|
|
10807
|
-
}
|
|
10808
|
-
},
|
|
10809
|
-
{
|
|
10810
|
-
coverage: pct$1(titlePages.length, total),
|
|
10811
|
-
check: {
|
|
10812
|
-
id: "entity-naming",
|
|
10813
|
-
title: "Title and H1 coverage observed",
|
|
10814
|
-
plainEnglish: `${pct$1(titlePages.length, total)}% of evaluated indexable pages have both a title and H1.`,
|
|
10815
|
-
action: "Review pages missing either signal. Keep brand, product, and person names accurate wherever they appear."
|
|
10816
|
-
}
|
|
10817
|
-
}
|
|
10818
|
-
].map(({ coverage, check }) => ({
|
|
10819
|
-
...check,
|
|
10820
|
-
status: "info",
|
|
10821
|
-
evidence: {
|
|
10822
|
-
...check.evidence,
|
|
10823
|
-
observedCoveragePercent: coverage
|
|
10824
|
-
}
|
|
10825
|
-
}));
|
|
10826
|
-
const dataStatus = report.status === "completed" && pages.length > 0 ? "complete" : "partial";
|
|
10827
|
-
return {
|
|
10828
|
-
reportId: report.id,
|
|
10829
|
-
url: report.config.url,
|
|
10830
|
-
generatedAt: report.generatedAt,
|
|
10831
|
-
dataStatus,
|
|
10832
|
-
evaluatedPages: pages.length,
|
|
10833
|
-
crawlPages: report.pages.length,
|
|
10834
|
-
assessment: "evidence-only",
|
|
10835
|
-
headline: dataStatus === "partial" ? "Entity evidence is incomplete; treat these findings as scoped to the evaluated pages, not the whole site." : "Entity signals are reported as observations, not a ranking, visibility, or machine-understanding score.",
|
|
10836
|
-
caveats: [
|
|
10837
|
-
...report.caveats,
|
|
10838
|
-
"This report deliberately has no aggregate entity score. Add schema, authors, dates, and sameAs only where they accurately fit the page and a documented use case.",
|
|
10839
|
-
...dataStatus === "partial" ? [`Entity coverage is based on ${pages.length} evaluated indexable page${pages.length === 1 ? "" : "s"} from a ${report.status} crawl.`] : [],
|
|
10840
|
-
...socialProfiles.length && !sameAs.length ? ["Social-domain links are unclassified navigation evidence, not proof that the linked profiles are official."] : []
|
|
10841
|
-
],
|
|
10842
|
-
checks,
|
|
10843
|
-
entities: {
|
|
10844
|
-
schemaTypes,
|
|
10845
|
-
sameAs,
|
|
10846
|
-
sameAsByType,
|
|
10847
|
-
socialProfiles,
|
|
10848
|
-
authors
|
|
10849
|
-
}
|
|
10850
|
-
};
|
|
11504
|
+
if (report.status === "failed") return {
|
|
11505
|
+
status: "unavailable",
|
|
11506
|
+
reason: unavailableReason(report)
|
|
11507
|
+
};
|
|
11508
|
+
dependencies.saveCrawlReport(report);
|
|
11509
|
+
return {
|
|
11510
|
+
status: input.refresh ? "refreshed" : "created",
|
|
11511
|
+
report,
|
|
11512
|
+
...report.status === "partial" ? { reason: "The technical crawl completed with partial coverage." } : {}
|
|
11513
|
+
};
|
|
11514
|
+
} catch (error) {
|
|
11515
|
+
return {
|
|
11516
|
+
status: "unavailable",
|
|
11517
|
+
reason: `The technical crawl could not run: ${error instanceof Error ? error.message : String(error)}`
|
|
11518
|
+
};
|
|
11519
|
+
}
|
|
10851
11520
|
}
|
|
10852
11521
|
//#endregion
|
|
10853
11522
|
//#region packages/core/src/export/diagnosis-segment-csv.ts
|
|
@@ -11881,7 +12550,7 @@ const VALUE_WEIGHT = {
|
|
|
11881
12550
|
medium: .75,
|
|
11882
12551
|
low: .15
|
|
11883
12552
|
};
|
|
11884
|
-
function matchesPattern
|
|
12553
|
+
function matchesPattern(pattern, value) {
|
|
11885
12554
|
if (!pattern.includes("*")) return value.includes(pattern);
|
|
11886
12555
|
const parts = pattern.split("*");
|
|
11887
12556
|
let position = 0;
|
|
@@ -11898,7 +12567,7 @@ function filteredIssues(report, filters) {
|
|
|
11898
12567
|
return report.issues.filter((issue) => {
|
|
11899
12568
|
if (filters.category && issue.category !== filters.category) return false;
|
|
11900
12569
|
if (filters.severity && issue.severity !== filters.severity) return false;
|
|
11901
|
-
if (filters.url && !matchesPattern
|
|
12570
|
+
if (filters.url && !matchesPattern(filters.url, issue.url)) return false;
|
|
11902
12571
|
if ((filters.recommendation ?? "fix") !== recommendationForRule(issue.ruleId)) return false;
|
|
11903
12572
|
return true;
|
|
11904
12573
|
});
|
|
@@ -12183,125 +12852,6 @@ function escapeHtml(value) {
|
|
|
12183
12852
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
12184
12853
|
}
|
|
12185
12854
|
//#endregion
|
|
12186
|
-
//#region packages/core/src/analyze/crawler/geo-gaps.ts
|
|
12187
|
-
const ELIGIBILITY_RULES = /* @__PURE__ */ new Set([
|
|
12188
|
-
"connection_error",
|
|
12189
|
-
"client_error",
|
|
12190
|
-
"server_error",
|
|
12191
|
-
"robots_blocked",
|
|
12192
|
-
"noindex",
|
|
12193
|
-
"x_robots_noindex",
|
|
12194
|
-
"canonicalized_page"
|
|
12195
|
-
]);
|
|
12196
|
-
function matchingGeoGaps(report) {
|
|
12197
|
-
const eligibilityIssuesByUrl = /* @__PURE__ */ new Map();
|
|
12198
|
-
for (const issue of report.issues) {
|
|
12199
|
-
if (!ELIGIBILITY_RULES.has(issue.ruleId)) continue;
|
|
12200
|
-
const issues = eligibilityIssuesByUrl.get(issue.url) ?? [];
|
|
12201
|
-
issues.push({
|
|
12202
|
-
ruleId: issue.ruleId,
|
|
12203
|
-
title: issue.title,
|
|
12204
|
-
severity: issue.severity
|
|
12205
|
-
});
|
|
12206
|
-
eligibilityIssuesByUrl.set(issue.url, issues);
|
|
12207
|
-
}
|
|
12208
|
-
return report.pages.map((page) => {
|
|
12209
|
-
const geo = page.geo ?? {
|
|
12210
|
-
semanticHtml: false,
|
|
12211
|
-
structuredData: false,
|
|
12212
|
-
hasAuthor: false,
|
|
12213
|
-
hasDate: false,
|
|
12214
|
-
questionHeadings: 0,
|
|
12215
|
-
structuredBlocks: 0,
|
|
12216
|
-
answerable: false
|
|
12217
|
-
};
|
|
12218
|
-
const snippetEligibility = effectiveSnippetControl({
|
|
12219
|
-
metaRobots: page.metaRobots,
|
|
12220
|
-
xRobotsTag: page.xRobotsTag
|
|
12221
|
-
});
|
|
12222
|
-
return {
|
|
12223
|
-
url: page.finalUrl,
|
|
12224
|
-
issueCount: eligibilityIssuesByUrl.get(page.url)?.length ?? 0,
|
|
12225
|
-
issues: eligibilityIssuesByUrl.get(page.url) ?? [],
|
|
12226
|
-
searchEligibility: {
|
|
12227
|
-
successfulHtmlResponse: page.status >= 200 && page.status < 300 && page.contentType?.toLowerCase().includes("text/html") === true,
|
|
12228
|
-
crawlAllowed: page.robotsTxt?.allowed ?? null,
|
|
12229
|
-
indexableCandidate: page.indexable,
|
|
12230
|
-
declaredIndexability: page.declaredIndexability,
|
|
12231
|
-
indexability: page.indexability,
|
|
12232
|
-
snippetEligibility
|
|
12233
|
-
},
|
|
12234
|
-
observations: {
|
|
12235
|
-
semanticHtml: geo.semanticHtml,
|
|
12236
|
-
structuredData: geo.structuredData,
|
|
12237
|
-
hasAuthor: geo.hasAuthor,
|
|
12238
|
-
hasDate: geo.hasDate,
|
|
12239
|
-
questionHeadings: geo.questionHeadings,
|
|
12240
|
-
structuredBlocks: geo.structuredBlocks,
|
|
12241
|
-
answerable: geo.answerable
|
|
12242
|
-
}
|
|
12243
|
-
};
|
|
12244
|
-
}).filter((gap) => gap.issueCount > 0 || !gap.searchEligibility.successfulHtmlResponse || gap.searchEligibility.crawlAllowed === false || !gap.searchEligibility.indexableCandidate || gap.searchEligibility.snippetEligibility.status !== "not-restricted").sort((a, b) => b.issueCount - a.issueCount || a.url.localeCompare(b.url));
|
|
12245
|
-
}
|
|
12246
|
-
function partialReasons(report) {
|
|
12247
|
-
const reasons = [];
|
|
12248
|
-
if (report.pages.length === 0) reasons.push("no-pages-evaluated");
|
|
12249
|
-
if (report.status !== "completed") reasons.push("crawl-not-complete");
|
|
12250
|
-
if (report.requestEvidenceStatus === "partial") reasons.push("request-evidence-partial");
|
|
12251
|
-
if (report.requestEvidenceStatus === "unavailable") reasons.push("request-evidence-unavailable");
|
|
12252
|
-
if (report.summary.pageLimitReached) reasons.push("page-limit-reached");
|
|
12253
|
-
if (report.summary.skippedUrls > 0) reasons.push("skipped-urls");
|
|
12254
|
-
if (report.summary.failedRequests > 0) reasons.push("failed-requests");
|
|
12255
|
-
if (report.summary.extractionFailures > 0) reasons.push("extraction-failures");
|
|
12256
|
-
return reasons;
|
|
12257
|
-
}
|
|
12258
|
-
function geoGapsReport(report, filters = {}) {
|
|
12259
|
-
const matches = matchingGeoGaps(report);
|
|
12260
|
-
const limit = filters.limit ?? 50;
|
|
12261
|
-
const eligibilityGaps = matches.slice(0, limit);
|
|
12262
|
-
const reasons = partialReasons(report);
|
|
12263
|
-
const truncated = eligibilityGaps.length < matches.length;
|
|
12264
|
-
return {
|
|
12265
|
-
schemaVersion: 1,
|
|
12266
|
-
dataStatus: report.pages.length ? reasons.length ? "partial" : "complete" : "unavailable",
|
|
12267
|
-
source: {
|
|
12268
|
-
provider: "seo-crawl",
|
|
12269
|
-
reportId: report.id,
|
|
12270
|
-
definitionId: report.definitionId,
|
|
12271
|
-
generatedAt: report.generatedAt,
|
|
12272
|
-
startUrl: report.config.url,
|
|
12273
|
-
crawlStatus: report.status,
|
|
12274
|
-
requestEvidenceStatus: report.requestEvidenceStatus,
|
|
12275
|
-
configuredMaxPages: report.config.maxPages,
|
|
12276
|
-
pageLimitReached: report.summary.pageLimitReached,
|
|
12277
|
-
discoveredUrls: report.summary.discoveredUrls,
|
|
12278
|
-
queuedUrls: report.summary.queuedUrls,
|
|
12279
|
-
crawledUrls: report.summary.crawledUrls,
|
|
12280
|
-
skippedUrls: report.summary.skippedUrls,
|
|
12281
|
-
failedRequests: report.summary.failedRequests,
|
|
12282
|
-
extractionFailures: report.summary.extractionFailures,
|
|
12283
|
-
partialReasons: reasons
|
|
12284
|
-
},
|
|
12285
|
-
selection: {
|
|
12286
|
-
evaluatedPages: report.pages.length,
|
|
12287
|
-
totalMatchedPages: matches.length,
|
|
12288
|
-
returnedPages: eligibilityGaps.length,
|
|
12289
|
-
limit,
|
|
12290
|
-
truncated
|
|
12291
|
-
},
|
|
12292
|
-
eligibilityGaps,
|
|
12293
|
-
warnings: report.warnings,
|
|
12294
|
-
caveats: [
|
|
12295
|
-
...report.caveats,
|
|
12296
|
-
...truncated ? [`${matches.length - eligibilityGaps.length} matched pages were omitted by the output limit.`] : [],
|
|
12297
|
-
"No detected restriction among evaluated pages does not prove indexing, selection, visibility, or citation."
|
|
12298
|
-
]
|
|
12299
|
-
};
|
|
12300
|
-
}
|
|
12301
|
-
function geoGaps(report, filters = {}) {
|
|
12302
|
-
return geoGapsReport(report, filters).eligibilityGaps;
|
|
12303
|
-
}
|
|
12304
|
-
//#endregion
|
|
12305
12855
|
//#region packages/core/src/analyze/crawler/history-provenance.ts
|
|
12306
12856
|
function snapshotCaps(report) {
|
|
12307
12857
|
return {
|
|
@@ -12614,129 +13164,6 @@ function compareCrawlReports(input) {
|
|
|
12614
13164
|
};
|
|
12615
13165
|
}
|
|
12616
13166
|
//#endregion
|
|
12617
|
-
//#region packages/core/src/analyze/crawler/llms.ts
|
|
12618
|
-
function estimatedTokens(value) {
|
|
12619
|
-
return Math.ceil(value.length / 4);
|
|
12620
|
-
}
|
|
12621
|
-
function originHost(url) {
|
|
12622
|
-
return new URL(url).hostname.replace(/^www\./, "");
|
|
12623
|
-
}
|
|
12624
|
-
function cleanTitle(page) {
|
|
12625
|
-
return page.title ?? page.h1 ?? (new URL(page.finalUrl).pathname || page.finalUrl);
|
|
12626
|
-
}
|
|
12627
|
-
function sectionForPage(page) {
|
|
12628
|
-
const path = new URL(page.finalUrl).pathname.toLowerCase();
|
|
12629
|
-
if (/\/(docs|documentation|guide|guides|help|kb|learn)\b/.test(path)) return "Docs and guides";
|
|
12630
|
-
if (/\/(blog|articles|news|resources)\b/.test(path)) return "Articles";
|
|
12631
|
-
if (/\/(product|products|features|tools|solutions)\b/.test(path)) return "Products and tools";
|
|
12632
|
-
if (/\/(pricing|plans)\b/.test(path)) return "Pricing";
|
|
12633
|
-
if (/\/(about|company|team|contact)\b/.test(path)) return "Company";
|
|
12634
|
-
return "Core pages";
|
|
12635
|
-
}
|
|
12636
|
-
function defaultExclude(url) {
|
|
12637
|
-
return /\/(login|logout|cart|checkout|account|admin|wp-admin|search|tag|privacy|terms|cookie|legal)\b/i.test(new URL(url).pathname);
|
|
12638
|
-
}
|
|
12639
|
-
function matchesPattern(value, patterns = []) {
|
|
12640
|
-
return patterns.some((pattern) => {
|
|
12641
|
-
if (!pattern.includes("*")) return value.includes(pattern);
|
|
12642
|
-
const parts = pattern.split("*");
|
|
12643
|
-
let cursor = 0;
|
|
12644
|
-
for (const part of parts) {
|
|
12645
|
-
if (!part) continue;
|
|
12646
|
-
const found = value.slice(cursor).indexOf(part);
|
|
12647
|
-
if (found === -1) return false;
|
|
12648
|
-
cursor += found + part.length;
|
|
12649
|
-
}
|
|
12650
|
-
return true;
|
|
12651
|
-
});
|
|
12652
|
-
}
|
|
12653
|
-
function candidatePages(report, options = {}) {
|
|
12654
|
-
return report.pages.filter((page) => page.indexable && page.status < 400).filter((page) => !defaultExclude(page.finalUrl)).filter((page) => !matchesPattern(page.finalUrl, options.exclude)).sort((a, b) => (b.internalLinkAuthorityScore ?? 0) - (a.internalLinkAuthorityScore ?? 0) || (b.wordCount ?? 0) - (a.wordCount ?? 0) || a.finalUrl.localeCompare(b.finalUrl));
|
|
12655
|
-
}
|
|
12656
|
-
function auditLlmsTxt(report) {
|
|
12657
|
-
const llmsTxt = report.ai?.llmsTxt;
|
|
12658
|
-
const pages = candidatePages(report);
|
|
12659
|
-
const issues = [];
|
|
12660
|
-
if (pages.length < 3) issues.push({
|
|
12661
|
-
id: "thin-llms-inventory",
|
|
12662
|
-
severity: "low",
|
|
12663
|
-
title: "The crawl has a small llms.txt inventory",
|
|
12664
|
-
plainEnglish: "There are very few indexable candidate pages to include in llms.txt.",
|
|
12665
|
-
action: "If you choose to generate the optional file, run a deeper crawl or sitemap crawl first.",
|
|
12666
|
-
evidence: { candidatePages: pages.length }
|
|
12667
|
-
});
|
|
12668
|
-
return {
|
|
12669
|
-
reportId: report.id,
|
|
12670
|
-
url: report.config.url,
|
|
12671
|
-
exists: Boolean(llmsTxt?.exists),
|
|
12672
|
-
llmsTxtUrl: llmsTxt?.url ?? new URL("/llms.txt", report.config.url).toString(),
|
|
12673
|
-
status: llmsTxt?.status,
|
|
12674
|
-
optional: true,
|
|
12675
|
-
googleSearchImpact: "none",
|
|
12676
|
-
guidanceUrl: "https://developers.google.com/search/updates#clarifying-guidance-on-llms-txt-files",
|
|
12677
|
-
headline: llmsTxt?.exists ? "An optional llms.txt file is present. It has no Google Search visibility impact." : "No llms.txt file was found. This is not an SEO issue and requires no action.",
|
|
12678
|
-
issues,
|
|
12679
|
-
recommendedPages: pages.slice(0, 25).map((page) => ({
|
|
12680
|
-
url: page.finalUrl,
|
|
12681
|
-
title: page.title,
|
|
12682
|
-
section: sectionForPage(page),
|
|
12683
|
-
reason: (page.internalLinkAuthorityScore ?? 0) > 0 ? "High internal link authority in this crawl." : "Indexable page with useful content signals."
|
|
12684
|
-
}))
|
|
12685
|
-
};
|
|
12686
|
-
}
|
|
12687
|
-
function generateLlmsTxt(report, options = {}) {
|
|
12688
|
-
const maxUrls = options.maxUrls ?? 250;
|
|
12689
|
-
const tokenBudget = options.tokenBudget ?? 12e3;
|
|
12690
|
-
const title = options.title ?? originHost(report.config.url);
|
|
12691
|
-
const description = options.description ?? `Curated entry points for agents reading ${originHost(report.config.url)}.`;
|
|
12692
|
-
const pages = candidatePages(report, options).slice(0, maxUrls);
|
|
12693
|
-
const sections = /* @__PURE__ */ new Map();
|
|
12694
|
-
for (const page of pages) {
|
|
12695
|
-
const section = sectionForPage(page);
|
|
12696
|
-
sections.set(section, [...sections.get(section) ?? [], page]);
|
|
12697
|
-
}
|
|
12698
|
-
const lines = [
|
|
12699
|
-
`# ${title}`,
|
|
12700
|
-
"",
|
|
12701
|
-
`> ${description}`,
|
|
12702
|
-
""
|
|
12703
|
-
];
|
|
12704
|
-
let includedUrls = 0;
|
|
12705
|
-
const counts = {};
|
|
12706
|
-
for (const [sectionName, sectionPages] of sections) {
|
|
12707
|
-
const sectionLines = [`## ${sectionName}`, ""];
|
|
12708
|
-
let sectionCount = 0;
|
|
12709
|
-
for (const page of sectionPages) {
|
|
12710
|
-
const title = cleanTitle(page).replace(/\]/g, "\\]");
|
|
12711
|
-
const note = page.metaDescription ? ` - ${page.metaDescription.replace(/\s+/g, " ").slice(0, 160)}` : "";
|
|
12712
|
-
const line = `- [${title}](${page.finalUrl})${note}`;
|
|
12713
|
-
if (estimatedTokens([
|
|
12714
|
-
...lines,
|
|
12715
|
-
...sectionLines,
|
|
12716
|
-
line,
|
|
12717
|
-
""
|
|
12718
|
-
].join("\n")) > tokenBudget && includedUrls > 0) break;
|
|
12719
|
-
sectionLines.push(line);
|
|
12720
|
-
sectionCount += 1;
|
|
12721
|
-
includedUrls += 1;
|
|
12722
|
-
}
|
|
12723
|
-
if (!sectionCount) break;
|
|
12724
|
-
sectionLines.push("");
|
|
12725
|
-
lines.push(...sectionLines);
|
|
12726
|
-
counts[sectionName] = sectionCount;
|
|
12727
|
-
if (sectionCount < sectionPages.length) break;
|
|
12728
|
-
}
|
|
12729
|
-
lines.push("## Notes", "", "- Optional metadata for agents and services that explicitly support llms.txt.", "- [Google Search guidance](https://developers.google.com/search/updates#clarifying-guidance-on-llms-txt-files) says llms.txt does not affect visibility or rankings.", `- Generated from crawl report ${report.id}.`, `- Crawl generated at ${report.generatedAt}.`);
|
|
12730
|
-
if (report.caveats.length) lines.push(...report.caveats.map((caveat) => `- Caveat: ${caveat}`));
|
|
12731
|
-
const content = `${lines.join("\n")}\n`;
|
|
12732
|
-
return {
|
|
12733
|
-
content,
|
|
12734
|
-
includedUrls,
|
|
12735
|
-
estimatedTokens: estimatedTokens(content),
|
|
12736
|
-
sections: counts
|
|
12737
|
-
};
|
|
12738
|
-
}
|
|
12739
|
-
//#endregion
|
|
12740
13167
|
//#region packages/core/src/analyze/crawler/okf.ts
|
|
12741
13168
|
const OKF_DEFAULT_CONCEPTS = 500;
|
|
12742
13169
|
const OKF_MAX_CONCEPTS = 5e3;
|
|
@@ -25972,6 +26399,7 @@ function deleteClient(idOrName) {
|
|
|
25972
26399
|
const client = getClient(idOrName);
|
|
25973
26400
|
if (!client) return false;
|
|
25974
26401
|
config.clients = config.clients.filter((item) => item.id !== client.id);
|
|
26402
|
+
if (config.defaultSite === client.siteUrl && !config.clients.some((item) => item.siteUrl === client.siteUrl) && !config.sites.some((site) => site.siteUrl === client.siteUrl)) config.defaultSite = void 0;
|
|
25975
26403
|
writeConfig(config);
|
|
25976
26404
|
return true;
|
|
25977
26405
|
}
|
|
@@ -26764,6 +27192,6 @@ function isoDate(daysAgo = 0) {
|
|
|
26764
27192
|
//#region packages/core/src/version.ts
|
|
26765
27193
|
const SEO_VERSION = version;
|
|
26766
27194
|
//#endregion
|
|
26767
|
-
export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AUDIT_PAGE_RULE_IDS, BROWSER_USER_AGENT, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DataForSeoProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, LocalCredentialsProvider, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, affectedUrls, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, allocateIndexUrlsToProperties, analyzeAiReferralRows, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, boundedSitemapInventory, buildOkfBundle, buildPseoAuditReportFromRows, cannibalReport, changeMeasurementCsvRows, classifyCommunityIntent, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteTokens, deriveBrandTerms, detectBrandTerms, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, expectedCtrForPosition, explainOkfValidation, explainRule, fetchCruxFieldData, fetchPage, fetchSearchStatusIncidents, fetchSitemapUrls, fileSize, finalGscDateRange, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getPreviousRun, getPrivacySnapshot, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTokenStorageStatus, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isSkippableReportError, isoDate, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, linkRecover, linkRecoverRecommendation, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, loginWithLoopback, makeRunId, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLoadedCrawlReport, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, propertyMatchesUrl, providerPreferenceSchema, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, readConfig, readOauthClient, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, redirectTrace, refreshAuthToken, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, renderCrawlCsv, renderCrawlHtml, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, saveClient, saveCrawlReport, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, sessionSourceInListFilter, setDefaultClient, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, strikingDistance, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trafficAnomaly, unavailableSegmentImpactReport, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateOkfFiles, verifyQueryContent, workflowPresentation, writeConfig, writeOauthClient, writeTokens, xRobotsDirectives };
|
|
27195
|
+
export { AI_REFERRAL_SOURCES, AI_REFERRAL_SOURCES_VERSION, AI_SEARCH_SCORECARD_METHODOLOGY_ID, AI_SEARCH_SCORECARD_METHODOLOGY_VERSION, AUDIT_PAGE_RULE_IDS, BROWSER_USER_AGENT, CANNIBAL_MAXIMUM_DOMINANT_SHARE, CANNIBAL_MINIMUM_PAGE_SHARE, CRAWL_REPORT_STORAGE_VERSION, CTR_DEFAULT_LIMIT, CTR_DEFAULT_MIN_IMPRESSIONS, CTR_MAX_LIMIT, CTR_MAX_MIN_IMPRESSIONS, DataForSeoProvider, GSC_FINAL_DATA_LAG_DAYS, GSC_TIME_ZONE, INTERNAL_LINK_LEXICAL_TARGET_LIMIT, INTERNAL_LINK_MATCH_EVIDENCE_LIMIT, JsonFileStorageAdapter, LocalCredentialsProvider, OKF_DEFAULT_CONCEPTS, OKF_MAX_CONCEPTS, REPORT_BASELINE_MAX_AGE_MS, REPORT_BASELINE_MAX_DEPTH, REPORT_BASELINE_MAX_PAGES, SCORECARD_CHECK_WEIGHTS, SCORECARD_STATUS_CREDIT, SEO_VERSION, SERP_TITLE_REFERENCE_WIDTH_PX, SERP_TITLE_WIDTH_PROFILE, SemrushProvider, SeoError, ServiceAccountAccessTokenClient, SessionLedger, affectedUrls, aiPromptsForQuery, aiReadiness, aiReferralSourceForValue, aiReferralsReport, aiSearchScorecard, allocateIndexUrlsToProperties, analyzeAiReferralRows, analyzeCannibalRows, analyzeCtrUnderperformersFromRows, analyzeDecay, analyzeIndexCoverageSignals, analyzeInternalLinksFromRows, analyzePageOpportunitiesFromRows, analyzeQueryClustersFromRows, analyzeQuickWinsFromRows, analyzeSecondPageRows, analyzeSegmentRows, analyzeStrikingDistanceRows, analyzeTrafficAnomalyRows, assertUrlMatchesGscProperty, attachCrawlRecommendations, auditCrawlPages, auditCrawlRequests, auditLlmsTxt, auditPage, authStatus, boundedSitemapInventory, buildOkfBundle, buildPseoAuditReportFromRows, cannibalReport, changeMeasurementCsvRows, classifyCommunityIntent, clearCache, clientProfileSchema, clusterPseoTemplates, clusterQueryRows, combineRobotsValues, communityIntentReport, compareCrawlPages, compareCrawlReports, compareSegmentRows, configSchema, contentCoverageRecommendation, contentCoverageType, contentOptimizationFromPageOpportunities, contentOptimizationReport, countLabel, crawlConfigHash, crawlConfigSchema, crawlDefinitionId, crawlDiff, crawlFixToQueueItem, crawlImplementationQueueWorkflow, crawlIssueGroupSchema, crawlIssueSchema, crawlOne, crawlPageSnapshotSchema, crawlReportSchema, crawlReportStore, crawlReportSummarySchema, crawlRequestObservationSchema, crawlRunId, crawlSite, crawlTopFixSchema, crawlerJsonSchemas, crawlerRuleCategorySchema, crawlerRuleIdSchema, crawlerRuleInfoSchema, crawlerRuleRecommendationSchema, crawlerRuleSeveritySchema, crawlerSchemas, createAuthorizedClient, createContentGroup, createCrawlReport, createCtrBenchmarkContext, createGoogleAccessTokenClient, createLogger, createPageRenderer, ctrUnderperformersReport, decayClusterDrafts, decayComparisonRange, decayingReport, deleteChange, deleteClient, deleteContentGroup, deleteCrawlReport, deleteTokens, deriveBrandTerms, detectBrandTerms, diagnoseCsvFiles, diagnoseProperty, diagnosePropertyWorkflow, effectiveRobotsDirectives, effectiveSnippetControl, ensureSeoCliDirs, entityReadiness, estimateSerpTitleWidth, expectedCtrForPosition, explainOkfValidation, explainRule, fetchCruxFieldData, fetchPage, fetchSearchStatusIncidents, fetchSitemapUrls, fileSize, finalGscDateRange, findOverlappingSearchUpdates, formatRelativeExpiry, ga4MatchReason, ga4PropertyIdFromName, ga4ReportQualityWarnings, ga4RequestCanUseCache, ga4RowsToObjects, generateLlmsTxt, geoGaps, geoGapsReport, getAuthModeStatus, getCacheStats, getChange, getClient, getClientConfig, getContentGroup, getDb, getKeywordProvider, getPreviousRun, getPrivacySnapshot, getRepeatedLinkRecoverUrls, getRunPages, getSeoCliPaths, getServiceAccountConfig, getServiceAccountStatus, getTokenStorageStatus, groupCrawlIssues, hasMetaRobotsDirective, hasRule, hasXRobotsDirective, hashKey, indexCoveragePlan, indexCoverageSignals, indexMonitor, indexWatch, inferPseoTemplate, inferTemplateMovement, insertCrawlRun, insertLinkRecoverRun, inspectUrl, internalLinksReport, interpretUpdateCorrelation, isBrandQuery, isCompatibleTechnicalBaseline, isCurrentTechnicalBaseline, isSkippableReportError, isoDate, latestCrawlReport, latestCrawlSummaries, latestFinalGscDate, latestIndexWatchSummary, latestLinkRecoverSummary, linkRecover, linkRecoverRecommendation, listChanges, listClients, listContentGroups, listCrawlReports, listGa4AccountSummaries, listGa4DataStreams, listRules, listSearchUpdates, listSites, loadCrawlReport, localCredentialsProvider, loginWithLoopback, makeRunId, matchGa4WebStreams, measureChange, measureCoverage, metaRobotsDirectives, migrateLegacyMacConfig, missingOAuthClientMessage, missingSharedClientLoginMessage, monitoringStatus, monthlyReport, monthlyReportWorkflow, narrativeCsvFiles, normalizeCrawlConfig, normalizeForCoverage, normalizeHttpUrl, normalizeJavaScriptRenderingMode, normalizeLoadedCrawlReport, okfConceptLimit, pageEvidenceRecommendation, pageOpportunitiesReport, parseOlderThan, parsePseoPath, performanceAudit, performanceReportIsCacheable, planDueIndexUrls, planIndexCoverageFromUrls, plural, propertyMatchesUrl, providerPreferenceSchema, pseoAuditOptions, pseoAuditReport, pseoCsvFiles, pseoIndexStatus, pseoPresentation, publicHttpFetch, queryClusterReport, queryClusterTokens, queryContentCoverageFromPage, queryContentFieldsFromPage, queryFieldCoverage, queryOpportunityRecommendation, queryPageMetrics, queryPageTopQuery, queryPagesMetrics, queryPagesMetricsBatch, queryPagesTopQueries, queryPagesTopQueriesBatch, querySearchAnalytics, quickWinsCsvRows, quickWinsReport, readConfig, readOauthClient, readTokens, recommendCrawlDiffItem, recommendationForRule, recordChange, redirectTrace, refreshAuthToken, refreshPrioritiesCsvFiles, refreshPrioritiesWorkflow, renderCrawlCsv, renderCrawlHtml, renderCrawlMarkdownTickets, renderCrawlPagesCsv, renderCrawlPretty, renderCsv, renderPresentationTablesMarkdown, renderPseoMarkdown, renderWorkflowMarkdown, reportNarrative, reportPresentation, requestHeaders, resolveSegmentRanges, resolveSeoCliPaths, resolveTechnicalBaseline, reviewObservations, roundedPosition, runDoctor, runGa4Report, saveClient, saveCrawlReport, secondPage, segmentComparisonRange, segmentImpact, segmentRangeDays, selectAffectedUrls, selectDueIndexUrls, seoErrorEnvelope, seoToAiQueryReport, sessionSourceInListFilter, setDefaultClient, setTokenStorageMode, shiftIsoDate, shouldExcludeBrandQuery, siteSchema, sqliteCrawlReportStore, strikingDistance, summarizeCrawlReport, pageTechnicalSignals as technicalPageSignals, technicalWatchWorkflow, templateForUrl, toSeoError, tokenSchema, topCrawlRecommendations, topFixes, trafficAnomaly, unavailableSegmentImpactReport, unicodeTokens, updateCorrelation, updatePostmortemCsvFiles, updatePostmortemWorkflow, validateOkfFiles, verifyQueryContent, workflowPresentation, writeConfig, writeOauthClient, writeTokens, xRobotsDirectives };
|
|
26768
27196
|
|
|
26769
27197
|
//# sourceMappingURL=index.js.map
|