@pseolint/core 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 (223) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dist/algorithms/entity-mask.d.ts +3 -0
  4. package/dist/algorithms/entity-mask.d.ts.map +1 -0
  5. package/dist/algorithms/entity-mask.js +8 -0
  6. package/dist/algorithms/entity-mask.js.map +1 -0
  7. package/dist/algorithms/entity-mask.test.d.ts +2 -0
  8. package/dist/algorithms/entity-mask.test.d.ts.map +1 -0
  9. package/dist/algorithms/entity-mask.test.js +23 -0
  10. package/dist/algorithms/entity-mask.test.js.map +1 -0
  11. package/dist/algorithms/simhash.d.ts +4 -0
  12. package/dist/algorithms/simhash.d.ts.map +1 -0
  13. package/dist/algorithms/simhash.js +64 -0
  14. package/dist/algorithms/simhash.js.map +1 -0
  15. package/dist/algorithms/simhash.test.d.ts +2 -0
  16. package/dist/algorithms/simhash.test.d.ts.map +1 -0
  17. package/dist/algorithms/simhash.test.js +23 -0
  18. package/dist/algorithms/simhash.test.js.map +1 -0
  19. package/dist/algorithms/tf-idf.d.ts +8 -0
  20. package/dist/algorithms/tf-idf.d.ts.map +1 -0
  21. package/dist/algorithms/tf-idf.js +55 -0
  22. package/dist/algorithms/tf-idf.js.map +1 -0
  23. package/dist/auditor.d.ts +3 -0
  24. package/dist/auditor.d.ts.map +1 -0
  25. package/dist/auditor.js +730 -0
  26. package/dist/auditor.js.map +1 -0
  27. package/dist/auditor.test.d.ts +2 -0
  28. package/dist/auditor.test.d.ts.map +1 -0
  29. package/dist/auditor.test.js +134 -0
  30. package/dist/auditor.test.js.map +1 -0
  31. package/dist/enrich-findings.d.ts +9 -0
  32. package/dist/enrich-findings.d.ts.map +1 -0
  33. package/dist/enrich-findings.js +436 -0
  34. package/dist/enrich-findings.js.map +1 -0
  35. package/dist/formatters/console.d.ts +6 -0
  36. package/dist/formatters/console.d.ts.map +1 -0
  37. package/dist/formatters/console.js +237 -0
  38. package/dist/formatters/console.js.map +1 -0
  39. package/dist/formatters/html.d.ts +3 -0
  40. package/dist/formatters/html.d.ts.map +1 -0
  41. package/dist/formatters/html.js +170 -0
  42. package/dist/formatters/html.js.map +1 -0
  43. package/dist/formatters/index.d.ts +6 -0
  44. package/dist/formatters/index.d.ts.map +1 -0
  45. package/dist/formatters/index.js +5 -0
  46. package/dist/formatters/index.js.map +1 -0
  47. package/dist/formatters/json.d.ts +3 -0
  48. package/dist/formatters/json.d.ts.map +1 -0
  49. package/dist/formatters/json.js +4 -0
  50. package/dist/formatters/json.js.map +1 -0
  51. package/dist/formatters/markdown.d.ts +3 -0
  52. package/dist/formatters/markdown.d.ts.map +1 -0
  53. package/dist/formatters/markdown.js +93 -0
  54. package/dist/formatters/markdown.js.map +1 -0
  55. package/dist/index.d.ts +45 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +45 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/page-classifier.d.ts +4 -0
  60. package/dist/page-classifier.d.ts.map +1 -0
  61. package/dist/page-classifier.js +133 -0
  62. package/dist/page-classifier.js.map +1 -0
  63. package/dist/parser.d.ts +3 -0
  64. package/dist/parser.d.ts.map +1 -0
  65. package/dist/parser.js +131 -0
  66. package/dist/parser.js.map +1 -0
  67. package/dist/parser.test.d.ts +2 -0
  68. package/dist/parser.test.d.ts.map +1 -0
  69. package/dist/parser.test.js +37 -0
  70. package/dist/parser.test.js.map +1 -0
  71. package/dist/renderer.d.ts +15 -0
  72. package/dist/renderer.d.ts.map +1 -0
  73. package/dist/renderer.js +124 -0
  74. package/dist/renderer.js.map +1 -0
  75. package/dist/rule-references.d.ts +2 -0
  76. package/dist/rule-references.d.ts.map +1 -0
  77. package/dist/rule-references.js +35 -0
  78. package/dist/rule-references.js.map +1 -0
  79. package/dist/rules/cannibal/keyword-collision.d.ts +3 -0
  80. package/dist/rules/cannibal/keyword-collision.d.ts.map +1 -0
  81. package/dist/rules/cannibal/keyword-collision.js +25 -0
  82. package/dist/rules/cannibal/keyword-collision.js.map +1 -0
  83. package/dist/rules/cannibal/title-overlap.d.ts +3 -0
  84. package/dist/rules/cannibal/title-overlap.d.ts.map +1 -0
  85. package/dist/rules/cannibal/title-overlap.js +43 -0
  86. package/dist/rules/cannibal/title-overlap.js.map +1 -0
  87. package/dist/rules/cannibal/url-pattern.d.ts +3 -0
  88. package/dist/rules/cannibal/url-pattern.d.ts.map +1 -0
  89. package/dist/rules/cannibal/url-pattern.js +48 -0
  90. package/dist/rules/cannibal/url-pattern.js.map +1 -0
  91. package/dist/rules/content/eeat-signals.d.ts +3 -0
  92. package/dist/rules/content/eeat-signals.d.ts.map +1 -0
  93. package/dist/rules/content/eeat-signals.js +46 -0
  94. package/dist/rules/content/eeat-signals.js.map +1 -0
  95. package/dist/rules/content/heading-uniqueness.d.ts +3 -0
  96. package/dist/rules/content/heading-uniqueness.d.ts.map +1 -0
  97. package/dist/rules/content/heading-uniqueness.js +56 -0
  98. package/dist/rules/content/heading-uniqueness.js.map +1 -0
  99. package/dist/rules/content/meta-uniqueness.d.ts +3 -0
  100. package/dist/rules/content/meta-uniqueness.d.ts.map +1 -0
  101. package/dist/rules/content/meta-uniqueness.js +28 -0
  102. package/dist/rules/content/meta-uniqueness.js.map +1 -0
  103. package/dist/rules/content/missing-author.d.ts +3 -0
  104. package/dist/rules/content/missing-author.d.ts.map +1 -0
  105. package/dist/rules/content/missing-author.js +26 -0
  106. package/dist/rules/content/missing-author.js.map +1 -0
  107. package/dist/rules/content/unique-value.d.ts +3 -0
  108. package/dist/rules/content/unique-value.d.ts.map +1 -0
  109. package/dist/rules/content/unique-value.js +26 -0
  110. package/dist/rules/content/unique-value.js.map +1 -0
  111. package/dist/rules/links/cluster-connectivity.d.ts +7 -0
  112. package/dist/rules/links/cluster-connectivity.d.ts.map +1 -0
  113. package/dist/rules/links/cluster-connectivity.js +73 -0
  114. package/dist/rules/links/cluster-connectivity.js.map +1 -0
  115. package/dist/rules/links/cluster-key.d.ts +3 -0
  116. package/dist/rules/links/cluster-key.d.ts.map +1 -0
  117. package/dist/rules/links/cluster-key.js +22 -0
  118. package/dist/rules/links/cluster-key.js.map +1 -0
  119. package/dist/rules/links/dead-ends.d.ts +3 -0
  120. package/dist/rules/links/dead-ends.d.ts.map +1 -0
  121. package/dist/rules/links/dead-ends.js +13 -0
  122. package/dist/rules/links/dead-ends.js.map +1 -0
  123. package/dist/rules/links/hub-pages.d.ts +7 -0
  124. package/dist/rules/links/hub-pages.d.ts.map +1 -0
  125. package/dist/rules/links/hub-pages.js +73 -0
  126. package/dist/rules/links/hub-pages.js.map +1 -0
  127. package/dist/rules/links/link-depth.d.ts +3 -0
  128. package/dist/rules/links/link-depth.d.ts.map +1 -0
  129. package/dist/rules/links/link-depth.js +46 -0
  130. package/dist/rules/links/link-depth.js.map +1 -0
  131. package/dist/rules/links/orphan-pages.d.ts +3 -0
  132. package/dist/rules/links/orphan-pages.d.ts.map +1 -0
  133. package/dist/rules/links/orphan-pages.js +19 -0
  134. package/dist/rules/links/orphan-pages.js.map +1 -0
  135. package/dist/rules/schema/consistency.d.ts +3 -0
  136. package/dist/rules/schema/consistency.d.ts.map +1 -0
  137. package/dist/rules/schema/consistency.js +44 -0
  138. package/dist/rules/schema/consistency.js.map +1 -0
  139. package/dist/rules/schema/json-ld-valid.d.ts +3 -0
  140. package/dist/rules/schema/json-ld-valid.d.ts.map +1 -0
  141. package/dist/rules/schema/json-ld-valid.js +47 -0
  142. package/dist/rules/schema/json-ld-valid.js.map +1 -0
  143. package/dist/rules/schema/required-fields.d.ts +3 -0
  144. package/dist/rules/schema/required-fields.d.ts.map +1 -0
  145. package/dist/rules/schema/required-fields.js +60 -0
  146. package/dist/rules/schema/required-fields.js.map +1 -0
  147. package/dist/rules/spam/boilerplate-ratio.d.ts +3 -0
  148. package/dist/rules/spam/boilerplate-ratio.d.ts.map +1 -0
  149. package/dist/rules/spam/boilerplate-ratio.js +50 -0
  150. package/dist/rules/spam/boilerplate-ratio.js.map +1 -0
  151. package/dist/rules/spam/doorway-pattern.d.ts +4 -0
  152. package/dist/rules/spam/doorway-pattern.d.ts.map +1 -0
  153. package/dist/rules/spam/doorway-pattern.js +47 -0
  154. package/dist/rules/spam/doorway-pattern.js.map +1 -0
  155. package/dist/rules/spam/entity-swap.d.ts +7 -0
  156. package/dist/rules/spam/entity-swap.d.ts.map +1 -0
  157. package/dist/rules/spam/entity-swap.js +26 -0
  158. package/dist/rules/spam/entity-swap.js.map +1 -0
  159. package/dist/rules/spam/near-duplicate.d.ts +11 -0
  160. package/dist/rules/spam/near-duplicate.d.ts.map +1 -0
  161. package/dist/rules/spam/near-duplicate.js +25 -0
  162. package/dist/rules/spam/near-duplicate.js.map +1 -0
  163. package/dist/rules/spam/publication-velocity.d.ts +3 -0
  164. package/dist/rules/spam/publication-velocity.d.ts.map +1 -0
  165. package/dist/rules/spam/publication-velocity.js +25 -0
  166. package/dist/rules/spam/publication-velocity.js.map +1 -0
  167. package/dist/rules/spam/template-coverage.d.ts +3 -0
  168. package/dist/rules/spam/template-coverage.d.ts.map +1 -0
  169. package/dist/rules/spam/template-coverage.js +87 -0
  170. package/dist/rules/spam/template-coverage.js.map +1 -0
  171. package/dist/rules/spam/template-diversity.d.ts +3 -0
  172. package/dist/rules/spam/template-diversity.d.ts.map +1 -0
  173. package/dist/rules/spam/template-diversity.js +19 -0
  174. package/dist/rules/spam/template-diversity.js.map +1 -0
  175. package/dist/rules/spam/thin-content.d.ts +6 -0
  176. package/dist/rules/spam/thin-content.d.ts.map +1 -0
  177. package/dist/rules/spam/thin-content.js +22 -0
  178. package/dist/rules/spam/thin-content.js.map +1 -0
  179. package/dist/rules/tech/canonical-consistency.d.ts +4 -0
  180. package/dist/rules/tech/canonical-consistency.d.ts.map +1 -0
  181. package/dist/rules/tech/canonical-consistency.js +78 -0
  182. package/dist/rules/tech/canonical-consistency.js.map +1 -0
  183. package/dist/rules/tech/canonical-noindex-conflict.d.ts +3 -0
  184. package/dist/rules/tech/canonical-noindex-conflict.d.ts.map +1 -0
  185. package/dist/rules/tech/canonical-noindex-conflict.js +27 -0
  186. package/dist/rules/tech/canonical-noindex-conflict.js.map +1 -0
  187. package/dist/rules/tech/hreflang-consistency.d.ts +3 -0
  188. package/dist/rules/tech/hreflang-consistency.d.ts.map +1 -0
  189. package/dist/rules/tech/hreflang-consistency.js +99 -0
  190. package/dist/rules/tech/hreflang-consistency.js.map +1 -0
  191. package/dist/rules/tech/og-completeness.d.ts +3 -0
  192. package/dist/rules/tech/og-completeness.d.ts.map +1 -0
  193. package/dist/rules/tech/og-completeness.js +35 -0
  194. package/dist/rules/tech/og-completeness.js.map +1 -0
  195. package/dist/rules/tech/redirect-chain.d.ts +3 -0
  196. package/dist/rules/tech/redirect-chain.d.ts.map +1 -0
  197. package/dist/rules/tech/redirect-chain.js +20 -0
  198. package/dist/rules/tech/redirect-chain.js.map +1 -0
  199. package/dist/rules/tech/robots-noindex-conflict.d.ts +3 -0
  200. package/dist/rules/tech/robots-noindex-conflict.d.ts.map +1 -0
  201. package/dist/rules/tech/robots-noindex-conflict.js +30 -0
  202. package/dist/rules/tech/robots-noindex-conflict.js.map +1 -0
  203. package/dist/rules/tech/robots-sitemap-presence.d.ts +3 -0
  204. package/dist/rules/tech/robots-sitemap-presence.d.ts.map +1 -0
  205. package/dist/rules/tech/robots-sitemap-presence.js +61 -0
  206. package/dist/rules/tech/robots-sitemap-presence.js.map +1 -0
  207. package/dist/rules/tech/sitemap-completeness.d.ts +3 -0
  208. package/dist/rules/tech/sitemap-completeness.d.ts.map +1 -0
  209. package/dist/rules/tech/sitemap-completeness.js +40 -0
  210. package/dist/rules/tech/sitemap-completeness.js.map +1 -0
  211. package/dist/rules/tech/soft-404.d.ts +3 -0
  212. package/dist/rules/tech/soft-404.d.ts.map +1 -0
  213. package/dist/rules/tech/soft-404.js +24 -0
  214. package/dist/rules/tech/soft-404.js.map +1 -0
  215. package/dist/types.d.ts +170 -0
  216. package/dist/types.d.ts.map +1 -0
  217. package/dist/types.js +2 -0
  218. package/dist/types.js.map +1 -0
  219. package/dist/url-normalize.d.ts +10 -0
  220. package/dist/url-normalize.d.ts.map +1 -0
  221. package/dist/url-normalize.js +52 -0
  222. package/dist/url-normalize.js.map +1 -0
  223. package/package.json +46 -0
@@ -0,0 +1,25 @@
1
+ export function publicationVelocityRule(pages, maxPerDay) {
2
+ const byDay = new Map();
3
+ for (const page of pages) {
4
+ if (!page.publishedDate) {
5
+ continue;
6
+ }
7
+ const day = page.publishedDate.slice(0, 10);
8
+ const group = byDay.get(day) ?? [];
9
+ group.push(page);
10
+ byDay.set(day, group);
11
+ }
12
+ const findings = [];
13
+ for (const [day, dayPages] of byDay.entries()) {
14
+ if (dayPages.length > maxPerDay) {
15
+ findings.push({
16
+ ruleId: "spam/publication-velocity",
17
+ severity: "warning",
18
+ message: `${dayPages.length} pages share publish date ${day}, exceeding max/day ${maxPerDay}.`,
19
+ fix: "Stagger publication dates across pages to avoid appearing auto-generated."
20
+ });
21
+ }
22
+ }
23
+ return findings;
24
+ }
25
+ //# sourceMappingURL=publication-velocity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publication-velocity.js","sourceRoot":"","sources":["../../../src/rules/spam/publication-velocity.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,uBAAuB,CAAC,KAAmB,EAAE,SAAiB;IAC5E,MAAM,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC9C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;YACxB,SAAS;QACX,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjB,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACxB,CAAC;IAED,MAAM,QAAQ,GAAiB,EAAE,CAAC;IAClC,KAAK,MAAM,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC9C,IAAI,QAAQ,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;YAChC,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM,EAAE,2BAA2B;gBACnC,QAAQ,EAAE,SAAS;gBACnB,OAAO,EAAE,GAAG,QAAQ,CAAC,MAAM,6BAA6B,GAAG,uBAAuB,SAAS,GAAG;gBAC9F,GAAG,EAAE,2EAA2E;aACjF,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { EntityMaskPattern, ParsedPage, RuleResult } from "../../types.js";
2
+ export declare function templateCoverageRule(pages: ParsedPage[], entityPatterns: EntityMaskPattern[], minPages: number): RuleResult[];
3
+ //# sourceMappingURL=template-coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-coverage.d.ts","sourceRoot":"","sources":["../../../src/rules/spam/template-coverage.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAchF,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,UAAU,EAAE,EACnB,cAAc,EAAE,iBAAiB,EAAE,EACnC,QAAQ,EAAE,MAAM,GACf,UAAU,EAAE,CAiFd"}
@@ -0,0 +1,87 @@
1
+ import { maskEntities } from "../../algorithms/entity-mask.js";
2
+ import { clusterKeyForUrl } from "../links/cluster-key.js";
3
+ function extractFilename(url) {
4
+ let path;
5
+ try {
6
+ path = new URL(url).pathname;
7
+ }
8
+ catch {
9
+ path = url.replace(/\\/g, "/");
10
+ }
11
+ const stripped = path.replace(/\/+$/, "").replace(/\.[^.]+$/, "");
12
+ const lastSlash = stripped.lastIndexOf("/");
13
+ return lastSlash >= 0 ? stripped.slice(lastSlash + 1) : stripped;
14
+ }
15
+ export function templateCoverageRule(pages, entityPatterns, minPages) {
16
+ const byCluster = new Map();
17
+ for (const p of pages) {
18
+ const key = clusterKeyForUrl(p.url);
19
+ const list = byCluster.get(key) ?? [];
20
+ list.push(p);
21
+ byCluster.set(key, list);
22
+ }
23
+ const findings = [];
24
+ for (const [clusterDir, group] of byCluster) {
25
+ if (group.length < minPages)
26
+ continue;
27
+ const maskedFilenames = group.map((p) => {
28
+ const filename = extractFilename(p.url);
29
+ return maskEntities(filename, entityPatterns);
30
+ });
31
+ // Group by segment count (different segment counts = different template patterns)
32
+ const bySegmentCount = new Map();
33
+ for (const name of maskedFilenames) {
34
+ const tokens = name.split("-").filter(Boolean);
35
+ const count = tokens.length;
36
+ const list = bySegmentCount.get(count) ?? [];
37
+ list.push(name);
38
+ bySegmentCount.set(count, list);
39
+ }
40
+ for (const [, names] of bySegmentCount) {
41
+ if (names.length < 2)
42
+ continue;
43
+ const segmentCount = names[0].split("-").filter(Boolean).length;
44
+ if (segmentCount === 0)
45
+ continue;
46
+ const tokenSets = Array.from({ length: segmentCount }, () => new Set());
47
+ for (const name of names) {
48
+ const tokens = name.split("-").filter(Boolean);
49
+ for (let pos = 0; pos < tokens.length && pos < segmentCount; pos += 1) {
50
+ tokenSets[pos].add(tokens[pos]);
51
+ }
52
+ }
53
+ const dimensions = [];
54
+ for (let pos = 0; pos < tokenSets.length; pos += 1) {
55
+ if (tokenSets[pos].size > 1) {
56
+ const samples = Array.from(tokenSets[pos]).slice(0, 3);
57
+ dimensions.push({ position: pos, values: tokenSets[pos].size, samples });
58
+ }
59
+ }
60
+ // No template pattern if all tokens vary or no tokens vary
61
+ if (dimensions.length === 0)
62
+ continue;
63
+ if (dimensions.length === segmentCount)
64
+ continue;
65
+ const totalCombinations = dimensions.reduce((acc, d) => acc * d.values, 1);
66
+ const coverage = names.length / totalCombinations;
67
+ const coveragePct = (coverage * 100).toFixed(1);
68
+ const dimDesc = dimensions
69
+ .map((d) => {
70
+ const sampleStr = d.samples.join(", ");
71
+ return `${d.values} values (e.g. ${sampleStr})`;
72
+ })
73
+ .join(" x ");
74
+ findings.push({
75
+ ruleId: "spam/template-coverage",
76
+ severity: "info",
77
+ message: `${clusterDir} has ${names.length} pages across ${dimensions.length} dimensions: ${dimDesc}. Coverage: ${names.length} of ${totalCombinations} combinations (${coveragePct}%).`,
78
+ fix: totalCombinations > names.length * 5
79
+ ? "Low coverage suggests an overly broad template matrix. Consider narrowing dimensions to combinations you can differentiate with unique content."
80
+ : "Coverage is reasonable. Ensure each combination provides genuinely unique content.",
81
+ relatedUrls: group.map((p) => p.url).sort()
82
+ });
83
+ }
84
+ }
85
+ return findings;
86
+ }
87
+ //# sourceMappingURL=template-coverage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-coverage.js","sourceRoot":"","sources":["../../../src/rules/spam/template-coverage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { ParsedPage, RuleResult } from "../../types.js";
2
+ export declare function templateDiversityRule(pages: ParsedPage[], minUniqueRatio: number): RuleResult[];
3
+ //# sourceMappingURL=template-diversity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-diversity.d.ts","sourceRoot":"","sources":["../../../src/rules/spam/template-diversity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE7D,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,UAAU,EAAE,EACnB,cAAc,EAAE,MAAM,GACrB,UAAU,EAAE,CAmBd"}
@@ -0,0 +1,19 @@
1
+ export function templateDiversityRule(pages, minUniqueRatio) {
2
+ if (pages.length === 0) {
3
+ return [];
4
+ }
5
+ const unique = new Set(pages.map((page) => page.structureSignature)).size;
6
+ const ratio = unique / pages.length;
7
+ if (ratio >= minUniqueRatio) {
8
+ return [];
9
+ }
10
+ return [
11
+ {
12
+ ruleId: "spam/template-diversity",
13
+ severity: "warning",
14
+ message: `Template diversity ratio is ${ratio.toFixed(2)} (min ${minUniqueRatio.toFixed(2)}).`,
15
+ fix: "Vary the HTML structure across pages. Add conditional sections, different layouts, or page-specific components."
16
+ }
17
+ ];
18
+ }
19
+ //# sourceMappingURL=template-diversity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-diversity.js","sourceRoot":"","sources":["../../../src/rules/spam/template-diversity.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,qBAAqB,CACnC,KAAmB,EACnB,cAAsB;IAEtB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC;IAC1E,MAAM,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IACpC,IAAI,KAAK,IAAI,cAAc,EAAE,CAAC;QAC5B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO;QACL;YACE,MAAM,EAAE,yBAAyB;YACjC,QAAQ,EAAE,SAAS;YACnB,OAAO,EAAE,+BAA+B,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;YAC9F,GAAG,EAAE,iHAAiH;SACvH;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { ParsedPage, RuleResult } from "../../types.js";
2
+ export declare function thinContentRule(pages: ParsedPage[], minWords: number): {
3
+ findings: RuleResult[];
4
+ thinContentUrls: Set<string>;
5
+ };
6
+ //# sourceMappingURL=thin-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thin-content.d.ts","sourceRoot":"","sources":["../../../src/rules/spam/thin-content.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAM7D,wBAAgB,eAAe,CAC7B,KAAK,EAAE,UAAU,EAAE,EACnB,QAAQ,EAAE,MAAM,GACf;IAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;IAAC,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAoB1D"}
@@ -0,0 +1,22 @@
1
+ function countWords(text) {
2
+ return text.split(/\s+/).filter(Boolean).length;
3
+ }
4
+ export function thinContentRule(pages, minWords) {
5
+ const findings = [];
6
+ const thinContentUrls = new Set();
7
+ for (const page of pages) {
8
+ const words = countWords(page.contentText);
9
+ if (words >= minWords) {
10
+ continue;
11
+ }
12
+ thinContentUrls.add(page.url);
13
+ findings.push({
14
+ ruleId: "spam/thin-content",
15
+ severity: "error",
16
+ message: `${page.url} has thin content (${words} words).`,
17
+ fix: `Add at least ${minWords - words} more words of substantive content relevant to this page's specific topic.`
18
+ });
19
+ }
20
+ return { findings, thinContentUrls };
21
+ }
22
+ //# sourceMappingURL=thin-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thin-content.js","sourceRoot":"","sources":["../../../src/rules/spam/thin-content.ts"],"names":[],"mappings":"AAEA,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,KAAmB,EACnB,QAAgB;IAEhB,MAAM,QAAQ,GAAiB,EAAE,CAAC;IAClC,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;IAE1C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC3C,IAAI,KAAK,IAAI,QAAQ,EAAE,CAAC;YACtB,SAAS;QACX,CAAC;QAED,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC9B,QAAQ,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,mBAAmB;YAC3B,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,sBAAsB,KAAK,UAAU;YACzD,GAAG,EAAE,gBAAgB,QAAQ,GAAG,KAAK,4EAA4E;SAClH,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC;AACvC,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { NormalizeUrlOptions, ParsedPage, RuleResult } from "../../types.js";
2
+ export declare function resolveCanonicalUrl(canonical: string, pageUrl: string, normalizeOpts: NormalizeUrlOptions): string | null;
3
+ export declare function canonicalConsistencyRule(pages: ParsedPage[], knownUrls: Set<string>, normalizeOpts: NormalizeUrlOptions): RuleResult[];
4
+ //# sourceMappingURL=canonical-consistency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-consistency.d.ts","sourceRoot":"","sources":["../../../src/rules/tech/canonical-consistency.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGlF,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,mBAAmB,GACjC,MAAM,GAAG,IAAI,CAef;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,UAAU,EAAE,EACnB,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,EACtB,aAAa,EAAE,mBAAmB,GACjC,UAAU,EAAE,CA+Dd"}
@@ -0,0 +1,78 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { normalizeAuditUrl } from "../../url-normalize.js";
3
+ export function resolveCanonicalUrl(canonical, pageUrl, normalizeOpts) {
4
+ const raw = canonical.trim();
5
+ if (!raw)
6
+ return null;
7
+ if (/^https?:\/\//i.test(raw))
8
+ return normalizeAuditUrl(raw, normalizeOpts);
9
+ if (/^https?:\/\//i.test(pageUrl)) {
10
+ try {
11
+ return normalizeAuditUrl(new URL(raw, pageUrl).href, normalizeOpts);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ return normalizeAuditUrl(resolve(dirname(pageUrl), raw), normalizeOpts);
18
+ }
19
+ export function canonicalConsistencyRule(pages, knownUrls, normalizeOpts) {
20
+ const findings = [];
21
+ for (const page of pages) {
22
+ if (!page.canonical) {
23
+ findings.push({
24
+ ruleId: "tech/canonical-consistency",
25
+ severity: "error",
26
+ message: `${page.url} is missing a canonical URL.`,
27
+ pageUrl: page.url,
28
+ fix: `Add <link rel="canonical" href="${page.url}" /> to the <head>.`
29
+ });
30
+ continue;
31
+ }
32
+ const canonicalUrl = resolveCanonicalUrl(page.canonical, page.url, normalizeOpts);
33
+ if (!canonicalUrl) {
34
+ findings.push({
35
+ ruleId: "tech/canonical-consistency",
36
+ severity: "error",
37
+ message: `${page.url} has an invalid canonical URL: ${page.canonical}.`,
38
+ pageUrl: page.url,
39
+ fix: "Fix the canonical URL syntax."
40
+ });
41
+ continue;
42
+ }
43
+ if (canonicalUrl === page.url)
44
+ continue;
45
+ findings.push({
46
+ ruleId: "tech/canonical-consistency",
47
+ severity: knownUrls.has(canonicalUrl) ? "warning" : "info",
48
+ message: knownUrls.has(canonicalUrl)
49
+ ? `${page.url} canonicalizes to another crawled page (${canonicalUrl}).`
50
+ : `${page.url} canonicalizes outside the crawl scope (${canonicalUrl}).`,
51
+ pageUrl: page.url,
52
+ relatedUrls: [canonicalUrl],
53
+ fix: "Verify this canonical target is intentional."
54
+ });
55
+ // Check HTTP Link header for canonical
56
+ if (page.httpMeta?.linkHeader) {
57
+ const linkCanonicalMatch = page.httpMeta.linkHeader.match(/<([^>]+)>;\s*rel="canonical"/i);
58
+ if (linkCanonicalMatch) {
59
+ const httpCanonical = normalizeAuditUrl(linkCanonicalMatch[1], normalizeOpts);
60
+ const htmlCanonical = page.canonical
61
+ ? resolveCanonicalUrl(page.canonical, page.url, normalizeOpts)
62
+ : null;
63
+ if (httpCanonical && htmlCanonical && httpCanonical !== htmlCanonical) {
64
+ findings.push({
65
+ ruleId: "tech/canonical-consistency",
66
+ severity: "error",
67
+ message: `${page.url} has conflicting canonical URLs: HTML says ${htmlCanonical}, HTTP Link header says ${httpCanonical}.`,
68
+ pageUrl: page.url,
69
+ relatedUrls: [htmlCanonical, httpCanonical],
70
+ fix: "Ensure the HTML <link rel='canonical'> and HTTP Link header agree on the canonical URL."
71
+ });
72
+ }
73
+ }
74
+ }
75
+ }
76
+ return findings;
77
+ }
78
+ //# sourceMappingURL=canonical-consistency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-consistency.js","sourceRoot":"","sources":["../../../src/rules/tech/canonical-consistency.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { NormalizeUrlOptions, ParsedPage, RuleResult } from "../../types.js";
2
+ export declare function canonicalNoindexConflictRule(pages: ParsedPage[], normalizeOpts: NormalizeUrlOptions): RuleResult[];
3
+ //# sourceMappingURL=canonical-noindex-conflict.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-noindex-conflict.d.ts","sourceRoot":"","sources":["../../../src/rules/tech/canonical-noindex-conflict.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGlF,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,UAAU,EAAE,EACnB,aAAa,EAAE,mBAAmB,GACjC,UAAU,EAAE,CA4Bd"}
@@ -0,0 +1,27 @@
1
+ import { resolveCanonicalUrl } from "./canonical-consistency.js";
2
+ export function canonicalNoindexConflictRule(pages, normalizeOpts) {
3
+ const findings = [];
4
+ for (const page of pages) {
5
+ const robots = page.robotsMeta.toLowerCase();
6
+ if (!robots.includes("noindex") || !page.canonical) {
7
+ continue;
8
+ }
9
+ const canonicalUrl = resolveCanonicalUrl(page.canonical, page.url, normalizeOpts);
10
+ if (!canonicalUrl) {
11
+ continue;
12
+ }
13
+ if (canonicalUrl === page.url) {
14
+ continue;
15
+ }
16
+ findings.push({
17
+ ruleId: "tech/canonical-noindex-conflict",
18
+ severity: "warning",
19
+ message: `${page.url} is noindex but canonicalizes to ${canonicalUrl}.`,
20
+ pageUrl: page.url,
21
+ relatedUrls: [canonicalUrl],
22
+ fix: "Remove the noindex directive or change the canonical to self-reference."
23
+ });
24
+ }
25
+ return findings;
26
+ }
27
+ //# sourceMappingURL=canonical-noindex-conflict.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-noindex-conflict.js","sourceRoot":"","sources":["../../../src/rules/tech/canonical-noindex-conflict.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE,MAAM,UAAU,4BAA4B,CAC1C,KAAmB,EACnB,aAAkC;IAElC,MAAM,QAAQ,GAAiB,EAAE,CAAC;IAElC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;QAC7C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACnD,SAAS;QACX,CAAC;QAED,MAAM,YAAY,GAAG,mBAAmB,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;QAClF,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,SAAS;QACX,CAAC;QACD,IAAI,YAAY,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC;YAC9B,SAAS;QACX,CAAC;QAED,QAAQ,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,iCAAiC;YACzC,QAAQ,EAAE,SAAS;YACnB,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,oCAAoC,YAAY,GAAG;YACvE,OAAO,EAAE,IAAI,CAAC,GAAG;YACjB,WAAW,EAAE,CAAC,YAAY,CAAC;YAC3B,GAAG,EAAE,yEAAyE;SAC/E,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { NormalizeUrlOptions, ParsedPage, RuleResult } from "../../types.js";
2
+ export declare function hreflangConsistencyRule(pages: ParsedPage[], normalizeOpts: NormalizeUrlOptions): RuleResult[];
3
+ //# sourceMappingURL=hreflang-consistency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hreflang-consistency.d.ts","sourceRoot":"","sources":["../../../src/rules/tech/hreflang-consistency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGlF,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,UAAU,EAAE,EACnB,aAAa,EAAE,mBAAmB,GACjC,UAAU,EAAE,CA0Gd"}
@@ -0,0 +1,99 @@
1
+ import { normalizeAuditUrl } from "../../url-normalize.js";
2
+ export function hreflangConsistencyRule(pages, normalizeOpts) {
3
+ const findings = [];
4
+ const hreflangMap = new Map();
5
+ for (const page of pages) {
6
+ if (page.hreflangs.length === 0) {
7
+ continue;
8
+ }
9
+ const seen = new Set();
10
+ let hasXDefault = false;
11
+ for (const entry of page.hreflangs) {
12
+ const lang = entry.lang.toLowerCase();
13
+ if (lang === "x-default") {
14
+ hasXDefault = true;
15
+ }
16
+ if (seen.has(lang)) {
17
+ findings.push({
18
+ ruleId: "tech/hreflang-consistency",
19
+ severity: "warning",
20
+ message: `${page.url} has duplicate hreflang entry for ${entry.lang}.`,
21
+ pageUrl: page.url,
22
+ fix: `Remove the duplicate hreflang entry for ${entry.lang} on this page.`
23
+ });
24
+ }
25
+ seen.add(lang);
26
+ if (!entry.href) {
27
+ findings.push({
28
+ ruleId: "tech/hreflang-consistency",
29
+ severity: "warning",
30
+ message: `${page.url} has hreflang ${entry.lang} without an href.`,
31
+ pageUrl: page.url,
32
+ fix: `Add a valid href to the hreflang ${entry.lang} entry on this page.`
33
+ });
34
+ continue;
35
+ }
36
+ if (!/^https?:\/\//i.test(entry.href)) {
37
+ findings.push({
38
+ ruleId: "tech/hreflang-consistency",
39
+ severity: "warning",
40
+ message: `${page.url} has non-absolute hreflang href (${entry.href}) for ${entry.lang}.`,
41
+ pageUrl: page.url,
42
+ fix: `Change the hreflang href for ${entry.lang} to an absolute URL (starting with https://).`
43
+ });
44
+ continue;
45
+ }
46
+ const normalizedHref = normalizeAuditUrl(entry.href, normalizeOpts);
47
+ const pageRefs = hreflangMap.get(page.url) ?? new Map();
48
+ pageRefs.set(lang, normalizedHref);
49
+ hreflangMap.set(page.url, pageRefs);
50
+ }
51
+ if (!hasXDefault) {
52
+ findings.push({
53
+ ruleId: "tech/hreflang-consistency",
54
+ severity: "info",
55
+ message: `${page.url} has hreflang annotations but no x-default entry.`,
56
+ pageUrl: page.url,
57
+ fix: `Add <link rel="alternate" hreflang="x-default" href="..."> to specify a fallback URL for unmatched locales.`
58
+ });
59
+ }
60
+ }
61
+ const checkedPairs = new Set();
62
+ for (const [pageUrl, refs] of hreflangMap) {
63
+ for (const [lang, targetUrl] of refs) {
64
+ if (lang === "x-default")
65
+ continue;
66
+ if (targetUrl === pageUrl)
67
+ continue;
68
+ const pairKey = [pageUrl, targetUrl].sort().join("||");
69
+ if (checkedPairs.has(pairKey))
70
+ continue;
71
+ checkedPairs.add(pairKey);
72
+ const targetRefs = hreflangMap.get(targetUrl);
73
+ if (!targetRefs) {
74
+ findings.push({
75
+ ruleId: "tech/hreflang-consistency",
76
+ severity: "warning",
77
+ message: `${pageUrl} declares hreflang ${lang} pointing to ${targetUrl}, but that page has no hreflang annotations back.`,
78
+ pageUrl,
79
+ relatedUrls: [targetUrl],
80
+ fix: `Add a reciprocal hreflang annotation on ${targetUrl} pointing back to ${pageUrl}.`
81
+ });
82
+ continue;
83
+ }
84
+ const reciprocal = Array.from(targetRefs.values()).some((href) => href === pageUrl);
85
+ if (!reciprocal) {
86
+ findings.push({
87
+ ruleId: "tech/hreflang-consistency",
88
+ severity: "warning",
89
+ message: `${pageUrl} declares hreflang ${lang} to ${targetUrl}, but ${targetUrl} does not link back.`,
90
+ pageUrl,
91
+ relatedUrls: [targetUrl],
92
+ fix: `Add a reciprocal hreflang annotation on ${targetUrl} pointing back to ${pageUrl}.`
93
+ });
94
+ }
95
+ }
96
+ }
97
+ return findings;
98
+ }
99
+ //# sourceMappingURL=hreflang-consistency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hreflang-consistency.js","sourceRoot":"","sources":["../../../src/rules/tech/hreflang-consistency.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { ParsedPage, RuleResult } from "../../types.js";
2
+ export declare function ogCompletenessRule(pages: ParsedPage[]): RuleResult[];
3
+ //# sourceMappingURL=og-completeness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"og-completeness.d.ts","sourceRoot":"","sources":["../../../src/rules/tech/og-completeness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE7D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAiCpE"}
@@ -0,0 +1,35 @@
1
+ export function ogCompletenessRule(pages) {
2
+ const incomplete = [];
3
+ for (const page of pages) {
4
+ const missing = [];
5
+ if (!page.og.title)
6
+ missing.push("og:title");
7
+ if (!page.og.description)
8
+ missing.push("og:description");
9
+ if (!page.og.image)
10
+ missing.push("og:image");
11
+ if (missing.length > 0) {
12
+ incomplete.push({ url: page.url, missing });
13
+ }
14
+ }
15
+ if (incomplete.length === 0)
16
+ return [];
17
+ if (incomplete.length === pages.length && pages.length > 3) {
18
+ const allMissing = new Set(incomplete.flatMap((i) => i.missing));
19
+ return [{
20
+ ruleId: "tech/og-completeness",
21
+ severity: "warning",
22
+ message: `All ${incomplete.length} pages are missing Open Graph tags (${Array.from(allMissing).join(", ")}).`,
23
+ fix: `Add Open Graph tags site-wide: ${Array.from(allMissing).join(", ")}.`,
24
+ relatedUrls: incomplete.map((i) => i.url).sort()
25
+ }];
26
+ }
27
+ return incomplete.map((item) => ({
28
+ ruleId: "tech/og-completeness",
29
+ severity: "warning",
30
+ message: `${item.url} is missing ${item.missing.join(", ")}.`,
31
+ pageUrl: item.url,
32
+ fix: `Add the missing Open Graph tags: ${item.missing.join(", ")}.`
33
+ }));
34
+ }
35
+ //# sourceMappingURL=og-completeness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"og-completeness.js","sourceRoot":"","sources":["../../../src/rules/tech/og-completeness.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,kBAAkB,CAAC,KAAmB;IACpD,MAAM,UAAU,GAA8C,EAAE,CAAC;IAEjE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK;YAAE,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC7C,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,WAAW;YAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACzD,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK;YAAE,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC7C,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,UAAU,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;IAED,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEvC,IAAI,UAAU,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3D,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QACjE,OAAO,CAAC;gBACN,MAAM,EAAE,sBAAsB;gBAC9B,QAAQ,EAAE,SAAS;gBACnB,OAAO,EAAE,OAAO,UAAU,CAAC,MAAM,uCAAuC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;gBAC7G,GAAG,EAAE,kCAAkC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;gBAC3E,WAAW,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE;aACjD,CAAC,CAAC;IACL,CAAC;IAED,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC/B,MAAM,EAAE,sBAA+B;QACvC,QAAQ,EAAE,SAAkB;QAC5B,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,eAAe,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;QAC7D,OAAO,EAAE,IAAI,CAAC,GAAG;QACjB,GAAG,EAAE,oCAAoC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;KACpE,CAAC,CAAC,CAAC;AACN,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ParsedPage, RuleResult } from "../../types.js";
2
+ export declare function redirectChainRule(pages: ParsedPage[]): RuleResult[];
3
+ //# sourceMappingURL=redirect-chain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirect-chain.d.ts","sourceRoot":"","sources":["../../../src/rules/tech/redirect-chain.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE7D,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAmBnE"}
@@ -0,0 +1,20 @@
1
+ export function redirectChainRule(pages) {
2
+ const findings = [];
3
+ for (const page of pages) {
4
+ if (!page.httpMeta)
5
+ continue;
6
+ const hops = page.httpMeta.redirectChain.length;
7
+ if (hops <= 2)
8
+ continue;
9
+ findings.push({
10
+ ruleId: "tech/redirect-chain",
11
+ severity: "warning",
12
+ message: `${page.url} has a ${hops}-hop redirect chain before reaching ${page.httpMeta.finalUrl}.`,
13
+ pageUrl: page.url,
14
+ relatedUrls: [...page.httpMeta.redirectChain, page.httpMeta.finalUrl],
15
+ fix: `Reduce the redirect chain to a single hop. Update internal links and sitemap to point to ${page.httpMeta.finalUrl}.`
16
+ });
17
+ }
18
+ return findings;
19
+ }
20
+ //# sourceMappingURL=redirect-chain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirect-chain.js","sourceRoot":"","sources":["../../../src/rules/tech/redirect-chain.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,iBAAiB,CAAC,KAAmB;IACnD,MAAM,QAAQ,GAAiB,EAAE,CAAC;IAElC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE,SAAS;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC;QAChD,IAAI,IAAI,IAAI,CAAC;YAAE,SAAS;QAExB,QAAQ,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,qBAAqB;YAC7B,QAAQ,EAAE,SAAS;YACnB,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,UAAU,IAAI,uCAAuC,IAAI,CAAC,QAAQ,CAAC,QAAQ,GAAG;YAClG,OAAO,EAAE,IAAI,CAAC,GAAG;YACjB,WAAW,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACrE,GAAG,EAAE,4FAA4F,IAAI,CAAC,QAAQ,CAAC,QAAQ,GAAG;SAC3H,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ParsedPage, RuleResult } from "../../types.js";
2
+ export declare function robotsNoindexConflictRule(pages: ParsedPage[], inbound: Map<string, number>): RuleResult[];
3
+ //# sourceMappingURL=robots-noindex-conflict.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots-noindex-conflict.d.ts","sourceRoot":"","sources":["../../../src/rules/tech/robots-noindex-conflict.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE7D,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,UAAU,EAAE,EACnB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B,UAAU,EAAE,CAiCd"}
@@ -0,0 +1,30 @@
1
+ export function robotsNoindexConflictRule(pages, inbound) {
2
+ const findings = [];
3
+ for (const page of pages) {
4
+ const htmlRobots = page.robotsMeta.toLowerCase();
5
+ const httpRobots = (page.httpMeta?.xRobotsTag ?? "").toLowerCase();
6
+ const isNoindex = htmlRobots.includes("noindex") || httpRobots.includes("noindex");
7
+ if (!isNoindex) {
8
+ continue;
9
+ }
10
+ const source = htmlRobots.includes("noindex") && httpRobots.includes("noindex")
11
+ ? "both HTML meta and X-Robots-Tag"
12
+ : htmlRobots.includes("noindex")
13
+ ? "HTML meta"
14
+ : "X-Robots-Tag header";
15
+ const inboundCount = inbound.get(page.url) ?? 0;
16
+ findings.push({
17
+ ruleId: "tech/robots-noindex-conflict",
18
+ severity: inboundCount > 0 ? "warning" : "info",
19
+ message: inboundCount > 0
20
+ ? `${page.url} is marked noindex (via ${source}) but has ${inboundCount} inbound internal links.`
21
+ : `${page.url} is marked noindex (via ${source}).`,
22
+ pageUrl: page.url,
23
+ fix: inboundCount > 0
24
+ ? "Either remove noindex or remove internal links pointing to this page."
25
+ : "Verify this page should be noindexed."
26
+ });
27
+ }
28
+ return findings;
29
+ }
30
+ //# sourceMappingURL=robots-noindex-conflict.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots-noindex-conflict.js","sourceRoot":"","sources":["../../../src/rules/tech/robots-noindex-conflict.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,yBAAyB,CACvC,KAAmB,EACnB,OAA4B;IAE5B,MAAM,QAAQ,GAAiB,EAAE,CAAC;IAElC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;QACjD,MAAM,UAAU,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QACnE,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QACnF,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,SAAS;QACX,CAAC;QAED,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC;YAC7E,CAAC,CAAC,iCAAiC;YACnC,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAC9B,CAAC,CAAC,WAAW;gBACb,CAAC,CAAC,qBAAqB,CAAC;QAE5B,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChD,QAAQ,CAAC,IAAI,CAAC;YACZ,MAAM,EAAE,8BAA8B;YACtC,QAAQ,EAAE,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM;YAC/C,OAAO,EACL,YAAY,GAAG,CAAC;gBACd,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,2BAA2B,MAAM,aAAa,YAAY,0BAA0B;gBACjG,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,2BAA2B,MAAM,IAAI;YACtD,OAAO,EAAE,IAAI,CAAC,GAAG;YACjB,GAAG,EAAE,YAAY,GAAG,CAAC;gBACnB,CAAC,CAAC,uEAAuE;gBACzE,CAAC,CAAC,uCAAuC;SAC5C,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { RuleResult } from "../../types.js";
2
+ export declare function robotsSitemapPresenceRule(source: string): Promise<RuleResult[]>;
3
+ //# sourceMappingURL=robots-sitemap-presence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots-sitemap-presence.d.ts","sourceRoot":"","sources":["../../../src/rules/tech/robots-sitemap-presence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAcjD,wBAAsB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAqDrF"}