@rivi-software/agentic-seo-check 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +210 -2
- package/dist/brand.d.ts +11 -0
- package/dist/brand.js +22 -0
- package/dist/brand.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +194 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/audit.d.ts +72 -0
- package/dist/core/audit.js +452 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/concurrency.d.ts +4 -0
- package/dist/core/concurrency.js +39 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/crawl.d.ts +40 -0
- package/dist/core/crawl.js +324 -0
- package/dist/core/crawl.js.map +1 -0
- package/dist/core/fetch.d.ts +27 -0
- package/dist/core/fetch.js +186 -0
- package/dist/core/fetch.js.map +1 -0
- package/dist/core/html/dom.d.ts +13 -0
- package/dist/core/html/dom.js +17 -0
- package/dist/core/html/dom.js.map +1 -0
- package/dist/core/html/extract.d.ts +10 -0
- package/dist/core/html/extract.js +207 -0
- package/dist/core/html/extract.js.map +1 -0
- package/dist/core/html/structured-data.d.ts +21 -0
- package/dist/core/html/structured-data.js +220 -0
- package/dist/core/html/structured-data.js.map +1 -0
- package/dist/core/html/text.d.ts +23 -0
- package/dist/core/html/text.js +152 -0
- package/dist/core/html/text.js.map +1 -0
- package/dist/core/lighthouse.d.ts +50 -0
- package/dist/core/lighthouse.js +216 -0
- package/dist/core/lighthouse.js.map +1 -0
- package/dist/core/performance-rules.d.ts +3 -0
- package/dist/core/performance-rules.js +125 -0
- package/dist/core/performance-rules.js.map +1 -0
- package/dist/core/render-rules.d.ts +8 -0
- package/dist/core/render-rules.js +144 -0
- package/dist/core/render-rules.js.map +1 -0
- package/dist/core/render.d.ts +22 -0
- package/dist/core/render.js +79 -0
- package/dist/core/render.js.map +1 -0
- package/dist/core/report/markdown.d.ts +2 -0
- package/dist/core/report/markdown.js +131 -0
- package/dist/core/report/markdown.js.map +1 -0
- package/dist/core/robots.d.ts +11 -0
- package/dist/core/robots.js +48 -0
- package/dist/core/robots.js.map +1 -0
- package/dist/core/rules/canonical.d.ts +2 -0
- package/dist/core/rules/canonical.js +141 -0
- package/dist/core/rules/canonical.js.map +1 -0
- package/dist/core/rules/content.d.ts +1 -0
- package/dist/core/rules/content.js +55 -0
- package/dist/core/rules/content.js.map +1 -0
- package/dist/core/rules/document.d.ts +1 -0
- package/dist/core/rules/document.js +83 -0
- package/dist/core/rules/document.js.map +1 -0
- package/dist/core/rules/headings.d.ts +1 -0
- package/dist/core/rules/headings.js +81 -0
- package/dist/core/rules/headings.js.map +1 -0
- package/dist/core/rules/hreflang.d.ts +1 -0
- package/dist/core/rules/hreflang.js +100 -0
- package/dist/core/rules/hreflang.js.map +1 -0
- package/dist/core/rules/images.d.ts +1 -0
- package/dist/core/rules/images.js +77 -0
- package/dist/core/rules/images.js.map +1 -0
- package/dist/core/rules/index.d.ts +14 -0
- package/dist/core/rules/index.js +77 -0
- package/dist/core/rules/index.js.map +1 -0
- package/dist/core/rules/indexing.d.ts +10 -0
- package/dist/core/rules/indexing.js +75 -0
- package/dist/core/rules/indexing.js.map +1 -0
- package/dist/core/rules/keyword.d.ts +1 -0
- package/dist/core/rules/keyword.js +111 -0
- package/dist/core/rules/keyword.js.map +1 -0
- package/dist/core/rules/links.d.ts +1 -0
- package/dist/core/rules/links.js +107 -0
- package/dist/core/rules/links.js.map +1 -0
- package/dist/core/rules/meta.d.ts +1 -0
- package/dist/core/rules/meta.js +81 -0
- package/dist/core/rules/meta.js.map +1 -0
- package/dist/core/rules/response.d.ts +1 -0
- package/dist/core/rules/response.js +93 -0
- package/dist/core/rules/response.js.map +1 -0
- package/dist/core/rules/rule.d.ts +42 -0
- package/dist/core/rules/rule.js +51 -0
- package/dist/core/rules/rule.js.map +1 -0
- package/dist/core/rules/social.d.ts +1 -0
- package/dist/core/rules/social.js +115 -0
- package/dist/core/rules/social.js.map +1 -0
- package/dist/core/rules/structured-data.d.ts +25 -0
- package/dist/core/rules/structured-data.js +381 -0
- package/dist/core/rules/structured-data.js.map +1 -0
- package/dist/core/rules/title.d.ts +1 -0
- package/dist/core/rules/title.js +74 -0
- package/dist/core/rules/title.js.map +1 -0
- package/dist/core/score.d.ts +3 -0
- package/dist/core/score.js +17 -0
- package/dist/core/score.js.map +1 -0
- package/dist/core/site-rules.d.ts +64 -0
- package/dist/core/site-rules.js +462 -0
- package/dist/core/site-rules.js.map +1 -0
- package/dist/core/sitemap.d.ts +13 -0
- package/dist/core/sitemap.js +140 -0
- package/dist/core/sitemap.js.map +1 -0
- package/dist/core/static.d.ts +9 -0
- package/dist/core/static.js +281 -0
- package/dist/core/static.js.map +1 -0
- package/dist/core/summary.d.ts +2 -0
- package/dist/core/summary.js +33 -0
- package/dist/core/summary.js.map +1 -0
- package/dist/core/types.d.ts +296 -0
- package/dist/core/types.js +33 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/url.d.ts +20 -0
- package/dist/core/url.js +191 -0
- package/dist/core/url.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-bin.d.ts +2 -0
- package/dist/mcp-bin.js +10 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/mcp.d.ts +5 -0
- package/dist/mcp.js +380 -0
- package/dist/mcp.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +107 -3
- package/skills/seo-audit/SKILL.md +53 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { cap, defineRule } from "./rule.js";
|
|
2
|
+
export const headingRules = [
|
|
3
|
+
defineRule({
|
|
4
|
+
id: "h1.missing",
|
|
5
|
+
category: "headings",
|
|
6
|
+
severity: "warning",
|
|
7
|
+
description: "The page has no <h1>.",
|
|
8
|
+
recommendation: "Add one <h1> that states the main topic of the page. It is usually the visible page heading.",
|
|
9
|
+
}, ({ page }, make) => page.h1s.length === 0
|
|
10
|
+
? make("Page has no <h1> heading.", { selector: "h1" })
|
|
11
|
+
: null),
|
|
12
|
+
defineRule({
|
|
13
|
+
id: "h1.multiple",
|
|
14
|
+
category: "headings",
|
|
15
|
+
severity: "info",
|
|
16
|
+
description: "The page has more than one <h1>.",
|
|
17
|
+
recommendation: "Prefer a single <h1> for the main topic and use <h2>-<h6> for sections. Multiple H1s are valid HTML but make the main topic less clear.",
|
|
18
|
+
}, ({ page }, make) => page.h1s.length > 1
|
|
19
|
+
? make(`Page has ${page.h1s.length} <h1> headings.`, {
|
|
20
|
+
selector: "h1",
|
|
21
|
+
evidence: { h1s: cap(page.h1s) },
|
|
22
|
+
})
|
|
23
|
+
: null),
|
|
24
|
+
defineRule({
|
|
25
|
+
id: "h1.empty",
|
|
26
|
+
category: "headings",
|
|
27
|
+
severity: "warning",
|
|
28
|
+
description: "An <h1> exists but contains no text.",
|
|
29
|
+
recommendation: "Put the page heading text inside the <h1>. If it wraps only an image, give the image descriptive alt text.",
|
|
30
|
+
}, ({ page }, make) => page.h1s.length > 0 && page.h1s.every((h) => !h)
|
|
31
|
+
? make("All <h1> elements are empty.", { selector: "h1" })
|
|
32
|
+
: null),
|
|
33
|
+
defineRule({
|
|
34
|
+
id: "h1.too-long",
|
|
35
|
+
category: "headings",
|
|
36
|
+
severity: "info",
|
|
37
|
+
description: "The <h1> is longer than the configured maximum (default 70 characters).",
|
|
38
|
+
recommendation: "Keep the H1 concise; move detail into the intro paragraph.",
|
|
39
|
+
}, ({ page, thresholds }, make) => {
|
|
40
|
+
const long = page.h1s.filter((h) => h.length > thresholds.h1MaxChars);
|
|
41
|
+
return long.length
|
|
42
|
+
? make(`${long.length} <h1> heading(s) exceed ${thresholds.h1MaxChars} characters.`, { selector: "h1", evidence: { h1s: cap(long) } })
|
|
43
|
+
: null;
|
|
44
|
+
}),
|
|
45
|
+
defineRule({
|
|
46
|
+
id: "headings.empty",
|
|
47
|
+
category: "headings",
|
|
48
|
+
severity: "warning",
|
|
49
|
+
description: "Heading elements (h2-h6) with no text content.",
|
|
50
|
+
recommendation: "Remove empty heading tags or fill them with text. Empty headings confuse outline parsers and screen readers.",
|
|
51
|
+
}, ({ page }, make) => {
|
|
52
|
+
const empty = page.headings.filter((h) => h.level > 1 && !h.text);
|
|
53
|
+
return empty.length
|
|
54
|
+
? make(`${empty.length} empty heading element(s) found.`, {
|
|
55
|
+
evidence: { levels: cap(empty.map((h) => `h${h.level}`)) },
|
|
56
|
+
})
|
|
57
|
+
: null;
|
|
58
|
+
}),
|
|
59
|
+
defineRule({
|
|
60
|
+
id: "headings.skipped-level",
|
|
61
|
+
category: "headings",
|
|
62
|
+
severity: "info",
|
|
63
|
+
description: "Heading levels are skipped (for example an <h2> followed directly by an <h4>).",
|
|
64
|
+
recommendation: "Use heading levels in order (h1 > h2 > h3) so the document outline reflects the content structure.",
|
|
65
|
+
}, ({ page }, make) => {
|
|
66
|
+
const skips = [];
|
|
67
|
+
let prev = 0;
|
|
68
|
+
for (const h of page.headings) {
|
|
69
|
+
if (prev > 0 && h.level > prev + 1) {
|
|
70
|
+
skips.push(`h${prev} → h${h.level} ("${h.text.slice(0, 40)}")`);
|
|
71
|
+
}
|
|
72
|
+
prev = h.level;
|
|
73
|
+
}
|
|
74
|
+
return skips.length
|
|
75
|
+
? make(`${skips.length} heading level skip(s) found.`, {
|
|
76
|
+
evidence: { skips: cap(skips) },
|
|
77
|
+
})
|
|
78
|
+
: null;
|
|
79
|
+
}),
|
|
80
|
+
];
|
|
81
|
+
//# sourceMappingURL=headings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headings.js","sourceRoot":"","sources":["../../../src/core/rules/headings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,UAAU,CACR;QACE,EAAE,EAAE,YAAY;QAChB,QAAQ,EAAE,UAAU;QACpB,QAAQ,EAAE,SAAS;QACnB,WAAW,EAAE,uBAAuB;QACpC,cAAc,EACZ,8FAA8F;KACjG,EACD,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,CACjB,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC;QACnB,CAAC,CAAC,IAAI,CAAC,2BAA2B,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QACvD,CAAC,CAAC,IAAI,CACX;IACD,UAAU,CACR;QACE,EAAE,EAAE,aAAa;QACjB,QAAQ,EAAE,UAAU;QACpB,QAAQ,EAAE,MAAM;QAChB,WAAW,EAAE,kCAAkC;QAC/C,cAAc,EACZ,yIAAyI;KAC5I,EACD,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,CACjB,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC;QACjB,CAAC,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,GAAG,CAAC,MAAM,iBAAiB,EAAE;YACjD,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;SACjC,CAAC;QACJ,CAAC,CAAC,IAAI,CACX;IACD,UAAU,CACR;QACE,EAAE,EAAE,UAAU;QACd,QAAQ,EAAE,UAAU;QACpB,QAAQ,EAAE,SAAS;QACnB,WAAW,EAAE,sCAAsC;QACnD,cAAc,EACZ,4GAA4G;KAC/G,EACD,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,CACjB,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QAC9C,CAAC,CAAC,IAAI,CAAC,8BAA8B,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAC1D,CAAC,CAAC,IAAI,CACX;IACD,UAAU,CACR;QACE,EAAE,EAAE,aAAa;QACjB,QAAQ,EAAE,UAAU;QACpB,QAAQ,EAAE,MAAM;QAChB,WAAW,EACT,yEAAyE;QAC3E,cAAc,EACZ,4DAA4D;KAC/D,EACD,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,EAAE;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;QAEtE,OAAO,IAAI,CAAC,MAAM;YAChB,CAAC,CAAC,IAAI,CACF,GAAG,IAAI,CAAC,MAAM,2BAA2B,UAAU,CAAC,UAAU,cAAc,EAC5E,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,CACjD;YACH,CAAC,CAAC,IAAI,CAAC;IACX,CAAC,CACF;IACD,UAAU,CACR;QACE,EAAE,EAAE,gBAAgB;QACpB,QAAQ,EAAE,UAAU;QACpB,QAAQ,EAAE,SAAS;QACnB,WAAW,EAAE,gDAAgD;QAC7D,cAAc,EACZ,8GAA8G;KACjH,EACD,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE;QACjB,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAElE,OAAO,KAAK,CAAC,MAAM;YACjB,CAAC,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,kCAAkC,EAAE;gBACtD,QAAQ,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE;aAC3D,CAAC;YACJ,CAAC,CAAC,IAAI,CAAC;IACX,CAAC,CACF;IACD,UAAU,CACR;QACE,EAAE,EAAE,wBAAwB;QAC5B,QAAQ,EAAE,UAAU;QACpB,QAAQ,EAAE,MAAM;QAChB,WAAW,EACT,gFAAgF;QAClF,cAAc,EACZ,oGAAoG;KACvG,EACD,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE;QACjB,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,IAAI,GAAG,CAAC,CAAC;QAEb,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC9B,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC;gBACnC,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;YAClE,CAAC;YAED,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC;QACjB,CAAC;QAED,OAAO,KAAK,CAAC,MAAM;YACjB,CAAC,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,+BAA+B,EAAE;gBACnD,QAAQ,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE;aAChC,CAAC;YACJ,CAAC,CAAC,IAAI,CAAC;IACX,CAAC,CACF;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const hreflangRules: import("./rule.js").Rule[];
|
|
@@ -0,0 +1,100 @@
|
|
|
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}))?)$/u;
|
|
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
|
|
15
|
+
? make(`${bad.length} hreflang value(s) are invalid.`, {
|
|
16
|
+
selector: 'link[rel="alternate"][hreflang]',
|
|
17
|
+
evidence: { invalid: cap(bad.map((h) => h.lang)) },
|
|
18
|
+
})
|
|
19
|
+
: null;
|
|
20
|
+
}),
|
|
21
|
+
defineRule({
|
|
22
|
+
id: "hreflang.relative-url",
|
|
23
|
+
category: "international",
|
|
24
|
+
severity: "warning",
|
|
25
|
+
description: "hreflang links must use absolute URLs.",
|
|
26
|
+
recommendation: "Use fully qualified URLs (https://example.com/nl/) in every hreflang link.",
|
|
27
|
+
docs: GOOGLE_DOCS.hreflang,
|
|
28
|
+
}, ({ page }, make) => {
|
|
29
|
+
const rel = page.hreflang.filter((h) => !/^https?:\/\//iu.test(h.href));
|
|
30
|
+
return rel.length
|
|
31
|
+
? make(`${rel.length} hreflang link(s) use relative URLs.`, {
|
|
32
|
+
selector: 'link[rel="alternate"][hreflang]',
|
|
33
|
+
evidence: { examples: cap(rel.map((h) => h.href)) },
|
|
34
|
+
})
|
|
35
|
+
: null;
|
|
36
|
+
}),
|
|
37
|
+
defineRule({
|
|
38
|
+
id: "hreflang.duplicate-lang",
|
|
39
|
+
category: "international",
|
|
40
|
+
severity: "warning",
|
|
41
|
+
description: "The same hreflang value appears more than once with different URLs.",
|
|
42
|
+
recommendation: "Each language/region code may map to exactly one URL per page.",
|
|
43
|
+
docs: GOOGLE_DOCS.hreflang,
|
|
44
|
+
}, ({ page }, make) => {
|
|
45
|
+
const seen = new Map();
|
|
46
|
+
for (const h of page.hreflang) {
|
|
47
|
+
const key = h.lang.toLowerCase();
|
|
48
|
+
const urls = seen.get(key) ?? new Set();
|
|
49
|
+
urls.add(h.url ?? h.href);
|
|
50
|
+
seen.set(key, urls);
|
|
51
|
+
}
|
|
52
|
+
const dupes = [...seen.entries()].flatMap(([lang, urls]) => urls.size > 1 ? [lang] : []);
|
|
53
|
+
return dupes.length
|
|
54
|
+
? make(`hreflang codes with conflicting URLs: ${dupes.join(", ")}.`, {
|
|
55
|
+
selector: 'link[rel="alternate"][hreflang]',
|
|
56
|
+
evidence: { codes: dupes },
|
|
57
|
+
})
|
|
58
|
+
: null;
|
|
59
|
+
}),
|
|
60
|
+
defineRule({
|
|
61
|
+
id: "hreflang.missing-self",
|
|
62
|
+
category: "international",
|
|
63
|
+
severity: "warning",
|
|
64
|
+
description: "The hreflang set does not include this page itself (self-referencing entry).",
|
|
65
|
+
recommendation: "Every page in an hreflang cluster must list itself as well as its alternates, otherwise the annotations may be ignored.",
|
|
66
|
+
docs: GOOGLE_DOCS.hreflang,
|
|
67
|
+
}, ({ page }, make) => {
|
|
68
|
+
if (page.hreflang.length === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const self = new Set([
|
|
72
|
+
normalizeUrl(page.url),
|
|
73
|
+
page.canonical ? normalizeUrl(page.canonical) : null,
|
|
74
|
+
].filter(Boolean));
|
|
75
|
+
const hasSelf = page.hreflang.some((h) => h.url && self.has(normalizeUrl(h.url)));
|
|
76
|
+
return hasSelf
|
|
77
|
+
? null
|
|
78
|
+
: make("hreflang set has no entry pointing at this page.", {
|
|
79
|
+
selector: 'link[rel="alternate"][hreflang]',
|
|
80
|
+
evidence: {
|
|
81
|
+
page: page.url,
|
|
82
|
+
alternates: cap(page.hreflang.map((h) => `${h.lang} → ${h.href}`)),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}),
|
|
86
|
+
defineRule({
|
|
87
|
+
id: "hreflang.missing-x-default",
|
|
88
|
+
category: "international",
|
|
89
|
+
severity: "info",
|
|
90
|
+
description: "The hreflang set has no x-default entry.",
|
|
91
|
+
recommendation: 'Add <link rel="alternate" hreflang="x-default" href="..."> pointing at the language-selector or default version.',
|
|
92
|
+
docs: GOOGLE_DOCS.hreflang,
|
|
93
|
+
}, ({ page }, make) => page.hreflang.length > 0 &&
|
|
94
|
+
!page.hreflang.some((h) => h.lang.toLowerCase() === "x-default")
|
|
95
|
+
? make("hreflang set has no x-default entry.", {
|
|
96
|
+
selector: 'link[rel="alternate"][hreflang]',
|
|
97
|
+
})
|
|
98
|
+
: null),
|
|
99
|
+
];
|
|
100
|
+
//# 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,77 @@
|
|
|
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)$/iu;
|
|
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: {
|
|
17
|
+
count: missing.length,
|
|
18
|
+
total: page.images.length,
|
|
19
|
+
examples: cap(missing.map((i) => i.src ?? "(no src)")),
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
: null;
|
|
23
|
+
}),
|
|
24
|
+
defineRule({
|
|
25
|
+
id: "img.alt-filename",
|
|
26
|
+
category: "images",
|
|
27
|
+
severity: "info",
|
|
28
|
+
description: "Image alt text looks like a filename (IMG_1234.jpg, screenshot.png).",
|
|
29
|
+
recommendation: "Replace filename-style alt text with a description of what the image shows.",
|
|
30
|
+
docs: GOOGLE_DOCS.images,
|
|
31
|
+
}, ({ page }, make) => {
|
|
32
|
+
const bad = page.images.filter((i) => i.alt && FILENAME_ALT.test(i.alt.trim()));
|
|
33
|
+
return bad.length
|
|
34
|
+
? make(`${bad.length} image(s) have filename-like alt text.`, {
|
|
35
|
+
evidence: {
|
|
36
|
+
examples: cap(bad.map((i) => ({ src: i.src, alt: i.alt }))),
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
: null;
|
|
40
|
+
}),
|
|
41
|
+
defineRule({
|
|
42
|
+
id: "img.dimensions-missing",
|
|
43
|
+
category: "images",
|
|
44
|
+
severity: "info",
|
|
45
|
+
description: "Images without explicit width and height attributes, which causes layout shift (CLS) while they load.",
|
|
46
|
+
recommendation: "Add width and height attributes (or CSS aspect-ratio) to <img> elements so the browser can reserve space before the image loads.",
|
|
47
|
+
docs: GOOGLE_DOCS.cwv,
|
|
48
|
+
}, ({ page }, make) => {
|
|
49
|
+
const missing = page.images.filter((i) => i.src && (!i.width || !i.height));
|
|
50
|
+
return missing.length
|
|
51
|
+
? make(`${missing.length} of ${page.images.length} images lack width/height attributes.`, {
|
|
52
|
+
selector: "img:not([width]), img:not([height])",
|
|
53
|
+
evidence: {
|
|
54
|
+
count: missing.length,
|
|
55
|
+
examples: cap(missing.map((i) => i.src)),
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
: null;
|
|
59
|
+
}),
|
|
60
|
+
defineRule({
|
|
61
|
+
id: "img.src-missing",
|
|
62
|
+
category: "images",
|
|
63
|
+
severity: "info",
|
|
64
|
+
description: "Images without a src attribute (usually lazy-loading via data-src without a fallback).",
|
|
65
|
+
recommendation: 'Use native loading="lazy" with a real src, or ensure the lazy-loader also works without JavaScript, so crawlers can discover the images.',
|
|
66
|
+
docs: GOOGLE_DOCS.images,
|
|
67
|
+
}, ({ page }, make) => {
|
|
68
|
+
const missing = page.images.filter((i) => !i.src);
|
|
69
|
+
return missing.length
|
|
70
|
+
? make(`${missing.length} image(s) have no src attribute.`, {
|
|
71
|
+
selector: "img:not([src])",
|
|
72
|
+
evidence: { count: missing.length },
|
|
73
|
+
})
|
|
74
|
+
: null;
|
|
75
|
+
}),
|
|
76
|
+
];
|
|
77
|
+
//# 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 const sortFindings: (findings: Finding[]) => Finding[];
|
|
13
|
+
export declare const runPageRules: (ctx: PageContext, options?: RunRulesOptions) => Finding[];
|
|
14
|
+
export declare const findRule: (id: string) => RuleMeta | undefined;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { PERFORMANCE_RULES } from "../performance-rules.js";
|
|
2
|
+
import { RENDER_RULES } from "../render-rules.js";
|
|
3
|
+
import { SITE_RULES } from "../site-rules.js";
|
|
4
|
+
import { canonicalRules } from "./canonical.js";
|
|
5
|
+
import { contentRules } from "./content.js";
|
|
6
|
+
import { documentRules } from "./document.js";
|
|
7
|
+
import { headingRules } from "./headings.js";
|
|
8
|
+
import { hreflangRules } from "./hreflang.js";
|
|
9
|
+
import { imageRules } from "./images.js";
|
|
10
|
+
import { indexingRules } from "./indexing.js";
|
|
11
|
+
import { keywordRules } from "./keyword.js";
|
|
12
|
+
import { linkRules } from "./links.js";
|
|
13
|
+
import { metaRules } from "./meta.js";
|
|
14
|
+
import { responseRules } from "./response.js";
|
|
15
|
+
import { socialRules } from "./social.js";
|
|
16
|
+
import { structuredDataRules } from "./structured-data.js";
|
|
17
|
+
import { titleRules } from "./title.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 const sortFindings = (findings) => {
|
|
43
|
+
const rank = { error: 0, warning: 1, info: 2 };
|
|
44
|
+
return findings.toSorted((a, b) => rank[a.severity] - rank[b.severity] || a.rule.localeCompare(b.rule));
|
|
45
|
+
};
|
|
46
|
+
export const runPageRules = (ctx, options = {}) => {
|
|
47
|
+
const findings = [];
|
|
48
|
+
for (const rule of PAGE_RULES) {
|
|
49
|
+
if (options.disable?.has(rule.meta.id)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (rule.meta.requires === "keyword" && !ctx.keyword) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
for (const f of rule.check(ctx)) {
|
|
57
|
+
if (!f.url) {
|
|
58
|
+
f.url = ctx.response?.finalUrl ?? ctx.page.url;
|
|
59
|
+
}
|
|
60
|
+
findings.push(f);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
findings.push({
|
|
65
|
+
rule: "tool.rule-error",
|
|
66
|
+
severity: "info",
|
|
67
|
+
category: "tool",
|
|
68
|
+
message: `Rule ${rule.meta.id} failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
69
|
+
recommendation: "Report this as a bug at https://github.com/rivi-software/agentic-seo-check/issues; the rest of the audit is unaffected.",
|
|
70
|
+
url: ctx.page.url,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return sortFindings(findings);
|
|
75
|
+
};
|
|
76
|
+
export const findRule = (id) => ALL_RULES.find((r) => r.id === id);
|
|
77
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/rules/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -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 const robotsDirectives: (page: ExtractedPage, response?: ResponseInfo) => RobotsDirectives;
|
|
10
|
+
export declare const indexingRules: import("./rule.js").Rule[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { defineRule, GOOGLE_DOCS } from "./rule.js";
|
|
2
|
+
/** Combine <meta name="robots"> / googlebot and the X-Robots-Tag header. */
|
|
3
|
+
export const robotsDirectives = (page, response) => {
|
|
4
|
+
const directives = [];
|
|
5
|
+
const sources = [];
|
|
6
|
+
for (const content of page.metaRobots) {
|
|
7
|
+
for (const d of content
|
|
8
|
+
.split(",")
|
|
9
|
+
.map((s) => s.trim())
|
|
10
|
+
.filter(Boolean)) {
|
|
11
|
+
directives.push(d);
|
|
12
|
+
sources.push("meta");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const header = response?.headers["x-robots-tag"];
|
|
16
|
+
if (header) {
|
|
17
|
+
for (const d of header
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.split(",")
|
|
20
|
+
.map((s) => s.trim())
|
|
21
|
+
.filter(Boolean)) {
|
|
22
|
+
// "googlebot: noindex" form
|
|
23
|
+
directives.push(d.includes(":") ? (d.split(":").pop() ?? d).trim() : d);
|
|
24
|
+
sources.push("header");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const noindex = directives.some((d) => d === "noindex" || d === "none");
|
|
28
|
+
const nofollow = directives.some((d) => d === "nofollow" || d === "none");
|
|
29
|
+
return {
|
|
30
|
+
noindex,
|
|
31
|
+
nofollow,
|
|
32
|
+
sources: [...new Set(sources)],
|
|
33
|
+
directives: [...new Set(directives)],
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export const indexingRules = [
|
|
37
|
+
defineRule({
|
|
38
|
+
id: "robots.noindex",
|
|
39
|
+
category: "indexing",
|
|
40
|
+
severity: "warning",
|
|
41
|
+
description: "The page tells search engines not to index it (meta robots or X-Robots-Tag).",
|
|
42
|
+
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.",
|
|
43
|
+
docs: GOOGLE_DOCS.robotsMeta,
|
|
44
|
+
}, ({ page, response }, make) => {
|
|
45
|
+
const r = robotsDirectives(page, response);
|
|
46
|
+
return r.noindex
|
|
47
|
+
? make(`Page is set to noindex via ${r.sources.join(" and ")}.`, {
|
|
48
|
+
selector: r.sources.includes("meta")
|
|
49
|
+
? 'head > meta[name="robots"]'
|
|
50
|
+
: undefined,
|
|
51
|
+
evidence: {
|
|
52
|
+
directives: r.directives,
|
|
53
|
+
sources: r.sources,
|
|
54
|
+
xRobotsTag: response?.headers["x-robots-tag"] ?? null,
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
: null;
|
|
58
|
+
}),
|
|
59
|
+
defineRule({
|
|
60
|
+
id: "robots.nofollow",
|
|
61
|
+
category: "indexing",
|
|
62
|
+
severity: "info",
|
|
63
|
+
description: "The page tells search engines not to follow its links.",
|
|
64
|
+
recommendation: "Remove nofollow unless you deliberately want link equity to stop at this page; it also hides linked pages from discovery.",
|
|
65
|
+
docs: GOOGLE_DOCS.robotsMeta,
|
|
66
|
+
}, ({ page, response }, make) => {
|
|
67
|
+
const r = robotsDirectives(page, response);
|
|
68
|
+
return r.nofollow
|
|
69
|
+
? make(`Page-level nofollow is set via ${r.sources.join(" and ")}.`, {
|
|
70
|
+
evidence: { directives: r.directives },
|
|
71
|
+
})
|
|
72
|
+
: null;
|
|
73
|
+
}),
|
|
74
|
+
];
|
|
75
|
+
//# 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,111 @@
|
|
|
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)
|
|
12
|
+
? make(`Title does not contain "${keyword}".`, {
|
|
13
|
+
selector: "head > title",
|
|
14
|
+
evidence: { title: page.title, keyword },
|
|
15
|
+
})
|
|
16
|
+
: null),
|
|
17
|
+
defineRule({
|
|
18
|
+
id: "keyword.not-in-h1",
|
|
19
|
+
category: "keyword",
|
|
20
|
+
severity: "warning",
|
|
21
|
+
description: "The target keyword does not appear in any H1.",
|
|
22
|
+
recommendation: "Use the target keyword in the H1 so the main heading matches the search intent.",
|
|
23
|
+
requires: "keyword",
|
|
24
|
+
}, ({ page, keyword }, make) => keyword &&
|
|
25
|
+
page.h1s.length > 0 &&
|
|
26
|
+
!page.h1s.some((h) => includesKeyword(h, keyword))
|
|
27
|
+
? make(`No <h1> contains "${keyword}".`, {
|
|
28
|
+
selector: "h1",
|
|
29
|
+
evidence: { h1s: page.h1s, keyword },
|
|
30
|
+
})
|
|
31
|
+
: null),
|
|
32
|
+
defineRule({
|
|
33
|
+
id: "keyword.not-in-description",
|
|
34
|
+
category: "keyword",
|
|
35
|
+
severity: "info",
|
|
36
|
+
description: "The target keyword does not appear in the meta description.",
|
|
37
|
+
recommendation: "Mention the keyword in the meta description; matching terms are bolded in search snippets.",
|
|
38
|
+
requires: "keyword",
|
|
39
|
+
}, ({ page, keyword }, make) => keyword &&
|
|
40
|
+
page.metaDescription &&
|
|
41
|
+
!includesKeyword(page.metaDescription, keyword)
|
|
42
|
+
? make(`Meta description does not contain "${keyword}".`, {
|
|
43
|
+
selector: 'head > meta[name="description"]',
|
|
44
|
+
evidence: { keyword },
|
|
45
|
+
})
|
|
46
|
+
: null),
|
|
47
|
+
defineRule({
|
|
48
|
+
id: "keyword.not-in-body",
|
|
49
|
+
category: "keyword",
|
|
50
|
+
severity: "warning",
|
|
51
|
+
description: "The target keyword does not appear in the visible body text.",
|
|
52
|
+
recommendation: "Use the keyword naturally in the body copy, ideally in the first paragraph.",
|
|
53
|
+
requires: "keyword",
|
|
54
|
+
}, ({ page, keyword }, make) => keyword &&
|
|
55
|
+
page.wordCount > 0 &&
|
|
56
|
+
keywordOccurrences(page.text, keyword) === 0
|
|
57
|
+
? make(`Body text does not contain "${keyword}".`, {
|
|
58
|
+
evidence: { keyword, wordCount: page.wordCount },
|
|
59
|
+
})
|
|
60
|
+
: null),
|
|
61
|
+
defineRule({
|
|
62
|
+
id: "keyword.not-in-url",
|
|
63
|
+
category: "keyword",
|
|
64
|
+
severity: "info",
|
|
65
|
+
description: "The target keyword does not appear in the URL path.",
|
|
66
|
+
recommendation: "Where the URL is still changeable, use a short slug containing the keyword (hyphen-separated).",
|
|
67
|
+
requires: "keyword",
|
|
68
|
+
}, ({ page, keyword, source }, make) => {
|
|
69
|
+
if (!keyword || source === "html") {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
let pathname = page.url;
|
|
73
|
+
try {
|
|
74
|
+
pathname = decodeURIComponent(new URL(page.url).pathname);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* keep */
|
|
78
|
+
}
|
|
79
|
+
const slug = pathname.replaceAll(/[-_/.+]+/gu, " ");
|
|
80
|
+
return includesKeyword(slug, keyword)
|
|
81
|
+
? null
|
|
82
|
+
: make(`URL path does not contain "${keyword}".`, {
|
|
83
|
+
evidence: { path: pathname, keyword },
|
|
84
|
+
});
|
|
85
|
+
}),
|
|
86
|
+
defineRule({
|
|
87
|
+
id: "keyword.density-high",
|
|
88
|
+
category: "keyword",
|
|
89
|
+
severity: "info",
|
|
90
|
+
description: "The target keyword makes up an unusually large share of the body text (default > 3%).",
|
|
91
|
+
recommendation: "Reduce repetition; write for readers and use synonyms. Keyword stuffing can trigger spam filters.",
|
|
92
|
+
requires: "keyword",
|
|
93
|
+
}, ({ page, keyword, thresholds }, make) => {
|
|
94
|
+
if (!keyword || page.wordCount < 50) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const occurrences = keywordOccurrences(page.text, keyword);
|
|
98
|
+
const kwWords = words(normalizeForComparison(keyword)).length || 1;
|
|
99
|
+
const density = ((occurrences * kwWords) / page.wordCount) * 100;
|
|
100
|
+
return density > thresholds.maxKeywordDensityPercent
|
|
101
|
+
? make(`Keyword density is ${density.toFixed(1)}% (${occurrences} occurrences in ${page.wordCount} words).`, {
|
|
102
|
+
evidence: {
|
|
103
|
+
occurrences,
|
|
104
|
+
wordCount: page.wordCount,
|
|
105
|
+
densityPercent: Number(density.toFixed(2)),
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
: null;
|
|
109
|
+
}),
|
|
110
|
+
];
|
|
111
|
+
//# 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[];
|