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.
Files changed (189) hide show
  1. package/PRIVACY.md +2 -2
  2. package/README.md +138 -42
  3. package/TERMS.md +2 -2
  4. package/TRADEMARKS.md +9 -5
  5. package/dist/cli.js +69 -52
  6. package/dist/cli.js.map +1 -1
  7. package/dist/{dist-CSY2_p-j.js → dist-CTBhWxn6.js} +49 -49
  8. package/dist/dist-CTBhWxn6.js.map +1 -0
  9. package/dist/{dist-FeAUNn9A.js → dist-eiewVfiS.js} +1 -1
  10. package/dist/index.d.ts +186 -94
  11. package/dist/index.js +843 -415
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp.d.ts +18 -1
  14. package/dist/mcp.js +6532 -4665
  15. package/dist/mcp.js.map +1 -1
  16. package/dist/{package-uzXzqDW6.js → package-BTOI9tde.js} +2 -2
  17. package/dist/{package-uzXzqDW6.js.map → package-BTOI9tde.js.map} +1 -1
  18. package/evals/README.md +117 -0
  19. package/evals/affected-urls.json +57 -0
  20. package/evals/ai-readiness.json +84 -0
  21. package/evals/ai-referrals.json +71 -0
  22. package/evals/ai-search-scorecard.json +70 -0
  23. package/evals/audit-page.json +57 -0
  24. package/evals/audit-urls.json +58 -0
  25. package/evals/cannibalisation.json +57 -0
  26. package/evals/community-intent.json +57 -0
  27. package/evals/compare-crawls.json +56 -0
  28. package/evals/content-optimization.json +57 -0
  29. package/evals/crawl-diff.json +58 -0
  30. package/evals/crawl-history.json +44 -0
  31. package/evals/crawl-report.json +44 -0
  32. package/evals/crawler-rules.json +44 -0
  33. package/evals/ctr-underperformers.json +57 -0
  34. package/evals/decaying-pages.json +57 -0
  35. package/evals/entity-readiness.json +57 -0
  36. package/evals/explain-crawl-issue.json +44 -0
  37. package/evals/generate-llms-txt.json +44 -0
  38. package/evals/geo-gaps.json +57 -0
  39. package/evals/index-coverage-plan.json +58 -0
  40. package/evals/index-coverage.json +70 -0
  41. package/evals/index-monitor.json +58 -0
  42. package/evals/index-watch.json +68 -0
  43. package/evals/internal-links.json +57 -0
  44. package/evals/link-recovery.json +58 -0
  45. package/evals/llms-txt-audit.json +44 -0
  46. package/evals/measure-change.json +58 -0
  47. package/evals/monthly-action-plan.json +56 -0
  48. package/evals/monthly-report.json +58 -0
  49. package/evals/narrative-report.json +58 -0
  50. package/evals/okf-build.json +44 -0
  51. package/evals/okf-validate.json +44 -0
  52. package/evals/page-opportunities.json +57 -0
  53. package/evals/performance-audit.json +57 -0
  54. package/evals/pseo-audit.json +84 -0
  55. package/evals/query-clusters.json +57 -0
  56. package/evals/quick-wins.json +86 -0
  57. package/evals/redirect-trace.json +57 -0
  58. package/evals/refresh-priorities.json +57 -0
  59. package/evals/search-performance-overview.json +86 -0
  60. package/evals/second-page.json +58 -0
  61. package/evals/segment-impact.json +57 -0
  62. package/evals/seo-fix-queue.json +85 -0
  63. package/evals/seo-geo-readiness.json +84 -0
  64. package/evals/seo-site-audit.json +85 -0
  65. package/evals/seo-to-ai-query.json +57 -0
  66. package/evals/seo.json +87 -0
  67. package/evals/setup-check.json +56 -0
  68. package/evals/site-crawl.json +44 -0
  69. package/evals/striking-distance.json +85 -0
  70. package/evals/technical-watch.json +57 -0
  71. package/evals/top-fixes.json +44 -0
  72. package/evals/traffic-anomaly.json +57 -0
  73. package/evals/update-correlation.json +57 -0
  74. package/evals/update-postmortem.json +57 -0
  75. package/package.json +5 -3
  76. package/skills/README.md +50 -61
  77. package/skills/seo/SKILL.md +88 -63
  78. package/skills/seo/agents/openai.yaml +2 -2
  79. package/dist/dist-CSY2_p-j.js.map +0 -1
  80. package/skills/affected-urls/SKILL.md +0 -39
  81. package/skills/affected-urls/agents/openai.yaml +0 -4
  82. package/skills/ai-readiness/SKILL.md +0 -34
  83. package/skills/ai-readiness/agents/openai.yaml +0 -4
  84. package/skills/ai-referrals/SKILL.md +0 -31
  85. package/skills/ai-referrals/agents/openai.yaml +0 -4
  86. package/skills/audit-page/SKILL.md +0 -50
  87. package/skills/audit-page/agents/openai.yaml +0 -4
  88. package/skills/audit-urls/SKILL.md +0 -39
  89. package/skills/audit-urls/agents/openai.yaml +0 -4
  90. package/skills/cannibalisation/SKILL.md +0 -31
  91. package/skills/cannibalisation/agents/openai.yaml +0 -4
  92. package/skills/community-intent/SKILL.md +0 -39
  93. package/skills/community-intent/agents/openai.yaml +0 -4
  94. package/skills/compare-crawls/SKILL.md +0 -40
  95. package/skills/compare-crawls/agents/openai.yaml +0 -4
  96. package/skills/content-optimization/SKILL.md +0 -31
  97. package/skills/content-optimization/agents/openai.yaml +0 -4
  98. package/skills/crawl-diff/SKILL.md +0 -51
  99. package/skills/crawl-diff/agents/openai.yaml +0 -4
  100. package/skills/crawl-history/SKILL.md +0 -37
  101. package/skills/crawl-history/agents/openai.yaml +0 -4
  102. package/skills/crawl-report/SKILL.md +0 -37
  103. package/skills/crawl-report/agents/openai.yaml +0 -4
  104. package/skills/crawler-rules/SKILL.md +0 -34
  105. package/skills/crawler-rules/agents/openai.yaml +0 -4
  106. package/skills/ctr-underperformers/SKILL.md +0 -31
  107. package/skills/ctr-underperformers/agents/openai.yaml +0 -4
  108. package/skills/decaying-pages/SKILL.md +0 -31
  109. package/skills/decaying-pages/agents/openai.yaml +0 -4
  110. package/skills/entity-readiness/SKILL.md +0 -34
  111. package/skills/entity-readiness/agents/openai.yaml +0 -4
  112. package/skills/explain-crawl-issue/SKILL.md +0 -32
  113. package/skills/explain-crawl-issue/agents/openai.yaml +0 -4
  114. package/skills/generate-llms-txt/SKILL.md +0 -37
  115. package/skills/generate-llms-txt/agents/openai.yaml +0 -4
  116. package/skills/geo-gaps/SKILL.md +0 -37
  117. package/skills/geo-gaps/agents/openai.yaml +0 -4
  118. package/skills/index-coverage/SKILL.md +0 -84
  119. package/skills/index-coverage/agents/openai.yaml +0 -4
  120. package/skills/index-coverage-plan/SKILL.md +0 -50
  121. package/skills/index-coverage-plan/agents/openai.yaml +0 -4
  122. package/skills/index-monitor/SKILL.md +0 -49
  123. package/skills/index-monitor/agents/openai.yaml +0 -4
  124. package/skills/index-watch/SKILL.md +0 -77
  125. package/skills/index-watch/agents/openai.yaml +0 -4
  126. package/skills/internal-links/SKILL.md +0 -31
  127. package/skills/internal-links/agents/openai.yaml +0 -4
  128. package/skills/link-recovery/SKILL.md +0 -51
  129. package/skills/link-recovery/agents/openai.yaml +0 -4
  130. package/skills/llms-txt-audit/SKILL.md +0 -34
  131. package/skills/llms-txt-audit/agents/openai.yaml +0 -4
  132. package/skills/measure-change/SKILL.md +0 -52
  133. package/skills/measure-change/agents/openai.yaml +0 -4
  134. package/skills/monthly-action-plan/SKILL.md +0 -50
  135. package/skills/monthly-action-plan/agents/openai.yaml +0 -4
  136. package/skills/monthly-report/SKILL.md +0 -50
  137. package/skills/monthly-report/agents/openai.yaml +0 -4
  138. package/skills/narrative-report/SKILL.md +0 -51
  139. package/skills/narrative-report/agents/openai.yaml +0 -4
  140. package/skills/okf-build/SKILL.md +0 -39
  141. package/skills/okf-build/agents/openai.yaml +0 -4
  142. package/skills/okf-validate/SKILL.md +0 -43
  143. package/skills/okf-validate/agents/openai.yaml +0 -4
  144. package/skills/page-opportunities/SKILL.md +0 -31
  145. package/skills/page-opportunities/agents/openai.yaml +0 -4
  146. package/skills/performance/SKILL.md +0 -50
  147. package/skills/performance/agents/openai.yaml +0 -4
  148. package/skills/performance-audit/SKILL.md +0 -31
  149. package/skills/performance-audit/agents/openai.yaml +0 -4
  150. package/skills/pseo-audit/SKILL.md +0 -103
  151. package/skills/pseo-audit/agents/openai.yaml +0 -4
  152. package/skills/query-clusters/SKILL.md +0 -31
  153. package/skills/query-clusters/agents/openai.yaml +0 -4
  154. package/skills/quick-wins/SKILL.md +0 -31
  155. package/skills/quick-wins/agents/openai.yaml +0 -4
  156. package/skills/redirect-trace/SKILL.md +0 -51
  157. package/skills/redirect-trace/agents/openai.yaml +0 -4
  158. package/skills/refresh-priorities/SKILL.md +0 -48
  159. package/skills/refresh-priorities/agents/openai.yaml +0 -4
  160. package/skills/search-performance-overview/SKILL.md +0 -48
  161. package/skills/search-performance-overview/agents/openai.yaml +0 -4
  162. package/skills/second-page/SKILL.md +0 -50
  163. package/skills/second-page/agents/openai.yaml +0 -4
  164. package/skills/segment-impact/SKILL.md +0 -50
  165. package/skills/segment-impact/agents/openai.yaml +0 -4
  166. package/skills/seo-fix-queue/SKILL.md +0 -50
  167. package/skills/seo-fix-queue/agents/openai.yaml +0 -4
  168. package/skills/seo-geo-readiness/SKILL.md +0 -60
  169. package/skills/seo-geo-readiness/agents/openai.yaml +0 -4
  170. package/skills/seo-site-audit/SKILL.md +0 -59
  171. package/skills/seo-site-audit/agents/openai.yaml +0 -4
  172. package/skills/seo-to-ai-query/SKILL.md +0 -31
  173. package/skills/seo-to-ai-query/agents/openai.yaml +0 -4
  174. package/skills/setup-check/SKILL.md +0 -49
  175. package/skills/setup-check/agents/openai.yaml +0 -4
  176. package/skills/site-crawl/SKILL.md +0 -39
  177. package/skills/site-crawl/agents/openai.yaml +0 -4
  178. package/skills/striking-distance/SKILL.md +0 -52
  179. package/skills/striking-distance/agents/openai.yaml +0 -4
  180. package/skills/technical-watch/SKILL.md +0 -49
  181. package/skills/technical-watch/agents/openai.yaml +0 -4
  182. package/skills/top-fixes/SKILL.md +0 -38
  183. package/skills/top-fixes/agents/openai.yaml +0 -4
  184. package/skills/traffic-anomaly/SKILL.md +0 -52
  185. package/skills/traffic-anomaly/agents/openai.yaml +0 -4
  186. package/skills/update-correlation/SKILL.md +0 -51
  187. package/skills/update-correlation/agents/openai.yaml +0 -4
  188. package/skills/update-postmortem/SKILL.md +0 -49
  189. 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-uzXzqDW6.js";
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>SEO Skills is connected</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
- SEO Skills
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. SEO Skills will help you choose a site and save a project profile before future reports.</p>
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>SEO Skills can read the Google data you choose. It cannot change your site or Google account.</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$2(part, total) {
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$2(botAccess.length - blockedBots.length, botAccess.length), 20) : 20 : 8,
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$2(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.",
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$2(semanticPages.length, pageCount)}% of indexable pages use semantic structure.` : "No indexable 2xx pages were available, so semantic HTML coverage was not evaluated.",
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$2(titlePages.length, pageCount)}% of indexable pages have title/H1 coverage and ${pct$2(metaPages.length, pageCount)}% have meta descriptions.` : "No indexable 2xx pages were available, so title, H1, and description coverage was not evaluated.",
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$2(langPages.length, pageCount)}% of indexable pages declare a language.` : "No indexable 2xx pages were available, so language declaration coverage was not evaluated.",
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$2(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.",
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$2(pages.length, report.pages.length || 1), 12),
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$2(pages.length, report.pages.length)}% of crawled pages are indexable 2xx pages.` : "No crawled pages were available, so status and indexability were not evaluated.",
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
- 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,
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
- 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
- ]
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$1(pattern, value) {
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$1(filters.url, issue.url)) return false;
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
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