@answerable-kit/audit 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 (162) hide show
  1. package/README.md +58 -0
  2. package/dist/checks/a1-title.d.ts +2 -0
  3. package/dist/checks/a1-title.d.ts.map +1 -0
  4. package/dist/checks/a1-title.js +34 -0
  5. package/dist/checks/a1-title.js.map +1 -0
  6. package/dist/checks/a10-apple-touch.d.ts +2 -0
  7. package/dist/checks/a10-apple-touch.d.ts.map +1 -0
  8. package/dist/checks/a10-apple-touch.js +21 -0
  9. package/dist/checks/a10-apple-touch.js.map +1 -0
  10. package/dist/checks/a3-description.d.ts +2 -0
  11. package/dist/checks/a3-description.d.ts.map +1 -0
  12. package/dist/checks/a3-description.js +34 -0
  13. package/dist/checks/a3-description.js.map +1 -0
  14. package/dist/checks/a4-canonical.d.ts +2 -0
  15. package/dist/checks/a4-canonical.d.ts.map +1 -0
  16. package/dist/checks/a4-canonical.js +32 -0
  17. package/dist/checks/a4-canonical.js.map +1 -0
  18. package/dist/checks/a5-html-lang.d.ts +2 -0
  19. package/dist/checks/a5-html-lang.d.ts.map +1 -0
  20. package/dist/checks/a5-html-lang.js +38 -0
  21. package/dist/checks/a5-html-lang.js.map +1 -0
  22. package/dist/checks/a6-viewport.d.ts +2 -0
  23. package/dist/checks/a6-viewport.d.ts.map +1 -0
  24. package/dist/checks/a6-viewport.js +30 -0
  25. package/dist/checks/a6-viewport.js.map +1 -0
  26. package/dist/checks/a7-charset.d.ts +2 -0
  27. package/dist/checks/a7-charset.d.ts.map +1 -0
  28. package/dist/checks/a7-charset.js +32 -0
  29. package/dist/checks/a7-charset.js.map +1 -0
  30. package/dist/checks/a8-robots.d.ts +2 -0
  31. package/dist/checks/a8-robots.d.ts.map +1 -0
  32. package/dist/checks/a8-robots.js +46 -0
  33. package/dist/checks/a8-robots.js.map +1 -0
  34. package/dist/checks/a9-favicon.d.ts +2 -0
  35. package/dist/checks/a9-favicon.d.ts.map +1 -0
  36. package/dist/checks/a9-favicon.js +28 -0
  37. package/dist/checks/a9-favicon.js.map +1 -0
  38. package/dist/checks/b1-single-h1.d.ts +2 -0
  39. package/dist/checks/b1-single-h1.d.ts.map +1 -0
  40. package/dist/checks/b1-single-h1.js +32 -0
  41. package/dist/checks/b1-single-h1.js.map +1 -0
  42. package/dist/checks/b11-internal-links.d.ts +2 -0
  43. package/dist/checks/b11-internal-links.d.ts.map +1 -0
  44. package/dist/checks/b11-internal-links.js +30 -0
  45. package/dist/checks/b11-internal-links.js.map +1 -0
  46. package/dist/checks/b14-lists-or-tables.d.ts +2 -0
  47. package/dist/checks/b14-lists-or-tables.d.ts.map +1 -0
  48. package/dist/checks/b14-lists-or-tables.js +26 -0
  49. package/dist/checks/b14-lists-or-tables.js.map +1 -0
  50. package/dist/checks/b3-heading-hierarchy.d.ts +2 -0
  51. package/dist/checks/b3-heading-hierarchy.d.ts.map +1 -0
  52. package/dist/checks/b3-heading-hierarchy.js +36 -0
  53. package/dist/checks/b3-heading-hierarchy.js.map +1 -0
  54. package/dist/checks/b4-h2-sections.d.ts +2 -0
  55. package/dist/checks/b4-h2-sections.d.ts.map +1 -0
  56. package/dist/checks/b4-h2-sections.js +32 -0
  57. package/dist/checks/b4-h2-sections.js.map +1 -0
  58. package/dist/checks/b8-external-citations.d.ts +2 -0
  59. package/dist/checks/b8-external-citations.d.ts.map +1 -0
  60. package/dist/checks/b8-external-citations.js +41 -0
  61. package/dist/checks/b8-external-citations.js.map +1 -0
  62. package/dist/checks/c1-json-ld.d.ts +2 -0
  63. package/dist/checks/c1-json-ld.d.ts.map +1 -0
  64. package/dist/checks/c1-json-ld.js +69 -0
  65. package/dist/checks/c1-json-ld.js.map +1 -0
  66. package/dist/checks/c2-organization.d.ts +2 -0
  67. package/dist/checks/c2-organization.d.ts.map +1 -0
  68. package/dist/checks/c2-organization.js +67 -0
  69. package/dist/checks/c2-organization.js.map +1 -0
  70. package/dist/checks/d1-about-page-linked.d.ts +2 -0
  71. package/dist/checks/d1-about-page-linked.d.ts.map +1 -0
  72. package/dist/checks/d1-about-page-linked.js +24 -0
  73. package/dist/checks/d1-about-page-linked.js.map +1 -0
  74. package/dist/checks/d2-privacy-linked.d.ts +2 -0
  75. package/dist/checks/d2-privacy-linked.d.ts.map +1 -0
  76. package/dist/checks/d2-privacy-linked.js +24 -0
  77. package/dist/checks/d2-privacy-linked.js.map +1 -0
  78. package/dist/checks/d3-terms-linked.d.ts +2 -0
  79. package/dist/checks/d3-terms-linked.d.ts.map +1 -0
  80. package/dist/checks/d3-terms-linked.js +24 -0
  81. package/dist/checks/d3-terms-linked.js.map +1 -0
  82. package/dist/checks/d4-contact-accessible.d.ts +2 -0
  83. package/dist/checks/d4-contact-accessible.d.ts.map +1 -0
  84. package/dist/checks/d4-contact-accessible.js +30 -0
  85. package/dist/checks/d4-contact-accessible.js.map +1 -0
  86. package/dist/checks/d5-chrome-trust-link.d.ts +2 -0
  87. package/dist/checks/d5-chrome-trust-link.d.ts.map +1 -0
  88. package/dist/checks/d5-chrome-trust-link.js +24 -0
  89. package/dist/checks/d5-chrome-trust-link.js.map +1 -0
  90. package/dist/checks/d6-footer-trust-links.d.ts +2 -0
  91. package/dist/checks/d6-footer-trust-links.d.ts.map +1 -0
  92. package/dist/checks/d6-footer-trust-links.js +49 -0
  93. package/dist/checks/d6-footer-trust-links.js.map +1 -0
  94. package/dist/checks/e1-review-profile.d.ts +2 -0
  95. package/dist/checks/e1-review-profile.d.ts.map +1 -0
  96. package/dist/checks/e1-review-profile.js +38 -0
  97. package/dist/checks/e1-review-profile.js.map +1 -0
  98. package/dist/checks/e10-same-as-three.d.ts +2 -0
  99. package/dist/checks/e10-same-as-three.d.ts.map +1 -0
  100. package/dist/checks/e10-same-as-three.js +81 -0
  101. package/dist/checks/e10-same-as-three.js.map +1 -0
  102. package/dist/checks/e11-linkedin-linked.d.ts +2 -0
  103. package/dist/checks/e11-linkedin-linked.d.ts.map +1 -0
  104. package/dist/checks/e11-linkedin-linked.js +24 -0
  105. package/dist/checks/e11-linkedin-linked.js.map +1 -0
  106. package/dist/checks/e7-github-linked.d.ts +2 -0
  107. package/dist/checks/e7-github-linked.d.ts.map +1 -0
  108. package/dist/checks/e7-github-linked.js +24 -0
  109. package/dist/checks/e7-github-linked.js.map +1 -0
  110. package/dist/checks/f1-og-title.d.ts +2 -0
  111. package/dist/checks/f1-og-title.d.ts.map +1 -0
  112. package/dist/checks/f1-og-title.js +21 -0
  113. package/dist/checks/f1-og-title.js.map +1 -0
  114. package/dist/checks/f2-og-description.d.ts +2 -0
  115. package/dist/checks/f2-og-description.d.ts.map +1 -0
  116. package/dist/checks/f2-og-description.js +21 -0
  117. package/dist/checks/f2-og-description.js.map +1 -0
  118. package/dist/checks/f3-og-image.d.ts +2 -0
  119. package/dist/checks/f3-og-image.d.ts.map +1 -0
  120. package/dist/checks/f3-og-image.js +28 -0
  121. package/dist/checks/f3-og-image.js.map +1 -0
  122. package/dist/checks/f5-og-url.d.ts +2 -0
  123. package/dist/checks/f5-og-url.d.ts.map +1 -0
  124. package/dist/checks/f5-og-url.js +28 -0
  125. package/dist/checks/f5-og-url.js.map +1 -0
  126. package/dist/checks/f6-twitter-card.d.ts +2 -0
  127. package/dist/checks/f6-twitter-card.d.ts.map +1 -0
  128. package/dist/checks/f6-twitter-card.js +29 -0
  129. package/dist/checks/f6-twitter-card.js.map +1 -0
  130. package/dist/checks/f7-twitter-image.d.ts +2 -0
  131. package/dist/checks/f7-twitter-image.d.ts.map +1 -0
  132. package/dist/checks/f7-twitter-image.js +33 -0
  133. package/dist/checks/f7-twitter-image.js.map +1 -0
  134. package/dist/checks/registry.d.ts +10 -0
  135. package/dist/checks/registry.d.ts.map +1 -0
  136. package/dist/checks/registry.js +75 -0
  137. package/dist/checks/registry.js.map +1 -0
  138. package/dist/crawler.d.ts +40 -0
  139. package/dist/crawler.d.ts.map +1 -0
  140. package/dist/crawler.js +66 -0
  141. package/dist/crawler.js.map +1 -0
  142. package/dist/index.d.ts +48 -0
  143. package/dist/index.d.ts.map +1 -0
  144. package/dist/index.js +47 -0
  145. package/dist/index.js.map +1 -0
  146. package/dist/parser.d.ts +15 -0
  147. package/dist/parser.d.ts.map +1 -0
  148. package/dist/parser.js +10 -0
  149. package/dist/parser.js.map +1 -0
  150. package/dist/reporters/console.d.ts +12 -0
  151. package/dist/reporters/console.d.ts.map +1 -0
  152. package/dist/reporters/console.js +110 -0
  153. package/dist/reporters/console.js.map +1 -0
  154. package/dist/runner.d.ts +35 -0
  155. package/dist/runner.d.ts.map +1 -0
  156. package/dist/runner.js +120 -0
  157. package/dist/runner.js.map +1 -0
  158. package/dist/types.d.ts +52 -0
  159. package/dist/types.d.ts.map +1 -0
  160. package/dist/types.js +2 -0
  161. package/dist/types.js.map +1 -0
  162. package/package.json +48 -0
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @answerable-kit/audit
2
+
3
+ The audit engine for the [Answerable](https://github.com/Anuj7411/answerable) SEO toolkit. Fetches a target URL, parses the HTML, runs every registered check in parallel, and returns a structured report with score, severity-grouped findings, evidence, and fix recommendations.
4
+
5
+ > **Pre-alpha.** Foundations + first 5 checks ship today. The full 50-check framework lands across subsequent PRs — see [AUDIT-FRAMEWORK.md](../../docs/internal/AUDIT-FRAMEWORK.md).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @answerable-kit/audit
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { audit, consoleReport } from '@answerable-kit/audit';
17
+
18
+ const report = await audit('https://example.com');
19
+ console.log(consoleReport(report));
20
+ ```
21
+
22
+ Or pass a pre-parsed DOM in if you already have one (handy in tests and CI):
23
+
24
+ ```ts
25
+ import { runChecks } from '@answerable-kit/audit';
26
+ import { loadHtml } from '@answerable-kit/audit/parser';
27
+ import { parseAbsoluteUrl } from '@answerable-kit/core';
28
+
29
+ const html = '<!doctype html><html lang="en"><head><title>...</title></head>...';
30
+ const report = await runChecks({
31
+ url: parseAbsoluteUrl('https://example.com'),
32
+ html,
33
+ dom: loadHtml(html),
34
+ });
35
+ ```
36
+
37
+ ## What ships today
38
+
39
+ | ID | Check | Severity | Pts |
40
+ |---|---|---|---|
41
+ | A1 | `<title>` present, 30-60 chars | critical | 3 |
42
+ | A3 | Meta description present, 120-160 chars | critical | 3 |
43
+ | A4 | Canonical URL declared (absolute) | critical | 3 |
44
+ | A5 | `<html lang>` attribute set | high | 2 |
45
+ | C1 | At least one JSON-LD block present and valid | critical | 3 |
46
+
47
+ Total: **14 points** out of the eventual 100. The remaining 45 checks land in follow-up PRs.
48
+
49
+ ## Architecture notes
50
+
51
+ - **Static-first parser.** Uses [cheerio](https://github.com/cheeriojs/cheerio) for parsing — fast, no headless browser. SPA support via Playwright lands when needed; the static engine catches everything that matters for server-rendered Next.js sites.
52
+ - **Polite UA.** Every fetch sends `User-Agent: Answerable/<version> (+https://github.com/Anuj7411/answerable)`.
53
+ - **Errors don't crash.** A check that throws is captured and emitted as a `skip` with the error message — the audit always completes.
54
+ - **Pure runner + fetching wrapper.** `runChecks()` takes pre-parsed input (no network), `audit()` is the convenience wrapper that fetches first. Tests use the pure runner so CI never hits the network.
55
+
56
+ ## License
57
+
58
+ [MIT](../../LICENSE) © 2026 Anuj Ojha
@@ -0,0 +1,2 @@
1
+ export declare const a1Title: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=a1-title.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a1-title.d.ts","sourceRoot":"","sources":["../../src/checks/a1-title.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,OAAO,oEA+BlB,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ const MIN_LENGTH = 30;
3
+ const MAX_LENGTH = 60;
4
+ export const a1Title = defineCheck({
5
+ id: 'A1',
6
+ category: 'meta-and-technical',
7
+ severity: 'critical',
8
+ points: 3,
9
+ description: '<title> present and 30-60 chars long',
10
+ rationale: 'The <title> is the single most prominent surface in SERPs and AI answer engines. Empty titles disappear; titles under 30 chars under-utilize valuable space; titles over 60 are truncated in Google SERPs.',
11
+ docsUrl: 'https://answerable.dev/docs/checks/A1',
12
+ run: ({ dom }) => {
13
+ const title = dom('title').first().text().trim();
14
+ if (!title) {
15
+ return {
16
+ status: 'fail',
17
+ fixRecommendation: 'Add a <title> tag inside <head>. Target 30-60 characters and include the page topic + brand.',
18
+ };
19
+ }
20
+ const len = title.length;
21
+ if (len < MIN_LENGTH || len > MAX_LENGTH) {
22
+ return {
23
+ status: 'warn',
24
+ evidence: `Title is ${len} chars: "${title}"`,
25
+ fixRecommendation: `Adjust the <title> to 30-60 characters (currently ${len}).`,
26
+ };
27
+ }
28
+ return {
29
+ status: 'pass',
30
+ evidence: `Title (${len} chars): "${title}"`,
31
+ };
32
+ },
33
+ });
34
+ //# sourceMappingURL=a1-title.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a1-title.js","sourceRoot":"","sources":["../../src/checks/a1-title.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,MAAM,UAAU,GAAG,EAAE,CAAC;AAEtB,MAAM,CAAC,MAAM,OAAO,GAAG,WAAW,CAAW;IAC3C,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,sCAAsC;IACnD,SAAS,EACP,4MAA4M;IAC9M,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;QACjD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,8FAA8F;aACjG,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC;QACzB,IAAI,GAAG,GAAG,UAAU,IAAI,GAAG,GAAG,UAAU,EAAE,CAAC;YACzC,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,YAAY,GAAG,YAAY,KAAK,GAAG;gBAC7C,iBAAiB,EAAE,qDAAqD,GAAG,IAAI;aAChF,CAAC;QACJ,CAAC;QACD,OAAO;YACL,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,UAAU,GAAG,aAAa,KAAK,GAAG;SAC7C,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const a10AppleTouchIcon: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=a10-apple-touch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a10-apple-touch.d.ts","sourceRoot":"","sources":["../../src/checks/a10-apple-touch.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,oEAoB5B,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const a10AppleTouchIcon = defineCheck({
3
+ id: 'A10',
4
+ category: 'meta-and-technical',
5
+ severity: 'low',
6
+ points: 1,
7
+ description: 'Apple touch icon linked',
8
+ rationale: 'Pinned web apps and bookmarked sites on iOS use the apple-touch-icon. Without it, iOS scales the favicon up and the result looks pixelated. 180×180 PNG is the modern recommendation.',
9
+ docsUrl: 'https://answerable.dev/docs/checks/A10',
10
+ run: ({ dom }) => {
11
+ const apple = dom('link[rel="apple-touch-icon"], link[rel="apple-touch-icon-precomposed"]');
12
+ if (apple.length === 0) {
13
+ return {
14
+ status: 'fail',
15
+ fixRecommendation: 'Add <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">.',
16
+ };
17
+ }
18
+ return { status: 'pass', evidence: `Found ${apple.length} apple-touch-icon link(s)` };
19
+ },
20
+ });
21
+ //# sourceMappingURL=a10-apple-touch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a10-apple-touch.js","sourceRoot":"","sources":["../../src/checks/a10-apple-touch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,CAAW;IACrD,EAAE,EAAE,KAAK;IACT,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,KAAK;IACf,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,yBAAyB;IACtC,SAAS,EACP,uLAAuL;IACzL,OAAO,EAAE,wCAAwC;IACjD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,KAAK,GAAG,GAAG,CAAC,wEAAwE,CAAC,CAAC;QAC5F,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,iFAAiF;aACpF,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,KAAK,CAAC,MAAM,2BAA2B,EAAE,CAAC;IACxF,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const a3Description: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=a3-description.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a3-description.d.ts","sourceRoot":"","sources":["../../src/checks/a3-description.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,aAAa,oEA+BxB,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ const MIN_LENGTH = 120;
3
+ const MAX_LENGTH = 160;
4
+ export const a3Description = defineCheck({
5
+ id: 'A3',
6
+ category: 'meta-and-technical',
7
+ severity: 'critical',
8
+ points: 3,
9
+ description: 'Meta description present and 120-160 chars long',
10
+ rationale: 'The meta description controls the SERP snippet 70% of the time. Missing descriptions force Google to synthesize one (often poorly). Out-of-range descriptions get truncated or padded with site boilerplate.',
11
+ docsUrl: 'https://answerable.dev/docs/checks/A3',
12
+ run: ({ dom }) => {
13
+ const description = dom('meta[name="description"]').attr('content')?.trim() ?? '';
14
+ if (!description) {
15
+ return {
16
+ status: 'fail',
17
+ fixRecommendation: 'Add <meta name="description" content="..."> inside <head>. Aim for 120-160 chars summarizing the page.',
18
+ };
19
+ }
20
+ const len = description.length;
21
+ if (len < MIN_LENGTH || len > MAX_LENGTH) {
22
+ return {
23
+ status: 'warn',
24
+ evidence: `Description is ${len} chars: "${description}"`,
25
+ fixRecommendation: `Adjust the meta description to 120-160 characters (currently ${len}).`,
26
+ };
27
+ }
28
+ return {
29
+ status: 'pass',
30
+ evidence: `Description (${len} chars): "${description}"`,
31
+ };
32
+ },
33
+ });
34
+ //# sourceMappingURL=a3-description.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a3-description.js","sourceRoot":"","sources":["../../src/checks/a3-description.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,UAAU,GAAG,GAAG,CAAC;AACvB,MAAM,UAAU,GAAG,GAAG,CAAC;AAEvB,MAAM,CAAC,MAAM,aAAa,GAAG,WAAW,CAAW;IACjD,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,iDAAiD;IAC9D,SAAS,EACP,8MAA8M;IAChN,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,WAAW,GAAG,GAAG,CAAC,0BAA0B,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QAClF,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,wGAAwG;aAC3G,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,CAAC;QAC/B,IAAI,GAAG,GAAG,UAAU,IAAI,GAAG,GAAG,UAAU,EAAE,CAAC;YACzC,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,kBAAkB,GAAG,YAAY,WAAW,GAAG;gBACzD,iBAAiB,EAAE,gEAAgE,GAAG,IAAI;aAC3F,CAAC;QACJ,CAAC;QACD,OAAO;YACL,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,gBAAgB,GAAG,aAAa,WAAW,GAAG;SACzD,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const a4Canonical: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=a4-canonical.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a4-canonical.d.ts","sourceRoot":"","sources":["../../src/checks/a4-canonical.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,WAAW,oEAgCtB,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const a4Canonical = defineCheck({
3
+ id: 'A4',
4
+ category: 'meta-and-technical',
5
+ severity: 'critical',
6
+ points: 3,
7
+ description: 'Canonical URL declared as an absolute http(s) link',
8
+ rationale: 'Without a canonical, near-duplicate pages (trailing slashes, query strings, locales) compete for the same ranking signals — splitting authority across versions. The canonical link tells search engines which URL to consolidate signals onto.',
9
+ docsUrl: 'https://answerable.dev/docs/checks/A4',
10
+ run: ({ dom }) => {
11
+ const href = dom('link[rel="canonical"]').attr('href')?.trim();
12
+ if (!href) {
13
+ return {
14
+ status: 'fail',
15
+ fixRecommendation: 'Add <link rel="canonical" href="https://example.com/page"> inside <head>. Use defineSeo() from @answerable-kit/metadata to emit this automatically.',
16
+ };
17
+ }
18
+ const isAbsolute = /^https?:\/\//i.test(href);
19
+ if (!isAbsolute) {
20
+ return {
21
+ status: 'warn',
22
+ evidence: `Canonical href is relative: "${href}"`,
23
+ fixRecommendation: 'Use an absolute https URL for the canonical link. Relative canonicals are valid but error-prone across crawlers.',
24
+ };
25
+ }
26
+ return {
27
+ status: 'pass',
28
+ evidence: `Canonical: ${href}`,
29
+ };
30
+ },
31
+ });
32
+ //# sourceMappingURL=a4-canonical.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a4-canonical.js","sourceRoot":"","sources":["../../src/checks/a4-canonical.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAW;IAC/C,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,oDAAoD;IACjE,SAAS,EACP,iPAAiP;IACnP,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,IAAI,GAAG,GAAG,CAAC,uBAAuB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;QAC/D,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,qJAAqJ;aACxJ,CAAC;QACJ,CAAC;QACD,MAAM,UAAU,GAAG,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9C,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,gCAAgC,IAAI,GAAG;gBACjD,iBAAiB,EACf,kHAAkH;aACrH,CAAC;QACJ,CAAC;QACD,OAAO;YACL,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,cAAc,IAAI,EAAE;SAC/B,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const a5HtmlLang: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=a5-html-lang.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a5-html-lang.d.ts","sourceRoot":"","sources":["../../src/checks/a5-html-lang.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,UAAU,oEA8BrB,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ /**
3
+ * Loose BCP 47 shape check: 2-3 letter primary tag, optional subtags.
4
+ * Doesn't validate against the IANA registry — catches obvious junk
5
+ * (numbers in primary tag, embedded spaces, single chars) without
6
+ * flagging valid-but-uncommon tags.
7
+ */
8
+ const BCP47_LIKE = /^[a-zA-Z]{2,3}(-[A-Za-z0-9]{1,8})*$/;
9
+ export const a5HtmlLang = defineCheck({
10
+ id: 'A5',
11
+ category: 'meta-and-technical',
12
+ severity: 'high',
13
+ points: 2,
14
+ description: '<html lang> attribute set to a BCP 47 language tag',
15
+ rationale: 'Screen readers, translation tools, and AI answer engines use <html lang> to know what language to interpret the page in. Without it, accessibility tooling guesses and assistive tech announces in the wrong voice.',
16
+ docsUrl: 'https://answerable.dev/docs/checks/A5',
17
+ run: ({ dom }) => {
18
+ const lang = dom('html').attr('lang')?.trim();
19
+ if (!lang) {
20
+ return {
21
+ status: 'fail',
22
+ fixRecommendation: 'Set the lang attribute on <html>, e.g. <html lang="en">.',
23
+ };
24
+ }
25
+ if (!BCP47_LIKE.test(lang)) {
26
+ return {
27
+ status: 'warn',
28
+ evidence: `Suspicious lang value: "${lang}"`,
29
+ fixRecommendation: 'Use a BCP 47 tag like "en", "en-US", "fr", or "zh-Hant" — letters and dashes only.',
30
+ };
31
+ }
32
+ return {
33
+ status: 'pass',
34
+ evidence: `lang="${lang}"`,
35
+ };
36
+ },
37
+ });
38
+ //# sourceMappingURL=a5-html-lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a5-html-lang.js","sourceRoot":"","sources":["../../src/checks/a5-html-lang.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD;;;;;GAKG;AACH,MAAM,UAAU,GAAG,qCAAqC,CAAC;AAEzD,MAAM,CAAC,MAAM,UAAU,GAAG,WAAW,CAAW;IAC9C,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,oDAAoD;IACjE,SAAS,EACP,qNAAqN;IACvN,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;QAC9C,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EAAE,0DAA0D;aAC9E,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,2BAA2B,IAAI,GAAG;gBAC5C,iBAAiB,EACf,oFAAoF;aACvF,CAAC;QACJ,CAAC;QACD,OAAO;YACL,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,SAAS,IAAI,GAAG;SAC3B,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const a6Viewport: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=a6-viewport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a6-viewport.d.ts","sourceRoot":"","sources":["../../src/checks/a6-viewport.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,UAAU,oEA8BrB,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const a6Viewport = defineCheck({
3
+ id: 'A6',
4
+ category: 'meta-and-technical',
5
+ severity: 'high',
6
+ points: 2,
7
+ description: 'Viewport meta tag declares device-width + initial-scale=1',
8
+ rationale: 'Without a viewport meta, mobile browsers render at desktop width and scale down — text becomes unreadable and Google Mobile-Friendly checks fail. The canonical value is `width=device-width, initial-scale=1`.',
9
+ docsUrl: 'https://answerable.dev/docs/checks/A6',
10
+ run: ({ dom }) => {
11
+ const content = dom('meta[name="viewport"]').attr('content')?.trim() ?? '';
12
+ if (!content) {
13
+ return {
14
+ status: 'fail',
15
+ fixRecommendation: 'Add <meta name="viewport" content="width=device-width, initial-scale=1"> inside <head>.',
16
+ };
17
+ }
18
+ const hasWidth = /width\s*=\s*device-width/i.test(content);
19
+ const hasScale = /initial-scale\s*=\s*1(\.0+)?\b/i.test(content);
20
+ if (hasWidth && hasScale) {
21
+ return { status: 'pass', evidence: `content="${content}"` };
22
+ }
23
+ return {
24
+ status: 'warn',
25
+ evidence: `content="${content}"`,
26
+ fixRecommendation: 'Include both width=device-width and initial-scale=1 in the viewport meta.',
27
+ };
28
+ },
29
+ });
30
+ //# sourceMappingURL=a6-viewport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a6-viewport.js","sourceRoot":"","sources":["../../src/checks/a6-viewport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,UAAU,GAAG,WAAW,CAAW;IAC9C,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,2DAA2D;IACxE,SAAS,EACP,iNAAiN;IACnN,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,OAAO,GAAG,GAAG,CAAC,uBAAuB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QAC3E,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,yFAAyF;aAC5F,CAAC;QACJ,CAAC;QACD,MAAM,QAAQ,GAAG,2BAA2B,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3D,MAAM,QAAQ,GAAG,iCAAiC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACjE,IAAI,QAAQ,IAAI,QAAQ,EAAE,CAAC;YACzB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,OAAO,GAAG,EAAE,CAAC;QAC9D,CAAC;QACD,OAAO;YACL,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,YAAY,OAAO,GAAG;YAChC,iBAAiB,EACf,2EAA2E;SAC9E,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const a7Charset: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=a7-charset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a7-charset.d.ts","sourceRoot":"","sources":["../../src/checks/a7-charset.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,SAAS,oEA+BpB,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const a7Charset = defineCheck({
3
+ id: 'A7',
4
+ category: 'meta-and-technical',
5
+ severity: 'medium',
6
+ points: 1,
7
+ description: 'Document charset declared as UTF-8',
8
+ rationale: 'Without an explicit charset declaration, browsers guess — usually correctly, but not always. Mis-detected encoding breaks special characters (currency symbols, accented letters, em dashes). The HTML5 form `<meta charset="utf-8">` is one line and ends the ambiguity.',
9
+ docsUrl: 'https://answerable.dev/docs/checks/A7',
10
+ run: ({ dom }) => {
11
+ // Prefer the modern <meta charset="..."> form; fall back to the legacy http-equiv form.
12
+ const modernCharset = dom('meta[charset]').attr('charset')?.trim();
13
+ const legacyContentType = dom('meta[http-equiv="Content-Type" i]').attr('content')?.trim();
14
+ const legacyCharset = legacyContentType?.match(/charset\s*=\s*([\w-]+)/i)?.[1];
15
+ const charset = (modernCharset ?? legacyCharset)?.toLowerCase();
16
+ if (!charset) {
17
+ return {
18
+ status: 'fail',
19
+ fixRecommendation: 'Add <meta charset="utf-8"> as the first element inside <head>.',
20
+ };
21
+ }
22
+ if (charset !== 'utf-8' && charset !== 'utf8') {
23
+ return {
24
+ status: 'warn',
25
+ evidence: `Declared charset: ${charset}`,
26
+ fixRecommendation: 'Use UTF-8. Non-UTF-8 encodings (Windows-1252, ISO-8859-1) break emoji and many international characters.',
27
+ };
28
+ }
29
+ return { status: 'pass', evidence: `charset=${charset}` };
30
+ },
31
+ });
32
+ //# sourceMappingURL=a7-charset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a7-charset.js","sourceRoot":"","sources":["../../src/checks/a7-charset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAW;IAC7C,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,QAAQ;IAClB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,oCAAoC;IACjD,SAAS,EACP,2QAA2Q;IAC7Q,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,wFAAwF;QACxF,MAAM,aAAa,GAAG,GAAG,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC;QACnE,MAAM,iBAAiB,GAAG,GAAG,CAAC,mCAAmC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC;QAC3F,MAAM,aAAa,GAAG,iBAAiB,EAAE,KAAK,CAAC,yBAAyB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC/E,MAAM,OAAO,GAAG,CAAC,aAAa,IAAI,aAAa,CAAC,EAAE,WAAW,EAAE,CAAC;QAChE,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EAAE,gEAAgE;aACpF,CAAC;QACJ,CAAC;QACD,IAAI,OAAO,KAAK,OAAO,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;YAC9C,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,qBAAqB,OAAO,EAAE;gBACxC,iBAAiB,EACf,0GAA0G;aAC7G,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,OAAO,EAAE,EAAE,CAAC;IAC5D,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const a8Robots: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=a8-robots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a8-robots.d.ts","sourceRoot":"","sources":["../../src/checks/a8-robots.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,QAAQ,oEA+CnB,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const a8Robots = defineCheck({
3
+ id: 'A8',
4
+ category: 'meta-and-technical',
5
+ severity: 'high',
6
+ points: 2,
7
+ description: 'Robots meta consistent with intent (no accidental noindex)',
8
+ rationale: 'A stray `noindex` is the most common SEO own-goal — usually left over from a staging environment. Pages with noindex never rank, period. This check flags any robots directive that could be hiding the page from search.',
9
+ docsUrl: 'https://answerable.dev/docs/checks/A8',
10
+ run: ({ dom }) => {
11
+ const content = dom('meta[name="robots"]').attr('content')?.trim().toLowerCase() ?? '';
12
+ if (!content) {
13
+ // No robots meta means default (index, follow) — fine for a live page.
14
+ return { status: 'pass', evidence: 'No robots meta (default: index, follow)' };
15
+ }
16
+ const hasNoindex = /\bnoindex\b/.test(content);
17
+ const hasNofollow = /\bnofollow\b/.test(content);
18
+ // Contradictory declarations: both "index" and "noindex", or both "follow" and "nofollow".
19
+ if ((/\bindex\b/.test(content) && hasNoindex) || (/\bfollow\b/.test(content) && hasNofollow)) {
20
+ return {
21
+ status: 'fail',
22
+ evidence: `content="${content}"`,
23
+ fixRecommendation: 'Resolve contradictory robots directives. Use either `index, follow` or `noindex, nofollow` — never both.',
24
+ };
25
+ }
26
+ if (hasNoindex) {
27
+ return {
28
+ status: 'warn',
29
+ evidence: `content="${content}"`,
30
+ fixRecommendation: 'Page is noindexed. If this is a live page, remove `noindex` — staging directives often leak into production.',
31
+ };
32
+ }
33
+ if (hasNofollow) {
34
+ return {
35
+ status: 'warn',
36
+ evidence: `content="${content}"`,
37
+ fixRecommendation: 'Page links carry `nofollow`. Confirm this is intentional — it tells search engines not to follow outbound links.',
38
+ };
39
+ }
40
+ return {
41
+ status: 'pass',
42
+ evidence: `content="${content}"`,
43
+ };
44
+ },
45
+ });
46
+ //# sourceMappingURL=a8-robots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a8-robots.js","sourceRoot":"","sources":["../../src/checks/a8-robots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,QAAQ,GAAG,WAAW,CAAW;IAC5C,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,4DAA4D;IACzE,SAAS,EACP,2NAA2N;IAC7N,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,OAAO,GAAG,GAAG,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC;QACvF,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,uEAAuE;YACvE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,yCAAyC,EAAE,CAAC;QACjF,CAAC;QACD,MAAM,UAAU,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/C,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACjD,2FAA2F;QAC3F,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,WAAW,CAAC,EAAE,CAAC;YAC7F,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,YAAY,OAAO,GAAG;gBAChC,iBAAiB,EACf,0GAA0G;aAC7G,CAAC;QACJ,CAAC;QACD,IAAI,UAAU,EAAE,CAAC;YACf,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,YAAY,OAAO,GAAG;gBAChC,iBAAiB,EACf,8GAA8G;aACjH,CAAC;QACJ,CAAC;QACD,IAAI,WAAW,EAAE,CAAC;YAChB,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,YAAY,OAAO,GAAG;gBAChC,iBAAiB,EACf,kHAAkH;aACrH,CAAC;QACJ,CAAC;QACD,OAAO;YACL,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,YAAY,OAAO,GAAG;SACjC,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const a9Favicon: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=a9-favicon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a9-favicon.d.ts","sourceRoot":"","sources":["../../src/checks/a9-favicon.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,SAAS,oEA4BpB,CAAC"}
@@ -0,0 +1,28 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const a9Favicon = defineCheck({
3
+ id: 'A9',
4
+ category: 'meta-and-technical',
5
+ severity: 'medium',
6
+ points: 1,
7
+ description: 'Favicon linked (any of rel="icon", "shortcut icon", or "mask-icon")',
8
+ rationale: 'Browsers fall back to /favicon.ico when no icon is declared, but the explicit form lets you ship modern multi-size SVG and PNG icons. Missing favicon links also miss the chance to brand bookmarks, browser tabs, and platform pinned-site widgets.',
9
+ docsUrl: 'https://answerable.dev/docs/checks/A9',
10
+ run: ({ dom }) => {
11
+ const iconLinks = dom('link[rel~="icon"], link[rel="shortcut icon"], link[rel="mask-icon"]');
12
+ if (iconLinks.length === 0) {
13
+ return {
14
+ status: 'fail',
15
+ fixRecommendation: 'Add <link rel="icon" href="/favicon.ico"> at minimum. Prefer multiple sizes (16, 32, 180) plus an SVG.',
16
+ };
17
+ }
18
+ if (iconLinks.length === 1) {
19
+ return {
20
+ status: 'warn',
21
+ evidence: 'Found 1 icon link',
22
+ fixRecommendation: 'Ship multiple favicon sizes (16, 32, 180) plus an SVG for Retina + dark-mode-aware icons.',
23
+ };
24
+ }
25
+ return { status: 'pass', evidence: `Found ${iconLinks.length} icon link(s)` };
26
+ },
27
+ });
28
+ //# sourceMappingURL=a9-favicon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a9-favicon.js","sourceRoot":"","sources":["../../src/checks/a9-favicon.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAW;IAC7C,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,QAAQ;IAClB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,qEAAqE;IAClF,SAAS,EACP,sPAAsP;IACxP,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,SAAS,GAAG,GAAG,CAAC,qEAAqE,CAAC,CAAC;QAC7F,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,wGAAwG;aAC3G,CAAC;QACJ,CAAC;QACD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,mBAAmB;gBAC7B,iBAAiB,EACf,2FAA2F;aAC9F,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,SAAS,CAAC,MAAM,eAAe,EAAE,CAAC;IAChF,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const b1SingleH1: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=b1-single-h1.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"b1-single-h1.d.ts","sourceRoot":"","sources":["../../src/checks/b1-single-h1.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,UAAU,oEAgCrB,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const b1SingleH1 = defineCheck({
3
+ id: 'B1',
4
+ category: 'content-structure',
5
+ severity: 'critical',
6
+ points: 3,
7
+ description: 'Exactly one <h1> on the page',
8
+ rationale: 'Search engines and AI answer engines use the <h1> to identify the primary topic of the page. Zero h1s leaves them guessing; multiple h1s split the signal and dilute ranking authority.',
9
+ docsUrl: 'https://answerable.dev/docs/checks/B1',
10
+ run: ({ dom }) => {
11
+ const h1s = dom('h1');
12
+ if (h1s.length === 0) {
13
+ return {
14
+ status: 'fail',
15
+ fixRecommendation: 'Add exactly one <h1> per page. The h1 should clearly state the page topic.',
16
+ };
17
+ }
18
+ if (h1s.length > 1) {
19
+ return {
20
+ status: 'warn',
21
+ evidence: `Found ${h1s.length} <h1> elements`,
22
+ fixRecommendation: 'Use exactly one <h1> per page. Multiple h1s confuse search engines about the primary topic — convert the extras to h2.',
23
+ };
24
+ }
25
+ const text = h1s.first().text().trim();
26
+ return {
27
+ status: 'pass',
28
+ evidence: text.length > 0 ? `<h1>: "${text.slice(0, 80)}"` : 'Single empty <h1>',
29
+ };
30
+ },
31
+ });
32
+ //# sourceMappingURL=b1-single-h1.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"b1-single-h1.js","sourceRoot":"","sources":["../../src/checks/b1-single-h1.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,UAAU,GAAG,WAAW,CAAW;IAC9C,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,mBAAmB;IAC7B,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,8BAA8B;IAC3C,SAAS,EACP,yLAAyL;IAC3L,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrB,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,4EAA4E;aAC/E,CAAC;QACJ,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACnB,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,SAAS,GAAG,CAAC,MAAM,gBAAgB;gBAC7C,iBAAiB,EACf,wHAAwH;aAC3H,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;QACvC,OAAO;YACL,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,mBAAmB;SACjF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const b11InternalLinks: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=b11-internal-links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"b11-internal-links.d.ts","sourceRoot":"","sources":["../../src/checks/b11-internal-links.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,gBAAgB,oEA4B3B,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ const MIN_INTERNAL_LINKS = 3;
3
+ export const b11InternalLinks = defineCheck({
4
+ id: 'B11',
5
+ category: 'content-structure',
6
+ severity: 'medium',
7
+ points: 1,
8
+ description: '≥3 internal links to sibling pages',
9
+ rationale: 'Internal links distribute authority across the site and tell crawlers how pages relate. A page with zero internal links is a dead-end; a page with three or more is part of a navigable web.',
10
+ docsUrl: 'https://answerable.dev/docs/checks/B11',
11
+ run: ({ dom }) => {
12
+ const internals = [];
13
+ dom('a[href]').each((_, el) => {
14
+ const href = dom(el).attr('href') ?? '';
15
+ // Internal: starts with `/` (path-only, not protocol-relative `//`).
16
+ if (href.startsWith('/') && !href.startsWith('//')) {
17
+ internals.push(href);
18
+ }
19
+ });
20
+ if (internals.length < MIN_INTERNAL_LINKS) {
21
+ return {
22
+ status: 'warn',
23
+ evidence: `Found ${internals.length} internal link(s)`,
24
+ fixRecommendation: `Add ≥${MIN_INTERNAL_LINKS} internal links to sibling pages. Internal linking distributes authority and helps crawlers discover related content.`,
25
+ };
26
+ }
27
+ return { status: 'pass', evidence: `${internals.length} internal link(s)` };
28
+ },
29
+ });
30
+ //# sourceMappingURL=b11-internal-links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"b11-internal-links.js","sourceRoot":"","sources":["../../src/checks/b11-internal-links.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAE7B,MAAM,CAAC,MAAM,gBAAgB,GAAG,WAAW,CAAW;IACpD,EAAE,EAAE,KAAK;IACT,QAAQ,EAAE,mBAAmB;IAC7B,QAAQ,EAAE,QAAQ;IAClB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,oCAAoC;IACjD,SAAS,EACP,8LAA8L;IAChM,OAAO,EAAE,wCAAwC;IACjD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,SAAS,GAAa,EAAE,CAAC;QAC/B,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE;YAC5B,MAAM,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACxC,qEAAqE;YACrE,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;gBACnD,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,IAAI,SAAS,CAAC,MAAM,GAAG,kBAAkB,EAAE,CAAC;YAC1C,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,SAAS,SAAS,CAAC,MAAM,mBAAmB;gBACtD,iBAAiB,EAAE,QAAQ,kBAAkB,uHAAuH;aACrK,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAC,MAAM,mBAAmB,EAAE,CAAC;IAC9E,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const b14ListsOrTables: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=b14-lists-or-tables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"b14-lists-or-tables.d.ts","sourceRoot":"","sources":["../../src/checks/b14-lists-or-tables.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,gBAAgB,oEAyB3B,CAAC"}
@@ -0,0 +1,26 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const b14ListsOrTables = defineCheck({
3
+ id: 'B14',
4
+ category: 'content-structure',
5
+ severity: 'medium',
6
+ points: 2,
7
+ description: 'Page uses lists (<ul>/<ol>) or tables for structured content',
8
+ rationale: 'AI answer engines preferentially extract content presented as lists and tables — they parse as structured data, not prose. A comparison written as a paragraph rarely gets surfaced; the same comparison in a table often does.',
9
+ docsUrl: 'https://answerable.dev/docs/checks/B14',
10
+ run: ({ dom }) => {
11
+ const ul = dom('ul').length;
12
+ const ol = dom('ol').length;
13
+ const table = dom('table').length;
14
+ if (ul + ol + table === 0) {
15
+ return {
16
+ status: 'warn',
17
+ fixRecommendation: 'Use <ul>, <ol>, or <table> for any content that compares, enumerates, or lists. Structured content is what AI engines quote.',
18
+ };
19
+ }
20
+ return {
21
+ status: 'pass',
22
+ evidence: `${ul} <ul>, ${ol} <ol>, ${table} <table>`,
23
+ };
24
+ },
25
+ });
26
+ //# sourceMappingURL=b14-lists-or-tables.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"b14-lists-or-tables.js","sourceRoot":"","sources":["../../src/checks/b14-lists-or-tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,gBAAgB,GAAG,WAAW,CAAW;IACpD,EAAE,EAAE,KAAK;IACT,QAAQ,EAAE,mBAAmB;IAC7B,QAAQ,EAAE,QAAQ;IAClB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,8DAA8D;IAC3E,SAAS,EACP,iOAAiO;IACnO,OAAO,EAAE,wCAAwC;IACjD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;QAC5B,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;QAC5B,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;QAClC,IAAI,EAAE,GAAG,EAAE,GAAG,KAAK,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,8HAA8H;aACjI,CAAC;QACJ,CAAC;QACD,OAAO;YACL,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,GAAG,EAAE,UAAU,EAAE,UAAU,KAAK,UAAU;SACrD,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const b3HeadingHierarchy: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=b3-heading-hierarchy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"b3-heading-hierarchy.d.ts","sourceRoot":"","sources":["../../src/checks/b3-heading-hierarchy.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,kBAAkB,oEAoC7B,CAAC"}