@rivi-software/agentic-seo-check 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -2
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +149 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/core/audit.d.ts +72 -0
  7. package/dist/core/audit.js +337 -0
  8. package/dist/core/audit.js.map +1 -0
  9. package/dist/core/concurrency.d.ts +4 -0
  10. package/dist/core/concurrency.js +29 -0
  11. package/dist/core/concurrency.js.map +1 -0
  12. package/dist/core/crawl.d.ts +40 -0
  13. package/dist/core/crawl.js +195 -0
  14. package/dist/core/crawl.js.map +1 -0
  15. package/dist/core/fetch.d.ts +27 -0
  16. package/dist/core/fetch.js +135 -0
  17. package/dist/core/fetch.js.map +1 -0
  18. package/dist/core/html/extract.d.ts +10 -0
  19. package/dist/core/html/extract.js +150 -0
  20. package/dist/core/html/extract.js.map +1 -0
  21. package/dist/core/html/structured-data.d.ts +11 -0
  22. package/dist/core/html/structured-data.js +185 -0
  23. package/dist/core/html/structured-data.js.map +1 -0
  24. package/dist/core/html/text.d.ts +23 -0
  25. package/dist/core/html/text.js +149 -0
  26. package/dist/core/html/text.js.map +1 -0
  27. package/dist/core/lighthouse.d.ts +49 -0
  28. package/dist/core/lighthouse.js +140 -0
  29. package/dist/core/lighthouse.js.map +1 -0
  30. package/dist/core/performance-rules.d.ts +3 -0
  31. package/dist/core/performance-rules.js +53 -0
  32. package/dist/core/performance-rules.js.map +1 -0
  33. package/dist/core/render-rules.d.ts +7 -0
  34. package/dist/core/render-rules.js +77 -0
  35. package/dist/core/render-rules.js.map +1 -0
  36. package/dist/core/render.d.ts +22 -0
  37. package/dist/core/render.js +73 -0
  38. package/dist/core/render.js.map +1 -0
  39. package/dist/core/report/markdown.d.ts +2 -0
  40. package/dist/core/report/markdown.js +152 -0
  41. package/dist/core/report/markdown.js.map +1 -0
  42. package/dist/core/robots.d.ts +11 -0
  43. package/dist/core/robots.js +33 -0
  44. package/dist/core/robots.js.map +1 -0
  45. package/dist/core/rules/canonical.d.ts +2 -0
  46. package/dist/core/rules/canonical.js +105 -0
  47. package/dist/core/rules/canonical.js.map +1 -0
  48. package/dist/core/rules/content.d.ts +1 -0
  49. package/dist/core/rules/content.js +41 -0
  50. package/dist/core/rules/content.js.map +1 -0
  51. package/dist/core/rules/document.d.ts +1 -0
  52. package/dist/core/rules/document.js +60 -0
  53. package/dist/core/rules/document.js.map +1 -0
  54. package/dist/core/rules/headings.d.ts +1 -0
  55. package/dist/core/rules/headings.js +61 -0
  56. package/dist/core/rules/headings.js.map +1 -0
  57. package/dist/core/rules/hreflang.d.ts +1 -0
  58. package/dist/core/rules/hreflang.js +68 -0
  59. package/dist/core/rules/hreflang.js.map +1 -0
  60. package/dist/core/rules/images.d.ts +1 -0
  61. package/dist/core/rules/images.js +59 -0
  62. package/dist/core/rules/images.js.map +1 -0
  63. package/dist/core/rules/index.d.ts +14 -0
  64. package/dist/core/rules/index.js +76 -0
  65. package/dist/core/rules/index.js.map +1 -0
  66. package/dist/core/rules/indexing.d.ts +10 -0
  67. package/dist/core/rules/indexing.js +53 -0
  68. package/dist/core/rules/indexing.js.map +1 -0
  69. package/dist/core/rules/keyword.d.ts +1 -0
  70. package/dist/core/rules/keyword.js +74 -0
  71. package/dist/core/rules/keyword.js.map +1 -0
  72. package/dist/core/rules/links.d.ts +1 -0
  73. package/dist/core/rules/links.js +72 -0
  74. package/dist/core/rules/links.js.map +1 -0
  75. package/dist/core/rules/meta.d.ts +1 -0
  76. package/dist/core/rules/meta.js +60 -0
  77. package/dist/core/rules/meta.js.map +1 -0
  78. package/dist/core/rules/response.d.ts +1 -0
  79. package/dist/core/rules/response.js +59 -0
  80. package/dist/core/rules/response.js.map +1 -0
  81. package/dist/core/rules/rule.d.ts +42 -0
  82. package/dist/core/rules/rule.js +51 -0
  83. package/dist/core/rules/rule.js.map +1 -0
  84. package/dist/core/rules/social.d.ts +1 -0
  85. package/dist/core/rules/social.js +74 -0
  86. package/dist/core/rules/social.js.map +1 -0
  87. package/dist/core/rules/structured-data.d.ts +23 -0
  88. package/dist/core/rules/structured-data.js +169 -0
  89. package/dist/core/rules/structured-data.js.map +1 -0
  90. package/dist/core/rules/title.d.ts +1 -0
  91. package/dist/core/rules/title.js +59 -0
  92. package/dist/core/rules/title.js.map +1 -0
  93. package/dist/core/score.d.ts +3 -0
  94. package/dist/core/score.js +15 -0
  95. package/dist/core/score.js.map +1 -0
  96. package/dist/core/site-rules.d.ts +64 -0
  97. package/dist/core/site-rules.js +251 -0
  98. package/dist/core/site-rules.js.map +1 -0
  99. package/dist/core/sitemap.d.ts +13 -0
  100. package/dist/core/sitemap.js +95 -0
  101. package/dist/core/sitemap.js.map +1 -0
  102. package/dist/core/static.d.ts +9 -0
  103. package/dist/core/static.js +184 -0
  104. package/dist/core/static.js.map +1 -0
  105. package/dist/core/summary.d.ts +2 -0
  106. package/dist/core/summary.js +28 -0
  107. package/dist/core/summary.js.map +1 -0
  108. package/dist/core/types.d.ts +296 -0
  109. package/dist/core/types.js +26 -0
  110. package/dist/core/types.js.map +1 -0
  111. package/dist/core/url.d.ts +20 -0
  112. package/dist/core/url.js +139 -0
  113. package/dist/core/url.js.map +1 -0
  114. package/dist/index.d.ts +11 -0
  115. package/dist/index.js +10 -0
  116. package/dist/index.js.map +1 -0
  117. package/dist/mcp-bin.d.ts +2 -0
  118. package/dist/mcp-bin.js +7 -0
  119. package/dist/mcp-bin.js.map +1 -0
  120. package/dist/mcp.d.ts +5 -0
  121. package/dist/mcp.js +190 -0
  122. package/dist/mcp.js.map +1 -0
  123. package/dist/version.d.ts +2 -0
  124. package/dist/version.js +4 -0
  125. package/dist/version.js.map +1 -0
  126. package/package.json +89 -3
  127. package/skills/seo-audit/SKILL.md +53 -0
@@ -0,0 +1,68 @@
1
+ import { normalizeUrl } from '../url.js';
2
+ import { cap, defineRule, GOOGLE_DOCS } from './rule.js';
3
+ const HREFLANG_CODE = /^(x-default|[a-zA-Z]{2,3}(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|\d{3}))?)$/;
4
+ export const hreflangRules = [
5
+ defineRule({
6
+ id: 'hreflang.invalid-code',
7
+ category: 'international',
8
+ severity: 'warning',
9
+ description: 'hreflang values that are not valid ISO 639-1 language codes with an optional ISO 3166-1 region (or x-default).',
10
+ recommendation: 'Use codes like "en", "en-gb", "nl-nl", "zh-hant" or "x-default". Country codes alone ("uk", "eu") are invalid.',
11
+ docs: GOOGLE_DOCS.hreflang,
12
+ }, ({ page }, make) => {
13
+ const bad = page.hreflang.filter((h) => !HREFLANG_CODE.test(h.lang));
14
+ return bad.length ? make(`${bad.length} hreflang value(s) are invalid.`, { selector: 'link[rel="alternate"][hreflang]', evidence: { invalid: cap(bad.map((h) => h.lang)) } }) : null;
15
+ }),
16
+ defineRule({
17
+ id: 'hreflang.relative-url',
18
+ category: 'international',
19
+ severity: 'warning',
20
+ description: 'hreflang links must use absolute URLs.',
21
+ recommendation: 'Use fully qualified URLs (https://example.com/nl/) in every hreflang link.',
22
+ docs: GOOGLE_DOCS.hreflang,
23
+ }, ({ page }, make) => {
24
+ const rel = page.hreflang.filter((h) => !/^https?:\/\//i.test(h.href));
25
+ return rel.length ? make(`${rel.length} hreflang link(s) use relative URLs.`, { selector: 'link[rel="alternate"][hreflang]', evidence: { examples: cap(rel.map((h) => h.href)) } }) : null;
26
+ }),
27
+ defineRule({
28
+ id: 'hreflang.duplicate-lang',
29
+ category: 'international',
30
+ severity: 'warning',
31
+ description: 'The same hreflang value appears more than once with different URLs.',
32
+ recommendation: 'Each language/region code may map to exactly one URL per page.',
33
+ docs: GOOGLE_DOCS.hreflang,
34
+ }, ({ page }, make) => {
35
+ const seen = new Map();
36
+ for (const h of page.hreflang) {
37
+ const key = h.lang.toLowerCase();
38
+ if (!seen.has(key))
39
+ seen.set(key, new Set());
40
+ seen.get(key).add(h.url ?? h.href);
41
+ }
42
+ const dupes = [...seen.entries()].filter(([, urls]) => urls.size > 1).map(([lang]) => lang);
43
+ return dupes.length ? make(`hreflang codes with conflicting URLs: ${dupes.join(', ')}.`, { selector: 'link[rel="alternate"][hreflang]', evidence: { codes: dupes } }) : null;
44
+ }),
45
+ defineRule({
46
+ id: 'hreflang.missing-self',
47
+ category: 'international',
48
+ severity: 'warning',
49
+ description: 'The hreflang set does not include this page itself (self-referencing entry).',
50
+ recommendation: 'Every page in an hreflang cluster must list itself as well as its alternates, otherwise the annotations may be ignored.',
51
+ docs: GOOGLE_DOCS.hreflang,
52
+ }, ({ page }, make) => {
53
+ if (page.hreflang.length === 0)
54
+ return null;
55
+ const self = new Set([normalizeUrl(page.url), page.canonical ? normalizeUrl(page.canonical) : null].filter(Boolean));
56
+ const hasSelf = page.hreflang.some((h) => h.url && self.has(normalizeUrl(h.url)));
57
+ return hasSelf ? null : make('hreflang set has no entry pointing at this page.', { selector: 'link[rel="alternate"][hreflang]', evidence: { page: page.url, alternates: cap(page.hreflang.map((h) => `${h.lang} → ${h.href}`)) } });
58
+ }),
59
+ defineRule({
60
+ id: 'hreflang.missing-x-default',
61
+ category: 'international',
62
+ severity: 'info',
63
+ description: 'The hreflang set has no x-default entry.',
64
+ recommendation: 'Add <link rel="alternate" hreflang="x-default" href="..."> pointing at the language-selector or default version.',
65
+ docs: GOOGLE_DOCS.hreflang,
66
+ }, ({ page }, make) => (page.hreflang.length > 0 && !page.hreflang.some((h) => h.lang.toLowerCase() === 'x-default') ? make('hreflang set has no x-default entry.', { selector: 'link[rel="alternate"][hreflang]' }) : null)),
67
+ ];
68
+ //# sourceMappingURL=hreflang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hreflang.js","sourceRoot":"","sources":["../../../src/core/rules/hreflang.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare const imageRules: import("./rule.js").Rule[];
@@ -0,0 +1,59 @@
1
+ import { cap, defineRule, GOOGLE_DOCS } from './rule.js';
2
+ const FILENAME_ALT = /^(img|image|dsc|dscn|dcim|photo|screenshot|screen shot|untitled)[-_ ]?\d*|\.(jpe?g|png|gif|webp|avif|svg)$/i;
3
+ export const imageRules = [
4
+ defineRule({
5
+ id: 'img.alt-missing',
6
+ category: 'images',
7
+ severity: 'warning',
8
+ description: 'Images without an alt attribute.',
9
+ recommendation: 'Give every <img> an alt attribute: a short description for meaningful images, alt="" for purely decorative ones. Alt text is used for image search and accessibility.',
10
+ docs: GOOGLE_DOCS.images,
11
+ }, ({ page }, make) => {
12
+ const missing = page.images.filter((i) => i.alt === null);
13
+ return missing.length
14
+ ? make(`${missing.length} of ${page.images.length} images have no alt attribute.`, {
15
+ selector: 'img:not([alt])',
16
+ evidence: { count: missing.length, total: page.images.length, examples: cap(missing.map((i) => i.src ?? '(no src)')) },
17
+ })
18
+ : null;
19
+ }),
20
+ defineRule({
21
+ id: 'img.alt-filename',
22
+ category: 'images',
23
+ severity: 'info',
24
+ description: 'Image alt text looks like a filename (IMG_1234.jpg, screenshot.png).',
25
+ recommendation: 'Replace filename-style alt text with a description of what the image shows.',
26
+ docs: GOOGLE_DOCS.images,
27
+ }, ({ page }, make) => {
28
+ const bad = page.images.filter((i) => i.alt && FILENAME_ALT.test(i.alt.trim()));
29
+ return bad.length ? make(`${bad.length} image(s) have filename-like alt text.`, { evidence: { examples: cap(bad.map((i) => ({ src: i.src, alt: i.alt }))) } }) : null;
30
+ }),
31
+ defineRule({
32
+ id: 'img.dimensions-missing',
33
+ category: 'images',
34
+ severity: 'info',
35
+ description: 'Images without explicit width and height attributes, which causes layout shift (CLS) while they load.',
36
+ recommendation: 'Add width and height attributes (or CSS aspect-ratio) to <img> elements so the browser can reserve space before the image loads.',
37
+ docs: GOOGLE_DOCS.cwv,
38
+ }, ({ page }, make) => {
39
+ const missing = page.images.filter((i) => i.src && (!i.width || !i.height));
40
+ return missing.length
41
+ ? make(`${missing.length} of ${page.images.length} images lack width/height attributes.`, {
42
+ selector: 'img:not([width]), img:not([height])',
43
+ evidence: { count: missing.length, examples: cap(missing.map((i) => i.src)) },
44
+ })
45
+ : null;
46
+ }),
47
+ defineRule({
48
+ id: 'img.src-missing',
49
+ category: 'images',
50
+ severity: 'info',
51
+ description: 'Images without a src attribute (usually lazy-loading via data-src without a fallback).',
52
+ recommendation: 'Use native loading="lazy" with a real src, or ensure the lazy-loader also works without JavaScript, so crawlers can discover the images.',
53
+ docs: GOOGLE_DOCS.images,
54
+ }, ({ page }, make) => {
55
+ const missing = page.images.filter((i) => !i.src);
56
+ return missing.length ? make(`${missing.length} image(s) have no src attribute.`, { selector: 'img:not([src])', evidence: { count: missing.length } }) : null;
57
+ }),
58
+ ];
59
+ //# sourceMappingURL=images.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"images.js","sourceRoot":"","sources":["../../../src/core/rules/images.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { Finding, RuleMeta } from '../types.js';
2
+ import type { PageContext, Rule } from './rule.js';
3
+ export type { PageContext, Rule } from './rule.js';
4
+ /** Rules evaluated against a single HTML document. */
5
+ export declare const PAGE_RULES: Rule[];
6
+ /** Metadata for every rule the tool can emit, including rules implemented outside the page-rule loop. */
7
+ export declare const ALL_RULES: RuleMeta[];
8
+ export interface RunRulesOptions {
9
+ /** Rule ids to skip. */
10
+ disable?: Set<string>;
11
+ }
12
+ export declare function runPageRules(ctx: PageContext, options?: RunRulesOptions): Finding[];
13
+ export declare function sortFindings(findings: Finding[]): Finding[];
14
+ export declare function findRule(id: string): RuleMeta | undefined;
@@ -0,0 +1,76 @@
1
+ import { canonicalRules } from './canonical.js';
2
+ import { contentRules } from './content.js';
3
+ import { documentRules } from './document.js';
4
+ import { headingRules } from './headings.js';
5
+ import { hreflangRules } from './hreflang.js';
6
+ import { imageRules } from './images.js';
7
+ import { indexingRules } from './indexing.js';
8
+ import { keywordRules } from './keyword.js';
9
+ import { linkRules } from './links.js';
10
+ import { metaRules } from './meta.js';
11
+ import { responseRules } from './response.js';
12
+ import { socialRules } from './social.js';
13
+ import { structuredDataRules } from './structured-data.js';
14
+ import { titleRules } from './title.js';
15
+ import { RENDER_RULES } from '../render-rules.js';
16
+ import { PERFORMANCE_RULES } from '../performance-rules.js';
17
+ import { SITE_RULES } from '../site-rules.js';
18
+ /** Rules evaluated against a single HTML document. */
19
+ export const PAGE_RULES = [
20
+ ...titleRules,
21
+ ...metaRules,
22
+ ...headingRules,
23
+ ...canonicalRules,
24
+ ...indexingRules,
25
+ ...documentRules,
26
+ ...socialRules,
27
+ ...imageRules,
28
+ ...linkRules,
29
+ ...hreflangRules,
30
+ ...structuredDataRules,
31
+ ...contentRules,
32
+ ...keywordRules,
33
+ ...responseRules,
34
+ ];
35
+ /** Metadata for every rule the tool can emit, including rules implemented outside the page-rule loop. */
36
+ export const ALL_RULES = [
37
+ ...PAGE_RULES.map((r) => r.meta),
38
+ ...RENDER_RULES,
39
+ ...PERFORMANCE_RULES,
40
+ ...SITE_RULES,
41
+ ];
42
+ export function runPageRules(ctx, options = {}) {
43
+ const findings = [];
44
+ for (const rule of PAGE_RULES) {
45
+ if (options.disable?.has(rule.meta.id))
46
+ continue;
47
+ if (rule.meta.requires === 'keyword' && !ctx.keyword)
48
+ continue;
49
+ try {
50
+ for (const f of rule.check(ctx)) {
51
+ if (!f.url)
52
+ f.url = ctx.response?.finalUrl ?? ctx.page.url;
53
+ findings.push(f);
54
+ }
55
+ }
56
+ catch (err) {
57
+ findings.push({
58
+ rule: 'tool.rule-error',
59
+ severity: 'info',
60
+ category: 'tool',
61
+ message: `Rule ${rule.meta.id} failed: ${err.message}`,
62
+ recommendation: 'Report this as a bug at https://github.com/rivi-software/agentic-seo-check/issues; the rest of the audit is unaffected.',
63
+ url: ctx.page.url,
64
+ });
65
+ }
66
+ }
67
+ return sortFindings(findings);
68
+ }
69
+ export function sortFindings(findings) {
70
+ const rank = { error: 0, warning: 1, info: 2 };
71
+ return [...findings].sort((a, b) => rank[a.severity] - rank[b.severity] || a.rule.localeCompare(b.rule));
72
+ }
73
+ export function findRule(id) {
74
+ return ALL_RULES.find((r) => r.id === id);
75
+ }
76
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/rules/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAI9C,sDAAsD;AACtD,MAAM,CAAC,MAAM,UAAU,GAAW;IAChC,GAAG,UAAU;IACb,GAAG,SAAS;IACZ,GAAG,YAAY;IACf,GAAG,cAAc;IACjB,GAAG,aAAa;IAChB,GAAG,aAAa;IAChB,GAAG,WAAW;IACd,GAAG,UAAU;IACb,GAAG,SAAS;IACZ,GAAG,aAAa;IAChB,GAAG,mBAAmB;IACtB,GAAG,YAAY;IACf,GAAG,YAAY;IACf,GAAG,aAAa;CACjB,CAAC;AAEF,yGAAyG;AACzG,MAAM,CAAC,MAAM,SAAS,GAAe;IACnC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAChC,GAAG,YAAY;IACf,GAAG,iBAAiB;IACpB,GAAG,UAAU;CACd,CAAC;AAOF,MAAM,UAAU,YAAY,CAAC,GAAgB,EAAE,UAA2B,EAAE;IAC1E,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,IAAI,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAAE,SAAS;QACjD,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,GAAG,CAAC,OAAO;YAAE,SAAS;QAC/D,IAAI,CAAC;YACH,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;gBAChC,IAAI,CAAC,CAAC,CAAC,GAAG;oBAAE,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,QAAQ,EAAE,QAAQ,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC;gBAC3D,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACnB,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,iBAAiB;gBACvB,QAAQ,EAAE,MAAM;gBAChB,QAAQ,EAAE,MAAM;gBAChB,OAAO,EAAE,QAAQ,IAAI,CAAC,IAAI,CAAC,EAAE,YAAa,GAAa,CAAC,OAAO,EAAE;gBACjE,cAAc,EAAE,yHAAyH;gBACzI,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,GAAG;aAClB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,YAAY,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAmB;IAC9C,MAAM,IAAI,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAW,CAAC;IACxD,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAC3G,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,EAAU;IACjC,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { ExtractedPage, ResponseInfo } from '../types.js';
2
+ export interface RobotsDirectives {
3
+ noindex: boolean;
4
+ nofollow: boolean;
5
+ sources: string[];
6
+ directives: string[];
7
+ }
8
+ /** Combine <meta name="robots"> / googlebot and the X-Robots-Tag header. */
9
+ export declare function robotsDirectives(page: ExtractedPage, response?: ResponseInfo): RobotsDirectives;
10
+ export declare const indexingRules: import("./rule.js").Rule[];
@@ -0,0 +1,53 @@
1
+ import { defineRule, GOOGLE_DOCS } from './rule.js';
2
+ /** Combine <meta name="robots"> / googlebot and the X-Robots-Tag header. */
3
+ export function robotsDirectives(page, response) {
4
+ const directives = [];
5
+ const sources = [];
6
+ for (const content of page.metaRobots) {
7
+ for (const d of content.split(',').map((s) => s.trim()).filter(Boolean)) {
8
+ directives.push(d);
9
+ sources.push('meta');
10
+ }
11
+ }
12
+ const header = response?.headers['x-robots-tag'];
13
+ if (header) {
14
+ for (const d of header.toLowerCase().split(',').map((s) => s.trim()).filter(Boolean)) {
15
+ // "googlebot: noindex" form
16
+ directives.push(d.includes(':') ? (d.split(':').pop() ?? d).trim() : d);
17
+ sources.push('header');
18
+ }
19
+ }
20
+ const noindex = directives.some((d) => d === 'noindex' || d === 'none');
21
+ const nofollow = directives.some((d) => d === 'nofollow' || d === 'none');
22
+ return { noindex, nofollow, sources: [...new Set(sources)], directives: [...new Set(directives)] };
23
+ }
24
+ export const indexingRules = [
25
+ defineRule({
26
+ id: 'robots.noindex',
27
+ category: 'indexing',
28
+ severity: 'warning',
29
+ description: 'The page tells search engines not to index it (meta robots or X-Robots-Tag).',
30
+ recommendation: 'If this page should appear in search results, remove the noindex directive. Preview/staging hosts often add X-Robots-Tag: noindex at the platform level; verify on production. If the page is intentionally private (login, thank-you, internal search), this is correct.',
31
+ docs: GOOGLE_DOCS.robotsMeta,
32
+ }, ({ page, response }, make) => {
33
+ const r = robotsDirectives(page, response);
34
+ return r.noindex
35
+ ? make(`Page is set to noindex via ${r.sources.join(' and ')}.`, {
36
+ selector: r.sources.includes('meta') ? 'head > meta[name="robots"]' : undefined,
37
+ evidence: { directives: r.directives, sources: r.sources, xRobotsTag: response?.headers['x-robots-tag'] ?? null },
38
+ })
39
+ : null;
40
+ }),
41
+ defineRule({
42
+ id: 'robots.nofollow',
43
+ category: 'indexing',
44
+ severity: 'info',
45
+ description: 'The page tells search engines not to follow its links.',
46
+ recommendation: 'Remove nofollow unless you deliberately want link equity to stop at this page; it also hides linked pages from discovery.',
47
+ docs: GOOGLE_DOCS.robotsMeta,
48
+ }, ({ page, response }, make) => {
49
+ const r = robotsDirectives(page, response);
50
+ return r.nofollow ? make(`Page-level nofollow is set via ${r.sources.join(' and ')}.`, { evidence: { directives: r.directives } }) : null;
51
+ }),
52
+ ];
53
+ //# sourceMappingURL=indexing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexing.js","sourceRoot":"","sources":["../../../src/core/rules/indexing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare const keywordRules: import("./rule.js").Rule[];
@@ -0,0 +1,74 @@
1
+ import { includesKeyword, keywordOccurrences, normalizeForComparison, words } from '../html/text.js';
2
+ import { defineRule } from './rule.js';
3
+ export const keywordRules = [
4
+ defineRule({
5
+ id: 'keyword.not-in-title',
6
+ category: 'keyword',
7
+ severity: 'warning',
8
+ description: 'The target keyword does not appear in the title.',
9
+ recommendation: 'Include the target keyword (or a close variant) near the start of the title.',
10
+ requires: 'keyword',
11
+ }, ({ page, keyword }, make) => (keyword && !includesKeyword(page.title, keyword) ? make(`Title does not contain "${keyword}".`, { selector: 'head > title', evidence: { title: page.title, keyword } }) : null)),
12
+ defineRule({
13
+ id: 'keyword.not-in-h1',
14
+ category: 'keyword',
15
+ severity: 'warning',
16
+ description: 'The target keyword does not appear in any H1.',
17
+ recommendation: 'Use the target keyword in the H1 so the main heading matches the search intent.',
18
+ requires: 'keyword',
19
+ }, ({ page, keyword }, make) => (keyword && page.h1s.length > 0 && !page.h1s.some((h) => includesKeyword(h, keyword)) ? make(`No <h1> contains "${keyword}".`, { selector: 'h1', evidence: { h1s: page.h1s, keyword } }) : null)),
20
+ defineRule({
21
+ id: 'keyword.not-in-description',
22
+ category: 'keyword',
23
+ severity: 'info',
24
+ description: 'The target keyword does not appear in the meta description.',
25
+ recommendation: 'Mention the keyword in the meta description; matching terms are bolded in search snippets.',
26
+ requires: 'keyword',
27
+ }, ({ page, keyword }, make) => (keyword && page.metaDescription && !includesKeyword(page.metaDescription, keyword) ? make(`Meta description does not contain "${keyword}".`, { selector: 'head > meta[name="description"]', evidence: { keyword } }) : null)),
28
+ defineRule({
29
+ id: 'keyword.not-in-body',
30
+ category: 'keyword',
31
+ severity: 'warning',
32
+ description: 'The target keyword does not appear in the visible body text.',
33
+ recommendation: 'Use the keyword naturally in the body copy, ideally in the first paragraph.',
34
+ requires: 'keyword',
35
+ }, ({ page, keyword }, make) => (keyword && page.wordCount > 0 && keywordOccurrences(page.text, keyword) === 0 ? make(`Body text does not contain "${keyword}".`, { evidence: { keyword, wordCount: page.wordCount } }) : null)),
36
+ defineRule({
37
+ id: 'keyword.not-in-url',
38
+ category: 'keyword',
39
+ severity: 'info',
40
+ description: 'The target keyword does not appear in the URL path.',
41
+ recommendation: 'Where the URL is still changeable, use a short slug containing the keyword (hyphen-separated).',
42
+ requires: 'keyword',
43
+ }, ({ page, keyword, source }, make) => {
44
+ if (!keyword || source === 'html')
45
+ return null;
46
+ let pathname = page.url;
47
+ try {
48
+ pathname = decodeURIComponent(new URL(page.url).pathname);
49
+ }
50
+ catch {
51
+ /* keep */
52
+ }
53
+ const slug = pathname.replace(/[-_/.+]+/g, ' ');
54
+ return includesKeyword(slug, keyword) ? null : make(`URL path does not contain "${keyword}".`, { evidence: { path: pathname, keyword } });
55
+ }),
56
+ defineRule({
57
+ id: 'keyword.density-high',
58
+ category: 'keyword',
59
+ severity: 'info',
60
+ description: 'The target keyword makes up an unusually large share of the body text (default > 3%).',
61
+ recommendation: 'Reduce repetition; write for readers and use synonyms. Keyword stuffing can trigger spam filters.',
62
+ requires: 'keyword',
63
+ }, ({ page, keyword, thresholds }, make) => {
64
+ if (!keyword || page.wordCount < 50)
65
+ return null;
66
+ const occurrences = keywordOccurrences(page.text, keyword);
67
+ const kwWords = words(normalizeForComparison(keyword)).length || 1;
68
+ const density = ((occurrences * kwWords) / page.wordCount) * 100;
69
+ return density > thresholds.maxKeywordDensityPercent
70
+ ? make(`Keyword density is ${density.toFixed(1)}% (${occurrences} occurrences in ${page.wordCount} words).`, { evidence: { occurrences, wordCount: page.wordCount, densityPercent: Number(density.toFixed(2)) } })
71
+ : null;
72
+ }),
73
+ ];
74
+ //# sourceMappingURL=keyword.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyword.js","sourceRoot":"","sources":["../../../src/core/rules/keyword.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare const linkRules: import("./rule.js").Rule[];
@@ -0,0 +1,72 @@
1
+ import { isGenericAnchor } from '../html/text.js';
2
+ import { cap, defineRule, GOOGLE_DOCS } from './rule.js';
3
+ export const linkRules = [
4
+ defineRule({
5
+ id: 'links.no-internal',
6
+ category: 'links',
7
+ severity: 'warning',
8
+ description: 'The page has no internal links, so crawlers cannot reach other pages from it and it passes no authority onward.',
9
+ recommendation: 'Link to related pages and the main navigation with plain <a href> elements.',
10
+ docs: GOOGLE_DOCS.links,
11
+ }, ({ page }, make) => (page.links.some((l) => l.internal) ? null : make('Page contains no internal links.', { evidence: { totalLinks: page.links.length } }))),
12
+ defineRule({
13
+ id: 'links.empty-anchor',
14
+ category: 'links',
15
+ severity: 'warning',
16
+ description: 'Links without any accessible name (no text, aria-label, title or image alt).',
17
+ recommendation: 'Add descriptive link text, or aria-label for icon links. Empty links tell crawlers nothing about the target page.',
18
+ docs: GOOGLE_DOCS.links,
19
+ }, ({ page }, make) => {
20
+ const empty = page.links.filter((l) => l.url && !l.hasAccessibleName);
21
+ return empty.length ? make(`${empty.length} link(s) have no accessible text.`, { evidence: { count: empty.length, examples: cap(empty.map((l) => l.href)) } }) : null;
22
+ }),
23
+ defineRule({
24
+ id: 'links.generic-anchor-text',
25
+ category: 'links',
26
+ severity: 'info',
27
+ description: 'Links whose text is generic ("click here", "read more", "learn more").',
28
+ recommendation: 'Use anchor text that describes the destination, e.g. "pricing plans" instead of "click here".',
29
+ docs: GOOGLE_DOCS.links,
30
+ }, ({ page }, make) => {
31
+ const generic = page.links.filter((l) => l.url && l.text && isGenericAnchor(l.text));
32
+ return generic.length
33
+ ? make(`${generic.length} link(s) use generic anchor text.`, { evidence: { count: generic.length, examples: cap(generic.map((l) => ({ text: l.text, href: l.href }))) } })
34
+ : null;
35
+ }),
36
+ defineRule({
37
+ id: 'links.uncrawlable-href',
38
+ category: 'links',
39
+ severity: 'info',
40
+ description: 'Anchors whose href is "#", empty, or a javascript: URL. Crawlers cannot follow them.',
41
+ recommendation: 'Use <button> for actions. For navigation, give the anchor a real URL even when JavaScript intercepts the click.',
42
+ docs: GOOGLE_DOCS.links,
43
+ }, ({ page }, make) => {
44
+ const bad = page.links.filter((l) => l.href === '' || l.href === '#' || /^javascript:/i.test(l.href));
45
+ return bad.length ? make(`${bad.length} anchor(s) have an uncrawlable href.`, { evidence: { count: bad.length, examples: cap(bad.map((l) => ({ href: l.href, text: l.text }))) } }) : null;
46
+ }),
47
+ defineRule({
48
+ id: 'links.insecure-internal',
49
+ category: 'links',
50
+ severity: 'warning',
51
+ description: 'Internal links use http:// while the page is served over https://.',
52
+ recommendation: 'Update internal links to https:// (or use relative URLs) to avoid an extra redirect on every click and crawl.',
53
+ docs: GOOGLE_DOCS.redirects,
54
+ }, ({ page, source }, make) => {
55
+ if (source !== 'url' || !page.url.startsWith('https://'))
56
+ return null;
57
+ const insecure = page.links.filter((l) => l.internal && l.url?.startsWith('http://'));
58
+ return insecure.length ? make(`${insecure.length} internal link(s) point to http:// URLs.`, { evidence: { count: insecure.length, examples: cap(insecure.map((l) => l.url)) } }) : null;
59
+ }),
60
+ defineRule({
61
+ id: 'links.nofollow-internal',
62
+ category: 'links',
63
+ severity: 'info',
64
+ description: 'Internal links carry rel="nofollow".',
65
+ recommendation: 'Remove nofollow from internal links unless the target is intentionally kept out of the index; nofollow on internal links wastes crawl signals.',
66
+ docs: GOOGLE_DOCS.links,
67
+ }, ({ page }, make) => {
68
+ const nf = page.links.filter((l) => l.internal && l.nofollow);
69
+ return nf.length ? make(`${nf.length} internal link(s) are rel="nofollow".`, { evidence: { count: nf.length, examples: cap(nf.map((l) => l.url)) } }) : null;
70
+ }),
71
+ ];
72
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.js","sourceRoot":"","sources":["../../../src/core/rules/links.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare const metaRules: import("./rule.js").Rule[];
@@ -0,0 +1,60 @@
1
+ import { normalizeForComparison } from '../html/text.js';
2
+ import { defineRule, GOOGLE_DOCS } from './rule.js';
3
+ const SELECTOR = 'head > meta[name="description"]';
4
+ export const metaRules = [
5
+ defineRule({
6
+ id: 'meta-description.missing',
7
+ category: 'meta',
8
+ severity: 'warning',
9
+ description: 'No <meta name="description"> is present or it is empty.',
10
+ recommendation: 'Add a unique meta description (about 70-160 characters) that summarises the page and invites the click. Search engines may show it as the snippet.',
11
+ docs: GOOGLE_DOCS.snippets,
12
+ }, ({ page }, make) => (page.metaDescription ? null : make('Page has no meta description.', { selector: SELECTOR }))),
13
+ defineRule({
14
+ id: 'meta-description.multiple',
15
+ category: 'meta',
16
+ severity: 'warning',
17
+ description: 'More than one meta description is present.',
18
+ recommendation: 'Keep a single meta description; remove or merge the others.',
19
+ docs: GOOGLE_DOCS.snippets,
20
+ }, ({ page }, make) => page.metaDescriptions.length > 1
21
+ ? make(`Page has ${page.metaDescriptions.length} meta description tags.`, { selector: SELECTOR, evidence: { descriptions: page.metaDescriptions } })
22
+ : null),
23
+ defineRule({
24
+ id: 'meta-description.too-long',
25
+ category: 'meta',
26
+ severity: 'warning',
27
+ description: 'Meta description exceeds the configured maximum (default 160 characters) and will be truncated.',
28
+ recommendation: 'Trim the description to about 160 characters, front-loading the key message.',
29
+ docs: GOOGLE_DOCS.snippets,
30
+ }, ({ page, thresholds }, make) => page.metaDescription && page.metaDescription.length > thresholds.descriptionMaxChars
31
+ ? make(`Meta description is ${page.metaDescription.length} characters (max ${thresholds.descriptionMaxChars}).`, {
32
+ selector: SELECTOR,
33
+ evidence: { description: page.metaDescription, length: page.metaDescription.length, max: thresholds.descriptionMaxChars },
34
+ })
35
+ : null),
36
+ defineRule({
37
+ id: 'meta-description.too-short',
38
+ category: 'meta',
39
+ severity: 'info',
40
+ description: 'Meta description is shorter than the configured minimum (default 70 characters).',
41
+ recommendation: 'Expand the description so it summarises the page content and its value to the visitor.',
42
+ docs: GOOGLE_DOCS.snippets,
43
+ }, ({ page, thresholds }, make) => page.metaDescription && page.metaDescription.length < thresholds.descriptionMinChars
44
+ ? make(`Meta description is only ${page.metaDescription.length} characters (recommended at least ${thresholds.descriptionMinChars}).`, {
45
+ selector: SELECTOR,
46
+ evidence: { description: page.metaDescription, length: page.metaDescription.length, min: thresholds.descriptionMinChars },
47
+ })
48
+ : null),
49
+ defineRule({
50
+ id: 'meta-description.same-as-title',
51
+ category: 'meta',
52
+ severity: 'info',
53
+ description: 'Meta description is identical to the title.',
54
+ recommendation: 'Write a description that adds information beyond the title instead of repeating it.',
55
+ docs: GOOGLE_DOCS.snippets,
56
+ }, ({ page }, make) => page.metaDescription && page.title && normalizeForComparison(page.metaDescription) === normalizeForComparison(page.title)
57
+ ? make('Meta description repeats the title verbatim.', { selector: SELECTOR, evidence: { title: page.title } })
58
+ : null),
59
+ ];
60
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","sourceRoot":"","sources":["../../../src/core/rules/meta.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare const responseRules: import("./rule.js").Rule[];