@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.
- package/README.md +58 -0
- package/dist/checks/a1-title.d.ts +2 -0
- package/dist/checks/a1-title.d.ts.map +1 -0
- package/dist/checks/a1-title.js +34 -0
- package/dist/checks/a1-title.js.map +1 -0
- package/dist/checks/a10-apple-touch.d.ts +2 -0
- package/dist/checks/a10-apple-touch.d.ts.map +1 -0
- package/dist/checks/a10-apple-touch.js +21 -0
- package/dist/checks/a10-apple-touch.js.map +1 -0
- package/dist/checks/a3-description.d.ts +2 -0
- package/dist/checks/a3-description.d.ts.map +1 -0
- package/dist/checks/a3-description.js +34 -0
- package/dist/checks/a3-description.js.map +1 -0
- package/dist/checks/a4-canonical.d.ts +2 -0
- package/dist/checks/a4-canonical.d.ts.map +1 -0
- package/dist/checks/a4-canonical.js +32 -0
- package/dist/checks/a4-canonical.js.map +1 -0
- package/dist/checks/a5-html-lang.d.ts +2 -0
- package/dist/checks/a5-html-lang.d.ts.map +1 -0
- package/dist/checks/a5-html-lang.js +38 -0
- package/dist/checks/a5-html-lang.js.map +1 -0
- package/dist/checks/a6-viewport.d.ts +2 -0
- package/dist/checks/a6-viewport.d.ts.map +1 -0
- package/dist/checks/a6-viewport.js +30 -0
- package/dist/checks/a6-viewport.js.map +1 -0
- package/dist/checks/a7-charset.d.ts +2 -0
- package/dist/checks/a7-charset.d.ts.map +1 -0
- package/dist/checks/a7-charset.js +32 -0
- package/dist/checks/a7-charset.js.map +1 -0
- package/dist/checks/a8-robots.d.ts +2 -0
- package/dist/checks/a8-robots.d.ts.map +1 -0
- package/dist/checks/a8-robots.js +46 -0
- package/dist/checks/a8-robots.js.map +1 -0
- package/dist/checks/a9-favicon.d.ts +2 -0
- package/dist/checks/a9-favicon.d.ts.map +1 -0
- package/dist/checks/a9-favicon.js +28 -0
- package/dist/checks/a9-favicon.js.map +1 -0
- package/dist/checks/b1-single-h1.d.ts +2 -0
- package/dist/checks/b1-single-h1.d.ts.map +1 -0
- package/dist/checks/b1-single-h1.js +32 -0
- package/dist/checks/b1-single-h1.js.map +1 -0
- package/dist/checks/b11-internal-links.d.ts +2 -0
- package/dist/checks/b11-internal-links.d.ts.map +1 -0
- package/dist/checks/b11-internal-links.js +30 -0
- package/dist/checks/b11-internal-links.js.map +1 -0
- package/dist/checks/b14-lists-or-tables.d.ts +2 -0
- package/dist/checks/b14-lists-or-tables.d.ts.map +1 -0
- package/dist/checks/b14-lists-or-tables.js +26 -0
- package/dist/checks/b14-lists-or-tables.js.map +1 -0
- package/dist/checks/b3-heading-hierarchy.d.ts +2 -0
- package/dist/checks/b3-heading-hierarchy.d.ts.map +1 -0
- package/dist/checks/b3-heading-hierarchy.js +36 -0
- package/dist/checks/b3-heading-hierarchy.js.map +1 -0
- package/dist/checks/b4-h2-sections.d.ts +2 -0
- package/dist/checks/b4-h2-sections.d.ts.map +1 -0
- package/dist/checks/b4-h2-sections.js +32 -0
- package/dist/checks/b4-h2-sections.js.map +1 -0
- package/dist/checks/b8-external-citations.d.ts +2 -0
- package/dist/checks/b8-external-citations.d.ts.map +1 -0
- package/dist/checks/b8-external-citations.js +41 -0
- package/dist/checks/b8-external-citations.js.map +1 -0
- package/dist/checks/c1-json-ld.d.ts +2 -0
- package/dist/checks/c1-json-ld.d.ts.map +1 -0
- package/dist/checks/c1-json-ld.js +69 -0
- package/dist/checks/c1-json-ld.js.map +1 -0
- package/dist/checks/c2-organization.d.ts +2 -0
- package/dist/checks/c2-organization.d.ts.map +1 -0
- package/dist/checks/c2-organization.js +67 -0
- package/dist/checks/c2-organization.js.map +1 -0
- package/dist/checks/d1-about-page-linked.d.ts +2 -0
- package/dist/checks/d1-about-page-linked.d.ts.map +1 -0
- package/dist/checks/d1-about-page-linked.js +24 -0
- package/dist/checks/d1-about-page-linked.js.map +1 -0
- package/dist/checks/d2-privacy-linked.d.ts +2 -0
- package/dist/checks/d2-privacy-linked.d.ts.map +1 -0
- package/dist/checks/d2-privacy-linked.js +24 -0
- package/dist/checks/d2-privacy-linked.js.map +1 -0
- package/dist/checks/d3-terms-linked.d.ts +2 -0
- package/dist/checks/d3-terms-linked.d.ts.map +1 -0
- package/dist/checks/d3-terms-linked.js +24 -0
- package/dist/checks/d3-terms-linked.js.map +1 -0
- package/dist/checks/d4-contact-accessible.d.ts +2 -0
- package/dist/checks/d4-contact-accessible.d.ts.map +1 -0
- package/dist/checks/d4-contact-accessible.js +30 -0
- package/dist/checks/d4-contact-accessible.js.map +1 -0
- package/dist/checks/d5-chrome-trust-link.d.ts +2 -0
- package/dist/checks/d5-chrome-trust-link.d.ts.map +1 -0
- package/dist/checks/d5-chrome-trust-link.js +24 -0
- package/dist/checks/d5-chrome-trust-link.js.map +1 -0
- package/dist/checks/d6-footer-trust-links.d.ts +2 -0
- package/dist/checks/d6-footer-trust-links.d.ts.map +1 -0
- package/dist/checks/d6-footer-trust-links.js +49 -0
- package/dist/checks/d6-footer-trust-links.js.map +1 -0
- package/dist/checks/e1-review-profile.d.ts +2 -0
- package/dist/checks/e1-review-profile.d.ts.map +1 -0
- package/dist/checks/e1-review-profile.js +38 -0
- package/dist/checks/e1-review-profile.js.map +1 -0
- package/dist/checks/e10-same-as-three.d.ts +2 -0
- package/dist/checks/e10-same-as-three.d.ts.map +1 -0
- package/dist/checks/e10-same-as-three.js +81 -0
- package/dist/checks/e10-same-as-three.js.map +1 -0
- package/dist/checks/e11-linkedin-linked.d.ts +2 -0
- package/dist/checks/e11-linkedin-linked.d.ts.map +1 -0
- package/dist/checks/e11-linkedin-linked.js +24 -0
- package/dist/checks/e11-linkedin-linked.js.map +1 -0
- package/dist/checks/e7-github-linked.d.ts +2 -0
- package/dist/checks/e7-github-linked.d.ts.map +1 -0
- package/dist/checks/e7-github-linked.js +24 -0
- package/dist/checks/e7-github-linked.js.map +1 -0
- package/dist/checks/f1-og-title.d.ts +2 -0
- package/dist/checks/f1-og-title.d.ts.map +1 -0
- package/dist/checks/f1-og-title.js +21 -0
- package/dist/checks/f1-og-title.js.map +1 -0
- package/dist/checks/f2-og-description.d.ts +2 -0
- package/dist/checks/f2-og-description.d.ts.map +1 -0
- package/dist/checks/f2-og-description.js +21 -0
- package/dist/checks/f2-og-description.js.map +1 -0
- package/dist/checks/f3-og-image.d.ts +2 -0
- package/dist/checks/f3-og-image.d.ts.map +1 -0
- package/dist/checks/f3-og-image.js +28 -0
- package/dist/checks/f3-og-image.js.map +1 -0
- package/dist/checks/f5-og-url.d.ts +2 -0
- package/dist/checks/f5-og-url.d.ts.map +1 -0
- package/dist/checks/f5-og-url.js +28 -0
- package/dist/checks/f5-og-url.js.map +1 -0
- package/dist/checks/f6-twitter-card.d.ts +2 -0
- package/dist/checks/f6-twitter-card.d.ts.map +1 -0
- package/dist/checks/f6-twitter-card.js +29 -0
- package/dist/checks/f6-twitter-card.js.map +1 -0
- package/dist/checks/f7-twitter-image.d.ts +2 -0
- package/dist/checks/f7-twitter-image.d.ts.map +1 -0
- package/dist/checks/f7-twitter-image.js +33 -0
- package/dist/checks/f7-twitter-image.js.map +1 -0
- package/dist/checks/registry.d.ts +10 -0
- package/dist/checks/registry.d.ts.map +1 -0
- package/dist/checks/registry.js +75 -0
- package/dist/checks/registry.js.map +1 -0
- package/dist/crawler.d.ts +40 -0
- package/dist/crawler.d.ts.map +1 -0
- package/dist/crawler.js +66 -0
- package/dist/crawler.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +15 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +10 -0
- package/dist/parser.js.map +1 -0
- package/dist/reporters/console.d.ts +12 -0
- package/dist/reporters/console.d.ts.map +1 -0
- package/dist/reporters/console.js +110 -0
- package/dist/reporters/console.js.map +1 -0
- package/dist/runner.d.ts +35 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +120 -0
- package/dist/runner.js.map +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- 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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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"}
|