@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
@@ -0,0 +1,36 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const b3HeadingHierarchy = defineCheck({
3
+ id: 'B3',
4
+ category: 'content-structure',
5
+ severity: 'high',
6
+ points: 2,
7
+ description: 'Logical heading hierarchy (h2 before h3, no skipped levels)',
8
+ rationale: 'Screen readers and AI extraction models use heading order as the document outline. Skipping from h1 → h3 (with no h2 between) tells them the h3 is a subsection of nothing, which breaks chunking.',
9
+ docsUrl: 'https://answerable.dev/docs/checks/B3',
10
+ run: ({ dom }) => {
11
+ const headings = dom('h2, h3');
12
+ if (headings.length === 0) {
13
+ return { status: 'skip', evidence: 'No h2 or h3 elements to evaluate.' };
14
+ }
15
+ const firstTag = headings.first().prop('tagName')?.toLowerCase();
16
+ if (firstTag === 'h3' && dom('h2').length === 0) {
17
+ return {
18
+ status: 'warn',
19
+ evidence: 'Page uses h3 without any h2',
20
+ fixRecommendation: 'Use h2 for major sections and h3 for subsections under each h2. Skipping h2 confuses screen readers and content extractors.',
21
+ };
22
+ }
23
+ if (firstTag === 'h3') {
24
+ return {
25
+ status: 'warn',
26
+ evidence: 'First subheading is an h3 (precedes any h2)',
27
+ fixRecommendation: 'Start the page outline with h2 before introducing h3 subsections.',
28
+ };
29
+ }
30
+ return {
31
+ status: 'pass',
32
+ evidence: `${dom('h2').length} h2, ${dom('h3').length} h3 in order`,
33
+ };
34
+ },
35
+ });
36
+ //# sourceMappingURL=b3-heading-hierarchy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"b3-heading-hierarchy.js","sourceRoot":"","sources":["../../src/checks/b3-heading-hierarchy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,kBAAkB,GAAG,WAAW,CAAW;IACtD,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,mBAAmB;IAC7B,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,6DAA6D;IAC1E,SAAS,EACP,oMAAoM;IACtM,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC/B,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,mCAAmC,EAAE,CAAC;QAC3E,CAAC;QAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,CAAC;QACjE,IAAI,QAAQ,KAAK,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAChD,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,6BAA6B;gBACvC,iBAAiB,EACf,6HAA6H;aAChI,CAAC;QACJ,CAAC;QACD,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,6CAA6C;gBACvD,iBAAiB,EAAE,mEAAmE;aACvF,CAAC;QACJ,CAAC;QACD,OAAO;YACL,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,cAAc;SACpE,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const b4H2Sections: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=b4-h2-sections.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"b4-h2-sections.d.ts","sourceRoot":"","sources":["../../src/checks/b4-h2-sections.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,YAAY,oEA6BvB,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ /** Anything longer than this in visible body text is treated as "long enough to warrant chunking". */
3
+ const LONG_THRESHOLD_CHARS = 1500;
4
+ export const b4H2Sections = defineCheck({
5
+ id: 'B4',
6
+ category: 'content-structure',
7
+ severity: 'high',
8
+ points: 2,
9
+ description: 'Long pages broken into multiple h2 sections',
10
+ rationale: 'AI answer engines extract paragraphs anchored to their nearest heading. A 2000-word wall with no h2 is one chunk; the same content with five h2 sections is five quotable answers. Chunking is what makes a page "answerable".',
11
+ docsUrl: 'https://answerable.dev/docs/checks/B4',
12
+ run: ({ dom }) => {
13
+ const bodyText = (dom('body').first().text() || dom.root().text()).trim();
14
+ const len = bodyText.length;
15
+ if (len < LONG_THRESHOLD_CHARS) {
16
+ return {
17
+ status: 'pass',
18
+ evidence: `Page is short (${len} chars); h2 sectioning not required.`,
19
+ };
20
+ }
21
+ const h2Count = dom('h2').length;
22
+ if (h2Count < 2) {
23
+ return {
24
+ status: 'warn',
25
+ evidence: `Long page (${len} chars) with only ${h2Count} h2 section(s)`,
26
+ fixRecommendation: 'Break long content into h2 sections (≥2) so AI engines can extract individual paragraphs as answers.',
27
+ };
28
+ }
29
+ return { status: 'pass', evidence: `${h2Count} h2 sections across ${len} chars of body text` };
30
+ },
31
+ });
32
+ //# sourceMappingURL=b4-h2-sections.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"b4-h2-sections.js","sourceRoot":"","sources":["../../src/checks/b4-h2-sections.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,sGAAsG;AACtG,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAElC,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAW;IAChD,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,mBAAmB;IAC7B,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,6CAA6C;IAC1D,SAAS,EACP,gOAAgO;IAClO,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,QAAQ,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1E,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC;QAC5B,IAAI,GAAG,GAAG,oBAAoB,EAAE,CAAC;YAC/B,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,kBAAkB,GAAG,sCAAsC;aACtE,CAAC;QACJ,CAAC;QACD,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;QACjC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;YAChB,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,cAAc,GAAG,qBAAqB,OAAO,gBAAgB;gBACvE,iBAAiB,EACf,sGAAsG;aACzG,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,uBAAuB,GAAG,qBAAqB,EAAE,CAAC;IACjG,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const b8ExternalCitations: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=b8-external-citations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"b8-external-citations.d.ts","sourceRoot":"","sources":["../../src/checks/b8-external-citations.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,oEAuC9B,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const b8ExternalCitations = defineCheck({
3
+ id: 'B8',
4
+ category: 'content-structure',
5
+ severity: 'medium',
6
+ points: 1,
7
+ description: 'At least one external citation (link to a different origin)',
8
+ rationale: 'Pages that cite authoritative outside sources earn more trust from both human readers and AI answer engines. Zero external links flag a page as a closed garden.',
9
+ docsUrl: 'https://answerable.dev/docs/checks/B8',
10
+ run: ({ dom, url }) => {
11
+ let pageOrigin;
12
+ try {
13
+ pageOrigin = new URL(url).origin;
14
+ }
15
+ catch {
16
+ return { status: 'skip', evidence: 'Could not parse the page URL.' };
17
+ }
18
+ const externals = [];
19
+ dom('a[href]').each((_, el) => {
20
+ const href = dom(el).attr('href');
21
+ if (href === undefined || !/^https?:\/\//i.test(href))
22
+ return;
23
+ try {
24
+ if (new URL(href).origin !== pageOrigin) {
25
+ externals.push(href);
26
+ }
27
+ }
28
+ catch {
29
+ // ignore malformed
30
+ }
31
+ });
32
+ if (externals.length === 0) {
33
+ return {
34
+ status: 'warn',
35
+ fixRecommendation: 'Add at least one external link to an authoritative source (research, official docs, well-known industry sites). External citations are a small but meaningful trust signal.',
36
+ };
37
+ }
38
+ return { status: 'pass', evidence: `${externals.length} external link(s)` };
39
+ },
40
+ });
41
+ //# sourceMappingURL=b8-external-citations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"b8-external-citations.js","sourceRoot":"","sources":["../../src/checks/b8-external-citations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,mBAAmB,GAAG,WAAW,CAAW;IACvD,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,mBAAmB;IAC7B,QAAQ,EAAE,QAAQ;IAClB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,6DAA6D;IAC1E,SAAS,EACP,kKAAkK;IACpK,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE;QACpB,IAAI,UAAkB,CAAC;QACvB,IAAI,CAAC;YACH,UAAU,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,+BAA+B,EAAE,CAAC;QACvE,CAAC;QAED,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,CAAC;YAClC,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,OAAO;YAC9D,IAAI,CAAC;gBACH,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;oBACxC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACvB,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,mBAAmB;YACrB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,6KAA6K;aAChL,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 c1JsonLd: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=c1-json-ld.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"c1-json-ld.d.ts","sourceRoot":"","sources":["../../src/checks/c1-json-ld.ts"],"names":[],"mappings":"AA6BA,eAAO,MAAM,QAAQ,oEAkDnB,CAAC"}
@@ -0,0 +1,69 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ function summarizeJsonLd(raw, index) {
3
+ let parsed;
4
+ try {
5
+ parsed = JSON.parse(raw);
6
+ }
7
+ catch (e) {
8
+ return { error: e instanceof Error ? e.message : String(e) };
9
+ }
10
+ if (parsed === null || typeof parsed !== 'object') {
11
+ return { index, type: undefined };
12
+ }
13
+ // schema.org JSON-LD may be a single object, an array (Graph form), or
14
+ // an object with @graph. Pull a representative @type label for evidence.
15
+ const root = Array.isArray(parsed) ? parsed[0] : parsed;
16
+ if (root === null || typeof root !== 'object') {
17
+ return { index, type: undefined };
18
+ }
19
+ const typeValue = root['@type'];
20
+ const type = typeof typeValue === 'string' ? typeValue : undefined;
21
+ return { index, type };
22
+ }
23
+ export const c1JsonLd = defineCheck({
24
+ id: 'C1',
25
+ category: 'structured-data',
26
+ severity: 'critical',
27
+ points: 3,
28
+ description: 'At least one <script type="application/ld+json"> with valid JSON',
29
+ rationale: 'JSON-LD is how Google, Perplexity, ChatGPT, and Claude understand a page beyond its prose. Missing structured data forces engines to infer entity type, often badly. Even one block (Organization, WebSite) lifts most pages out of the lowest tier.',
30
+ docsUrl: 'https://answerable.dev/docs/checks/C1',
31
+ run: ({ dom }) => {
32
+ const blocks = dom('script[type="application/ld+json"]');
33
+ if (blocks.length === 0) {
34
+ return {
35
+ status: 'fail',
36
+ fixRecommendation: 'Add a JSON-LD block. Start with organization() from @answerable-kit/schemas in your root layout.',
37
+ };
38
+ }
39
+ const summaries = [];
40
+ const errors = [];
41
+ blocks.each((i, el) => {
42
+ const raw = dom(el).text();
43
+ const result = summarizeJsonLd(raw, i);
44
+ if ('error' in result) {
45
+ errors.push(`block ${i}: ${result.error}`);
46
+ }
47
+ else {
48
+ summaries.push(result);
49
+ }
50
+ });
51
+ if (summaries.length === 0) {
52
+ return {
53
+ status: 'warn',
54
+ evidence: `Found ${blocks.length} JSON-LD block(s), but none parsed: ${errors.join('; ')}`,
55
+ fixRecommendation: 'Fix the JSON syntax in your structured-data blocks. JSON.parse must succeed.',
56
+ };
57
+ }
58
+ const typeList = summaries
59
+ .map((s) => s.type ?? '?')
60
+ .filter((t, i, arr) => arr.indexOf(t) === i)
61
+ .join(', ');
62
+ const errorTail = errors.length > 0 ? ` (${errors.length} invalid block(s) ignored)` : '';
63
+ return {
64
+ status: 'pass',
65
+ evidence: `${summaries.length} block(s): ${typeList}${errorTail}`,
66
+ };
67
+ },
68
+ });
69
+ //# sourceMappingURL=c1-json-ld.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"c1-json-ld.js","sourceRoot":"","sources":["../../src/checks/c1-json-ld.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const c2Organization: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=c2-organization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"c2-organization.d.ts","sourceRoot":"","sources":["../../src/checks/c2-organization.ts"],"names":[],"mappings":"AA6BA,eAAO,MAAM,cAAc,oEAuCzB,CAAC"}
@@ -0,0 +1,67 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ function nodeHasType(value, target) {
3
+ if (value === null || typeof value !== 'object')
4
+ return false;
5
+ const typeField = value['@type'];
6
+ if (typeof typeField === 'string') {
7
+ return typeField === target;
8
+ }
9
+ if (Array.isArray(typeField)) {
10
+ return typeField.some((t) => t === target);
11
+ }
12
+ return false;
13
+ }
14
+ function findOrganization(parsed) {
15
+ if (Array.isArray(parsed)) {
16
+ return parsed.some((item) => findOrganization(item));
17
+ }
18
+ if (parsed === null || typeof parsed !== 'object')
19
+ return false;
20
+ if (nodeHasType(parsed, 'Organization'))
21
+ return true;
22
+ // schema.org Graph form: { "@context": "...", "@graph": [ ... ] }
23
+ const graph = parsed['@graph'];
24
+ if (Array.isArray(graph)) {
25
+ return graph.some((node) => findOrganization(node));
26
+ }
27
+ return false;
28
+ }
29
+ export const c2Organization = defineCheck({
30
+ id: 'C2',
31
+ category: 'structured-data',
32
+ severity: 'high',
33
+ points: 2,
34
+ description: 'Organization JSON-LD present',
35
+ rationale: 'Organization schema is what tells Google and AI answer engines who you are as an entity — name, logo, social profiles, contact. It powers the brand panel in SERPs and is the foundation for E-E-A-T (the second E is for entity).',
36
+ docsUrl: 'https://answerable.dev/docs/checks/C2',
37
+ run: ({ dom }) => {
38
+ const blocks = dom('script[type="application/ld+json"]');
39
+ if (blocks.length === 0) {
40
+ return {
41
+ status: 'fail',
42
+ fixRecommendation: 'Add an Organization JSON-LD block. Use organization() from @answerable-kit/schemas in your root layout.',
43
+ };
44
+ }
45
+ let found = false;
46
+ blocks.each((_, el) => {
47
+ const raw = dom(el).text();
48
+ try {
49
+ const parsed = JSON.parse(raw);
50
+ if (findOrganization(parsed)) {
51
+ found = true;
52
+ }
53
+ }
54
+ catch {
55
+ // Invalid JSON is caught by C1; ignore here.
56
+ }
57
+ });
58
+ if (found) {
59
+ return { status: 'pass', evidence: 'Found @type="Organization" in JSON-LD' };
60
+ }
61
+ return {
62
+ status: 'fail',
63
+ fixRecommendation: 'Add an Organization JSON-LD block. Use organization() from @answerable-kit/schemas.',
64
+ };
65
+ },
66
+ });
67
+ //# sourceMappingURL=c2-organization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"c2-organization.js","sourceRoot":"","sources":["../../src/checks/c2-organization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,SAAS,WAAW,CAAC,KAAc,EAAE,MAAc;IACjD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,SAAS,GAAI,KAAiC,CAAC,OAAO,CAAC,CAAC;IAC9D,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;QAClC,OAAO,SAAS,KAAK,MAAM,CAAC;IAC9B,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7B,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAe;IACvC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1B,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAChE,IAAI,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC;QAAE,OAAO,IAAI,CAAC;IACrD,kEAAkE;IAClE,MAAM,KAAK,GAAI,MAAkC,CAAC,QAAQ,CAAC,CAAC;IAC5D,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,WAAW,CAAW;IAClD,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,iBAAiB;IAC3B,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,8BAA8B;IAC3C,SAAS,EACP,oOAAoO;IACtO,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,MAAM,GAAG,GAAG,CAAC,oCAAoC,CAAC,CAAC;QACzD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,yGAAyG;aAC5G,CAAC;QACJ,CAAC;QACD,IAAI,KAAK,GAAG,KAAK,CAAC;QAClB,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE;YACpB,MAAM,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBACxC,IAAI,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC7B,KAAK,GAAG,IAAI,CAAC;gBACf,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,6CAA6C;YAC/C,CAAC;QACH,CAAC,CAAC,CAAC;QACH,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,uCAAuC,EAAE,CAAC;QAC/E,CAAC;QACD,OAAO;YACL,MAAM,EAAE,MAAM;YACd,iBAAiB,EACf,qFAAqF;SACxF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const d1AboutPageLinked: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=d1-about-page-linked.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d1-about-page-linked.d.ts","sourceRoot":"","sources":["../../src/checks/d1-about-page-linked.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,oEAuB5B,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const d1AboutPageLinked = defineCheck({
3
+ id: 'D1',
4
+ category: 'eeat-and-authority',
5
+ severity: 'critical',
6
+ points: 3,
7
+ description: 'About page linked from this page',
8
+ rationale: "An About page is the single most-cited E-E-A-T signal Google enumerates. AI answer engines also crawl the About page to understand who's behind the content. No About link = no trust anchor.",
9
+ docsUrl: 'https://answerable.dev/docs/checks/D1',
10
+ run: ({ dom }) => {
11
+ const links = dom('a[href]').filter((_, el) => {
12
+ const href = dom(el).attr('href') ?? '';
13
+ return /\/about(\/|$|\?|#)/i.test(href);
14
+ });
15
+ if (links.length === 0) {
16
+ return {
17
+ status: 'fail',
18
+ fixRecommendation: 'Link to an /about page from this page (nav, footer, or body). Use the `about` template from @answerable-kit/templates to scaffold one.',
19
+ };
20
+ }
21
+ return { status: 'pass', evidence: `Found ${links.length} link(s) to /about` };
22
+ },
23
+ });
24
+ //# sourceMappingURL=d1-about-page-linked.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d1-about-page-linked.js","sourceRoot":"","sources":["../../src/checks/d1-about-page-linked.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,CAAW;IACrD,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,kCAAkC;IAC/C,SAAS,EACP,+LAA+L;IACjM,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE;YAC5C,MAAM,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACxC,OAAO,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;QACH,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,wIAAwI;aAC3I,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,KAAK,CAAC,MAAM,oBAAoB,EAAE,CAAC;IACjF,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const d2PrivacyLinked: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=d2-privacy-linked.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d2-privacy-linked.d.ts","sourceRoot":"","sources":["../../src/checks/d2-privacy-linked.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,eAAe,oEAuB1B,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const d2PrivacyLinked = defineCheck({
3
+ id: 'D2',
4
+ category: 'eeat-and-authority',
5
+ severity: 'critical',
6
+ points: 3,
7
+ description: 'Privacy policy linked from this page',
8
+ rationale: "A privacy policy is a baseline legal and trust requirement — GDPR, CCPA, and Google all expect one. Missing privacy is both a compliance risk and a signal that the site isn't serious.",
9
+ docsUrl: 'https://answerable.dev/docs/checks/D2',
10
+ run: ({ dom }) => {
11
+ const links = dom('a[href]').filter((_, el) => {
12
+ const href = dom(el).attr('href') ?? '';
13
+ return /\/privacy(\/|$|\?|#)/i.test(href) || /privacy-policy/i.test(href);
14
+ });
15
+ if (links.length === 0) {
16
+ return {
17
+ status: 'fail',
18
+ fixRecommendation: 'Link to a /privacy page from this page (typically the footer). Use the `privacy` template from @answerable-kit/templates.',
19
+ };
20
+ }
21
+ return { status: 'pass', evidence: `Found ${links.length} link(s) to /privacy` };
22
+ },
23
+ });
24
+ //# sourceMappingURL=d2-privacy-linked.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d2-privacy-linked.js","sourceRoot":"","sources":["../../src/checks/d2-privacy-linked.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAW;IACnD,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,sCAAsC;IACnD,SAAS,EACP,yLAAyL;IAC3L,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE;YAC5C,MAAM,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACxC,OAAO,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5E,CAAC,CAAC,CAAC;QACH,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,2HAA2H;aAC9H,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,KAAK,CAAC,MAAM,sBAAsB,EAAE,CAAC;IACnF,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const d3TermsLinked: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=d3-terms-linked.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d3-terms-linked.d.ts","sourceRoot":"","sources":["../../src/checks/d3-terms-linked.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,aAAa,oEAuBxB,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const d3TermsLinked = defineCheck({
3
+ id: 'D3',
4
+ category: 'eeat-and-authority',
5
+ severity: 'high',
6
+ points: 2,
7
+ description: 'Terms of use linked from this page',
8
+ rationale: 'Terms of use set the contract between site and user — required for any service that takes payment, collects accounts, or limits user behaviour. A missing terms page raises a red flag in trust-signal audits.',
9
+ docsUrl: 'https://answerable.dev/docs/checks/D3',
10
+ run: ({ dom }) => {
11
+ const links = dom('a[href]').filter((_, el) => {
12
+ const href = dom(el).attr('href') ?? '';
13
+ return /\/(terms|tos)(\/|$|\?|#)/i.test(href) || /terms-of-use/i.test(href);
14
+ });
15
+ if (links.length === 0) {
16
+ return {
17
+ status: 'fail',
18
+ fixRecommendation: 'Link to a /terms page from this page (typically the footer). Use the `terms` template from @answerable-kit/templates.',
19
+ };
20
+ }
21
+ return { status: 'pass', evidence: `Found ${links.length} link(s) to /terms` };
22
+ },
23
+ });
24
+ //# sourceMappingURL=d3-terms-linked.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d3-terms-linked.js","sourceRoot":"","sources":["../../src/checks/d3-terms-linked.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,aAAa,GAAG,WAAW,CAAW;IACjD,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,oCAAoC;IACjD,SAAS,EACP,gNAAgN;IAClN,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE;YAC5C,MAAM,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACxC,OAAO,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9E,CAAC,CAAC,CAAC;QACH,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,uHAAuH;aAC1H,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,KAAK,CAAC,MAAM,oBAAoB,EAAE,CAAC;IACjF,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const d4ContactAccessible: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=d4-contact-accessible.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d4-contact-accessible.d.ts","sourceRoot":"","sources":["../../src/checks/d4-contact-accessible.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,oEA2B9B,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const d4ContactAccessible = defineCheck({
3
+ id: 'D4',
4
+ category: 'eeat-and-authority',
5
+ severity: 'high',
6
+ points: 2,
7
+ description: 'Contact information accessible from this page',
8
+ rationale: 'A clear way to reach the team is an E-E-A-T baseline. Either a /contact page or a visible mailto: link counts — but one of them must be present, or AI engines (and humans) flag the site as untrustworthy.',
9
+ docsUrl: 'https://answerable.dev/docs/checks/D4',
10
+ run: ({ dom }) => {
11
+ const contactLinks = dom('a[href]').filter((_, el) => {
12
+ const href = dom(el).attr('href') ?? '';
13
+ return /\/contact(\/|$|\?|#)/i.test(href);
14
+ });
15
+ const mailtoLinks = dom('a[href^="mailto:"]');
16
+ if (contactLinks.length === 0 && mailtoLinks.length === 0) {
17
+ return {
18
+ status: 'fail',
19
+ fixRecommendation: 'Provide a way to reach you — either a /contact page or a mailto: link in the footer. Use the `contact` template from @answerable-kit/templates.',
20
+ };
21
+ }
22
+ const evidence = [];
23
+ if (contactLinks.length > 0)
24
+ evidence.push(`${contactLinks.length} /contact link(s)`);
25
+ if (mailtoLinks.length > 0)
26
+ evidence.push(`${mailtoLinks.length} mailto: link(s)`);
27
+ return { status: 'pass', evidence: evidence.join(', ') };
28
+ },
29
+ });
30
+ //# sourceMappingURL=d4-contact-accessible.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d4-contact-accessible.js","sourceRoot":"","sources":["../../src/checks/d4-contact-accessible.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,mBAAmB,GAAG,WAAW,CAAW;IACvD,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,+CAA+C;IAC5D,SAAS,EACP,6MAA6M;IAC/M,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,YAAY,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE;YACnD,MAAM,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACxC,OAAO,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;QACH,MAAM,WAAW,GAAG,GAAG,CAAC,oBAAoB,CAAC,CAAC;QAC9C,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1D,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,iJAAiJ;aACpJ,CAAC;QACJ,CAAC;QACD,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,MAAM,mBAAmB,CAAC,CAAC;QACtF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,MAAM,kBAAkB,CAAC,CAAC;QACnF,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAC3D,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const d5ChromeTrustLink: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=d5-chrome-trust-link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d5-chrome-trust-link.d.ts","sourceRoot":"","sources":["../../src/checks/d5-chrome-trust-link.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,oEAuB5B,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ export const d5ChromeTrustLink = defineCheck({
3
+ id: 'D5',
4
+ category: 'eeat-and-authority',
5
+ severity: 'high',
6
+ points: 2,
7
+ description: 'About / Trust link present in <nav> or <header>',
8
+ rationale: 'A trust link in the site chrome (nav or header) tells both users and crawlers that the site stands behind a public identity. Hiding About-style pages in a deep menu signals the opposite.',
9
+ docsUrl: 'https://answerable.dev/docs/checks/D5',
10
+ run: ({ dom }) => {
11
+ const chromeLinks = dom('nav a[href], header a[href]').filter((_, el) => {
12
+ const href = dom(el).attr('href') ?? '';
13
+ return /\/(about|team|company|newsroom)(\/|$|\?|#)/i.test(href);
14
+ });
15
+ if (chromeLinks.length === 0) {
16
+ return {
17
+ status: 'fail',
18
+ fixRecommendation: 'Put an "About" (or Team / Newsroom) link in your <nav> or <header>. Visible trust links are what users see first; AI engines weight them more than footer links.',
19
+ };
20
+ }
21
+ return { status: 'pass', evidence: `${chromeLinks.length} trust link(s) in chrome` };
22
+ },
23
+ });
24
+ //# sourceMappingURL=d5-chrome-trust-link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d5-chrome-trust-link.js","sourceRoot":"","sources":["../../src/checks/d5-chrome-trust-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,CAAW;IACrD,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,iDAAiD;IAC9D,SAAS,EACP,4LAA4L;IAC9L,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,WAAW,GAAG,GAAG,CAAC,6BAA6B,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE;YACtE,MAAM,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACxC,OAAO,6CAA6C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClE,CAAC,CAAC,CAAC;QACH,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,kKAAkK;aACrK,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,WAAW,CAAC,MAAM,0BAA0B,EAAE,CAAC;IACvF,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const d6FooterTrustLinks: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=d6-footer-trust-links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d6-footer-trust-links.d.ts","sourceRoot":"","sources":["../../src/checks/d6-footer-trust-links.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,kBAAkB,oEA0C7B,CAAC"}
@@ -0,0 +1,49 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ const FOOTER_TRUST_PATHS = [
3
+ { label: '/privacy', pattern: /\/privacy/i },
4
+ { label: '/terms', pattern: /\/(terms|tos)/i },
5
+ { label: '/contact', pattern: /\/contact/i },
6
+ ];
7
+ export const d6FooterTrustLinks = defineCheck({
8
+ id: 'D6',
9
+ category: 'eeat-and-authority',
10
+ severity: 'high',
11
+ points: 2,
12
+ description: 'Footer links to /privacy, /terms, and /contact',
13
+ rationale: 'The standard E-E-A-T footer pattern (privacy + terms + contact) is what crawlers expect. Missing the trio signals that the site cuts corners on the legal and trust basics.',
14
+ docsUrl: 'https://answerable.dev/docs/checks/D6',
15
+ run: ({ dom }) => {
16
+ const footer = dom('footer');
17
+ if (footer.length === 0) {
18
+ return {
19
+ status: 'fail',
20
+ fixRecommendation: 'Add a <footer> element with links to /privacy, /terms, and /contact. These three together are the standard trust footer.',
21
+ };
22
+ }
23
+ const found = [];
24
+ for (const { label, pattern } of FOOTER_TRUST_PATHS) {
25
+ const hit = dom('footer a[href]').filter((_, el) => {
26
+ const href = dom(el).attr('href') ?? '';
27
+ return pattern.test(href);
28
+ });
29
+ if (hit.length > 0)
30
+ found.push(label);
31
+ }
32
+ if (found.length === 3) {
33
+ return { status: 'pass', evidence: 'Footer has /privacy, /terms, /contact' };
34
+ }
35
+ if (found.length === 0) {
36
+ return {
37
+ status: 'fail',
38
+ evidence: 'Footer has none of /privacy, /terms, /contact',
39
+ fixRecommendation: 'Add all three trust links (privacy, terms, contact) to your <footer>.',
40
+ };
41
+ }
42
+ return {
43
+ status: 'warn',
44
+ evidence: `Footer has ${found.length} of 3 trust links: ${found.join(', ')}`,
45
+ fixRecommendation: `Add the missing footer trust link(s). Currently present: ${found.join(', ')}.`,
46
+ };
47
+ },
48
+ });
49
+ //# sourceMappingURL=d6-footer-trust-links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d6-footer-trust-links.js","sourceRoot":"","sources":["../../src/checks/d6-footer-trust-links.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,MAAM,kBAAkB,GAAG;IACzB,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE;IAC5C,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,gBAAgB,EAAE;IAC9C,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE;CACpC,CAAC;AAEX,MAAM,CAAC,MAAM,kBAAkB,GAAG,WAAW,CAAW;IACtD,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,gDAAgD;IAC7D,SAAS,EACP,6KAA6K;IAC/K,OAAO,EAAE,uCAAuC;IAChD,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;QACf,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,iBAAiB,EACf,0HAA0H;aAC7H,CAAC;QACJ,CAAC;QACD,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,kBAAkB,EAAE,CAAC;YACpD,MAAM,GAAG,GAAG,GAAG,CAAC,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE;gBACjD,MAAM,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;gBACxC,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC5B,CAAC,CAAC,CAAC;YACH,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxC,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,uCAAuC,EAAE,CAAC;QAC/E,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,QAAQ,EAAE,+CAA+C;gBACzD,iBAAiB,EAAE,uEAAuE;aAC3F,CAAC;QACJ,CAAC;QACD,OAAO;YACL,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,cAAc,KAAK,CAAC,MAAM,sBAAsB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;YAC5E,iBAAiB,EAAE,4DAA4D,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;SACnG,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const e1ReviewProfile: import("@answerable-kit/core").Check<import("cheerio").CheerioAPI>;
2
+ //# sourceMappingURL=e1-review-profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"e1-review-profile.d.ts","sourceRoot":"","sources":["../../src/checks/e1-review-profile.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,eAAe,oEA2B1B,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { defineCheck } from '@answerable-kit/core';
2
+ const REVIEW_HOSTS = [
3
+ 'g2.com',
4
+ 'producthunt.com',
5
+ 'capterra.com',
6
+ 'trustpilot.com',
7
+ 'glassdoor.com',
8
+ 'softwareadvice.com',
9
+ 'getapp.com',
10
+ ];
11
+ export const e1ReviewProfile = defineCheck({
12
+ id: 'E1',
13
+ category: 'offsite-citations',
14
+ severity: 'medium',
15
+ points: 2,
16
+ description: 'Link to a review / marketplace profile (G2, Product Hunt, Capterra, ...)',
17
+ rationale: "Third-party reviews are an off-site authority signal AI engines weight heavily — they're the SaaS equivalent of academic citations. One link to a review profile beats five sameAs entries on Twitter clones.",
18
+ docsUrl: 'https://answerable.dev/docs/checks/E1',
19
+ run: ({ dom }) => {
20
+ const matches = new Set();
21
+ dom('a[href]').each((_, el) => {
22
+ const href = dom(el).attr('href') ?? '';
23
+ for (const host of REVIEW_HOSTS) {
24
+ if (href.toLowerCase().includes(host)) {
25
+ matches.add(host);
26
+ }
27
+ }
28
+ });
29
+ if (matches.size === 0) {
30
+ return {
31
+ status: 'warn',
32
+ fixRecommendation: `Link to a review profile (e.g. ${REVIEW_HOSTS.slice(0, 4).join(', ')}). Off-site reviews are an authority signal AI engines lean on.`,
33
+ };
34
+ }
35
+ return { status: 'pass', evidence: `Found profile link(s): ${[...matches].join(', ')}` };
36
+ },
37
+ });
38
+ //# sourceMappingURL=e1-review-profile.js.map