@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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/algorithms/entity-mask.d.ts +3 -0
- package/dist/algorithms/entity-mask.d.ts.map +1 -0
- package/dist/algorithms/entity-mask.js +8 -0
- package/dist/algorithms/entity-mask.js.map +1 -0
- package/dist/algorithms/entity-mask.test.d.ts +2 -0
- package/dist/algorithms/entity-mask.test.d.ts.map +1 -0
- package/dist/algorithms/entity-mask.test.js +23 -0
- package/dist/algorithms/entity-mask.test.js.map +1 -0
- package/dist/algorithms/simhash.d.ts +4 -0
- package/dist/algorithms/simhash.d.ts.map +1 -0
- package/dist/algorithms/simhash.js +64 -0
- package/dist/algorithms/simhash.js.map +1 -0
- package/dist/algorithms/simhash.test.d.ts +2 -0
- package/dist/algorithms/simhash.test.d.ts.map +1 -0
- package/dist/algorithms/simhash.test.js +23 -0
- package/dist/algorithms/simhash.test.js.map +1 -0
- package/dist/algorithms/tf-idf.d.ts +8 -0
- package/dist/algorithms/tf-idf.d.ts.map +1 -0
- package/dist/algorithms/tf-idf.js +55 -0
- package/dist/algorithms/tf-idf.js.map +1 -0
- package/dist/auditor.d.ts +3 -0
- package/dist/auditor.d.ts.map +1 -0
- package/dist/auditor.js +730 -0
- package/dist/auditor.js.map +1 -0
- package/dist/auditor.test.d.ts +2 -0
- package/dist/auditor.test.d.ts.map +1 -0
- package/dist/auditor.test.js +134 -0
- package/dist/auditor.test.js.map +1 -0
- package/dist/enrich-findings.d.ts +9 -0
- package/dist/enrich-findings.d.ts.map +1 -0
- package/dist/enrich-findings.js +436 -0
- package/dist/enrich-findings.js.map +1 -0
- package/dist/formatters/console.d.ts +6 -0
- package/dist/formatters/console.d.ts.map +1 -0
- package/dist/formatters/console.js +237 -0
- package/dist/formatters/console.js.map +1 -0
- package/dist/formatters/html.d.ts +3 -0
- package/dist/formatters/html.d.ts.map +1 -0
- package/dist/formatters/html.js +170 -0
- package/dist/formatters/html.js.map +1 -0
- package/dist/formatters/index.d.ts +6 -0
- package/dist/formatters/index.d.ts.map +1 -0
- package/dist/formatters/index.js +5 -0
- package/dist/formatters/index.js.map +1 -0
- package/dist/formatters/json.d.ts +3 -0
- package/dist/formatters/json.d.ts.map +1 -0
- package/dist/formatters/json.js +4 -0
- package/dist/formatters/json.js.map +1 -0
- package/dist/formatters/markdown.d.ts +3 -0
- package/dist/formatters/markdown.d.ts.map +1 -0
- package/dist/formatters/markdown.js +93 -0
- package/dist/formatters/markdown.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/page-classifier.d.ts +4 -0
- package/dist/page-classifier.d.ts.map +1 -0
- package/dist/page-classifier.js +133 -0
- package/dist/page-classifier.js.map +1 -0
- package/dist/parser.d.ts +3 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +131 -0
- package/dist/parser.js.map +1 -0
- package/dist/parser.test.d.ts +2 -0
- package/dist/parser.test.d.ts.map +1 -0
- package/dist/parser.test.js +37 -0
- package/dist/parser.test.js.map +1 -0
- package/dist/renderer.d.ts +15 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +124 -0
- package/dist/renderer.js.map +1 -0
- package/dist/rule-references.d.ts +2 -0
- package/dist/rule-references.d.ts.map +1 -0
- package/dist/rule-references.js +35 -0
- package/dist/rule-references.js.map +1 -0
- package/dist/rules/cannibal/keyword-collision.d.ts +3 -0
- package/dist/rules/cannibal/keyword-collision.d.ts.map +1 -0
- package/dist/rules/cannibal/keyword-collision.js +25 -0
- package/dist/rules/cannibal/keyword-collision.js.map +1 -0
- package/dist/rules/cannibal/title-overlap.d.ts +3 -0
- package/dist/rules/cannibal/title-overlap.d.ts.map +1 -0
- package/dist/rules/cannibal/title-overlap.js +43 -0
- package/dist/rules/cannibal/title-overlap.js.map +1 -0
- package/dist/rules/cannibal/url-pattern.d.ts +3 -0
- package/dist/rules/cannibal/url-pattern.d.ts.map +1 -0
- package/dist/rules/cannibal/url-pattern.js +48 -0
- package/dist/rules/cannibal/url-pattern.js.map +1 -0
- package/dist/rules/content/eeat-signals.d.ts +3 -0
- package/dist/rules/content/eeat-signals.d.ts.map +1 -0
- package/dist/rules/content/eeat-signals.js +46 -0
- package/dist/rules/content/eeat-signals.js.map +1 -0
- package/dist/rules/content/heading-uniqueness.d.ts +3 -0
- package/dist/rules/content/heading-uniqueness.d.ts.map +1 -0
- package/dist/rules/content/heading-uniqueness.js +56 -0
- package/dist/rules/content/heading-uniqueness.js.map +1 -0
- package/dist/rules/content/meta-uniqueness.d.ts +3 -0
- package/dist/rules/content/meta-uniqueness.d.ts.map +1 -0
- package/dist/rules/content/meta-uniqueness.js +28 -0
- package/dist/rules/content/meta-uniqueness.js.map +1 -0
- package/dist/rules/content/missing-author.d.ts +3 -0
- package/dist/rules/content/missing-author.d.ts.map +1 -0
- package/dist/rules/content/missing-author.js +26 -0
- package/dist/rules/content/missing-author.js.map +1 -0
- package/dist/rules/content/unique-value.d.ts +3 -0
- package/dist/rules/content/unique-value.d.ts.map +1 -0
- package/dist/rules/content/unique-value.js +26 -0
- package/dist/rules/content/unique-value.js.map +1 -0
- package/dist/rules/links/cluster-connectivity.d.ts +7 -0
- package/dist/rules/links/cluster-connectivity.d.ts.map +1 -0
- package/dist/rules/links/cluster-connectivity.js +73 -0
- package/dist/rules/links/cluster-connectivity.js.map +1 -0
- package/dist/rules/links/cluster-key.d.ts +3 -0
- package/dist/rules/links/cluster-key.d.ts.map +1 -0
- package/dist/rules/links/cluster-key.js +22 -0
- package/dist/rules/links/cluster-key.js.map +1 -0
- package/dist/rules/links/dead-ends.d.ts +3 -0
- package/dist/rules/links/dead-ends.d.ts.map +1 -0
- package/dist/rules/links/dead-ends.js +13 -0
- package/dist/rules/links/dead-ends.js.map +1 -0
- package/dist/rules/links/hub-pages.d.ts +7 -0
- package/dist/rules/links/hub-pages.d.ts.map +1 -0
- package/dist/rules/links/hub-pages.js +73 -0
- package/dist/rules/links/hub-pages.js.map +1 -0
- package/dist/rules/links/link-depth.d.ts +3 -0
- package/dist/rules/links/link-depth.d.ts.map +1 -0
- package/dist/rules/links/link-depth.js +46 -0
- package/dist/rules/links/link-depth.js.map +1 -0
- package/dist/rules/links/orphan-pages.d.ts +3 -0
- package/dist/rules/links/orphan-pages.d.ts.map +1 -0
- package/dist/rules/links/orphan-pages.js +19 -0
- package/dist/rules/links/orphan-pages.js.map +1 -0
- package/dist/rules/schema/consistency.d.ts +3 -0
- package/dist/rules/schema/consistency.d.ts.map +1 -0
- package/dist/rules/schema/consistency.js +44 -0
- package/dist/rules/schema/consistency.js.map +1 -0
- package/dist/rules/schema/json-ld-valid.d.ts +3 -0
- package/dist/rules/schema/json-ld-valid.d.ts.map +1 -0
- package/dist/rules/schema/json-ld-valid.js +47 -0
- package/dist/rules/schema/json-ld-valid.js.map +1 -0
- package/dist/rules/schema/required-fields.d.ts +3 -0
- package/dist/rules/schema/required-fields.d.ts.map +1 -0
- package/dist/rules/schema/required-fields.js +60 -0
- package/dist/rules/schema/required-fields.js.map +1 -0
- package/dist/rules/spam/boilerplate-ratio.d.ts +3 -0
- package/dist/rules/spam/boilerplate-ratio.d.ts.map +1 -0
- package/dist/rules/spam/boilerplate-ratio.js +50 -0
- package/dist/rules/spam/boilerplate-ratio.js.map +1 -0
- package/dist/rules/spam/doorway-pattern.d.ts +4 -0
- package/dist/rules/spam/doorway-pattern.d.ts.map +1 -0
- package/dist/rules/spam/doorway-pattern.js +47 -0
- package/dist/rules/spam/doorway-pattern.js.map +1 -0
- package/dist/rules/spam/entity-swap.d.ts +7 -0
- package/dist/rules/spam/entity-swap.d.ts.map +1 -0
- package/dist/rules/spam/entity-swap.js +26 -0
- package/dist/rules/spam/entity-swap.js.map +1 -0
- package/dist/rules/spam/near-duplicate.d.ts +11 -0
- package/dist/rules/spam/near-duplicate.d.ts.map +1 -0
- package/dist/rules/spam/near-duplicate.js +25 -0
- package/dist/rules/spam/near-duplicate.js.map +1 -0
- package/dist/rules/spam/publication-velocity.d.ts +3 -0
- package/dist/rules/spam/publication-velocity.d.ts.map +1 -0
- package/dist/rules/spam/publication-velocity.js +25 -0
- package/dist/rules/spam/publication-velocity.js.map +1 -0
- package/dist/rules/spam/template-coverage.d.ts +3 -0
- package/dist/rules/spam/template-coverage.d.ts.map +1 -0
- package/dist/rules/spam/template-coverage.js +87 -0
- package/dist/rules/spam/template-coverage.js.map +1 -0
- package/dist/rules/spam/template-diversity.d.ts +3 -0
- package/dist/rules/spam/template-diversity.d.ts.map +1 -0
- package/dist/rules/spam/template-diversity.js +19 -0
- package/dist/rules/spam/template-diversity.js.map +1 -0
- package/dist/rules/spam/thin-content.d.ts +6 -0
- package/dist/rules/spam/thin-content.d.ts.map +1 -0
- package/dist/rules/spam/thin-content.js +22 -0
- package/dist/rules/spam/thin-content.js.map +1 -0
- package/dist/rules/tech/canonical-consistency.d.ts +4 -0
- package/dist/rules/tech/canonical-consistency.d.ts.map +1 -0
- package/dist/rules/tech/canonical-consistency.js +78 -0
- package/dist/rules/tech/canonical-consistency.js.map +1 -0
- package/dist/rules/tech/canonical-noindex-conflict.d.ts +3 -0
- package/dist/rules/tech/canonical-noindex-conflict.d.ts.map +1 -0
- package/dist/rules/tech/canonical-noindex-conflict.js +27 -0
- package/dist/rules/tech/canonical-noindex-conflict.js.map +1 -0
- package/dist/rules/tech/hreflang-consistency.d.ts +3 -0
- package/dist/rules/tech/hreflang-consistency.d.ts.map +1 -0
- package/dist/rules/tech/hreflang-consistency.js +99 -0
- package/dist/rules/tech/hreflang-consistency.js.map +1 -0
- package/dist/rules/tech/og-completeness.d.ts +3 -0
- package/dist/rules/tech/og-completeness.d.ts.map +1 -0
- package/dist/rules/tech/og-completeness.js +35 -0
- package/dist/rules/tech/og-completeness.js.map +1 -0
- package/dist/rules/tech/redirect-chain.d.ts +3 -0
- package/dist/rules/tech/redirect-chain.d.ts.map +1 -0
- package/dist/rules/tech/redirect-chain.js +20 -0
- package/dist/rules/tech/redirect-chain.js.map +1 -0
- package/dist/rules/tech/robots-noindex-conflict.d.ts +3 -0
- package/dist/rules/tech/robots-noindex-conflict.d.ts.map +1 -0
- package/dist/rules/tech/robots-noindex-conflict.js +30 -0
- package/dist/rules/tech/robots-noindex-conflict.js.map +1 -0
- package/dist/rules/tech/robots-sitemap-presence.d.ts +3 -0
- package/dist/rules/tech/robots-sitemap-presence.d.ts.map +1 -0
- package/dist/rules/tech/robots-sitemap-presence.js +61 -0
- package/dist/rules/tech/robots-sitemap-presence.js.map +1 -0
- package/dist/rules/tech/sitemap-completeness.d.ts +3 -0
- package/dist/rules/tech/sitemap-completeness.d.ts.map +1 -0
- package/dist/rules/tech/sitemap-completeness.js +40 -0
- package/dist/rules/tech/sitemap-completeness.js.map +1 -0
- package/dist/rules/tech/soft-404.d.ts +3 -0
- package/dist/rules/tech/soft-404.d.ts.map +1 -0
- package/dist/rules/tech/soft-404.js +24 -0
- package/dist/rules/tech/soft-404.js.map +1 -0
- package/dist/types.d.ts +170 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/url-normalize.d.ts +10 -0
- package/dist/url-normalize.d.ts.map +1 -0
- package/dist/url-normalize.js +52 -0
- package/dist/url-normalize.js.map +1 -0
- 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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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"}
|