ax-audit 2.4.0 → 3.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 (76) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +43 -17
  3. package/dist/checks/agent-json.d.ts +10 -0
  4. package/dist/checks/agent-json.d.ts.map +1 -1
  5. package/dist/checks/agent-json.js +66 -2
  6. package/dist/checks/agent-json.js.map +1 -1
  7. package/dist/checks/content-negotiation.d.ts +4 -0
  8. package/dist/checks/content-negotiation.d.ts.map +1 -0
  9. package/dist/checks/content-negotiation.js +138 -0
  10. package/dist/checks/content-negotiation.js.map +1 -0
  11. package/dist/checks/html-rendering.d.ts +21 -0
  12. package/dist/checks/html-rendering.d.ts.map +1 -0
  13. package/dist/checks/html-rendering.js +204 -0
  14. package/dist/checks/html-rendering.js.map +1 -0
  15. package/dist/checks/html-utils.d.ts +37 -0
  16. package/dist/checks/html-utils.d.ts.map +1 -0
  17. package/dist/checks/html-utils.js +93 -0
  18. package/dist/checks/html-utils.js.map +1 -0
  19. package/dist/checks/http-headers.js +1 -1
  20. package/dist/checks/http-headers.js.map +1 -1
  21. package/dist/checks/index.d.ts.map +1 -1
  22. package/dist/checks/index.js +12 -0
  23. package/dist/checks/index.js.map +1 -1
  24. package/dist/checks/llms-txt.d.ts.map +1 -1
  25. package/dist/checks/llms-txt.js +17 -2
  26. package/dist/checks/llms-txt.js.map +1 -1
  27. package/dist/checks/mcp.d.ts.map +1 -1
  28. package/dist/checks/mcp.js +11 -2
  29. package/dist/checks/mcp.js.map +1 -1
  30. package/dist/checks/meta-tags.d.ts +13 -0
  31. package/dist/checks/meta-tags.d.ts.map +1 -1
  32. package/dist/checks/meta-tags.js +104 -24
  33. package/dist/checks/meta-tags.js.map +1 -1
  34. package/dist/checks/openapi.d.ts.map +1 -1
  35. package/dist/checks/openapi.js +11 -2
  36. package/dist/checks/openapi.js.map +1 -1
  37. package/dist/checks/robots-txt.js +1 -1
  38. package/dist/checks/security-txt.js +1 -1
  39. package/dist/checks/seo-basics.d.ts +13 -0
  40. package/dist/checks/seo-basics.d.ts.map +1 -0
  41. package/dist/checks/seo-basics.js +222 -0
  42. package/dist/checks/seo-basics.js.map +1 -0
  43. package/dist/checks/sitemap.d.ts +12 -0
  44. package/dist/checks/sitemap.d.ts.map +1 -0
  45. package/dist/checks/sitemap.js +241 -0
  46. package/dist/checks/sitemap.js.map +1 -0
  47. package/dist/checks/structured-data.js +1 -1
  48. package/dist/checks/structured-data.js.map +1 -1
  49. package/dist/checks/tls-https.d.ts +13 -0
  50. package/dist/checks/tls-https.d.ts.map +1 -0
  51. package/dist/checks/tls-https.js +164 -0
  52. package/dist/checks/tls-https.js.map +1 -0
  53. package/dist/checks/utils.d.ts +13 -1
  54. package/dist/checks/utils.d.ts.map +1 -1
  55. package/dist/checks/utils.js +28 -0
  56. package/dist/checks/utils.js.map +1 -1
  57. package/dist/checks/well-known-ai.d.ts +17 -0
  58. package/dist/checks/well-known-ai.d.ts.map +1 -0
  59. package/dist/checks/well-known-ai.js +123 -0
  60. package/dist/checks/well-known-ai.js.map +1 -0
  61. package/dist/constants.d.ts +19 -0
  62. package/dist/constants.d.ts.map +1 -1
  63. package/dist/constants.js +50 -10
  64. package/dist/constants.js.map +1 -1
  65. package/dist/fetcher.d.ts +2 -2
  66. package/dist/fetcher.d.ts.map +1 -1
  67. package/dist/fetcher.js +42 -9
  68. package/dist/fetcher.js.map +1 -1
  69. package/dist/index.d.ts +1 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/scorer.d.ts.map +1 -1
  72. package/dist/scorer.js +8 -0
  73. package/dist/scorer.js.map +1 -1
  74. package/dist/types.d.ts +10 -1
  75. package/dist/types.d.ts.map +1 -1
  76. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  All notable changes to ax-audit are documented here.
4
4
 
5
+ ## [3.1.0] - 2026-06-06
6
+
7
+ ### Added
8
+
9
+ - **content-negotiation check (informational)**: probes the homepage with `Accept: text/markdown` to detect Markdown for Agents support — the pattern implemented by Cloudflare and Vercel and requested by Claude Code, Cursor, and OpenCode. Validates the negotiated `Content-Type`, that the body is actual Markdown (not a relabeled HTML document), `Vary: Accept` presence (shared-cache correctness), and reports the size reduction vs the HTML representation. Falls back to detecting `<link rel="alternate" type="text/markdown">` for partial credit.
10
+ - **Per-request fetch headers**: `CheckContext.fetch` now accepts an optional `{ headers }` argument. Custom headers merge case-insensitively over the defaults, and the in-memory cache keys on URL + normalized headers, mirroring `Vary` semantics on the wire. New exported type: `FetchOptions`.
11
+ - **31 new tests** (229 total): content-negotiation suite (19), fetcher integration suite against a real local HTTP server (9), and scorer coverage for weight-0 checks (3).
12
+
13
+ ### Fixed
14
+
15
+ - **Scorer division by zero**: `calculateOverallScore` returned `NaN` when every selected check had weight 0 (e.g. `--checks content-negotiation`). It now falls back to a plain average, and returns 0 for empty input.
16
+
17
+ ### Scoring
18
+
19
+ - The new check carries **weight 0 in 3.x**: it runs and reports findings but does not affect the overall score, so existing scores and baselines are unchanged. It will gain weight in v4.0, consistent with treating score-affecting changes as breaking (see 3.0.0).
20
+
21
+ ## [3.0.0] - 2026-04-30
22
+
23
+ ### Added — five new checks (full agent-optimization coverage)
24
+
25
+ - **html-rendering** (weight 9%): detects whether the static HTML response actually contains content, since most AI crawlers (GPTBot, ClaudeBot, CCBot, …) do not execute JavaScript. Heuristics: text length, word count, text-to-markup ratio, empty SPA mount points (`#root`, `#__next`, `#__nuxt`, `#app`, `#svelte`, `#gatsby`), semantic landmarks (`<main>`, `<article>`, `<header>`, `<footer>`, `<nav>`), single `<h1>`, `<noscript>` fallback, and `<img alt>` coverage.
26
+ - **sitemap** (weight 4%): locates the sitemap via `robots.txt` `Sitemap:` directive or `/sitemap.xml`, validates XML shape, parses `<urlset>` and `<sitemapindex>`, samples child sitemaps from indexes, scores `<lastmod>` coverage and freshness (>365d → stale), enforces 50k-URL / 50MB limits.
27
+ - **seo-basics** (weight 7%): `<title>` length 20–70, `<meta name="description">` length 70–160, `<link rel="canonical">` (absolute, single), `<html lang>` (BCP 47), `<meta charset="utf-8">`, `<meta name="viewport">`, hreflang completeness with `x-default`. Title/description duplication detection.
28
+ - **tls-https** (weight 5%): site is served over HTTPS, HTTP redirects to HTTPS, HSTS `max-age` >= 6 months (1 year for preload), `includeSubDomains`, `preload` directive eligibility per https://hstspreload.org.
29
+ - **well-known-ai** (weight 3%): emerging AI-specific discovery files — `/.well-known/ai.txt` (Spawning), `/.well-known/genai.txt`, `/ai-plugin.json` (legacy ChatGPT plugin), `/agents.json` (Wildcard / OpenAgents), `/.well-known/nlweb.json` (Microsoft NLWeb). Each present file scores; coverage is bonus rather than baseline.
30
+
31
+ ### Improved — existing checks
32
+
33
+ - **meta-tags**: now validates Open Graph completeness (`og:title`, `og:description`, `og:url`, `og:type`, `og:image`, `og:site_name`) and Twitter Card completeness (`twitter:card`, `twitter:title`, `twitter:description`, `twitter:image`). Reuses shared HTML utilities for tag matching.
34
+ - **agent-json**: validates the `url` field is absolute and matches the audited origin, and that every `skills[]` entry has both `id` and `description`.
35
+ - **llms-txt / agent-json / mcp / openapi**: validate `Content-Type` of the fetched resource (`text/plain` / `text/markdown` for llms.txt; `application/json` for the JSON manifests). Penalty: −5 per mismatch.
36
+ - **robots-txt**: `CORE_AI_CRAWLERS` extended (now 8 entries: GPTBot, ClaudeBot, ChatGPT-User, Claude-SearchBot, Google-Extended, PerplexityBot, OAI-SearchBot, CCBot). `ALL_AI_CRAWLERS` extended with MistralAI-User, KagiBot, GeminiBot, Goose, AwarioBot family, Bingbot, ImagesiftBot, omgili, Webzio-Extended, and others (47 known crawlers total).
37
+
38
+ ### Refactored
39
+
40
+ - New shared module `src/checks/html-utils.ts` with regex-based primitives for HTML inspection (`getMetaContent`, `findLinkTags`, `findMetaTagsByPrefix`, `extractVisibleText`, `countExecutableScripts`, `getTagAttribute`, …). Eliminates duplicated regex code across `meta-tags`, `seo-basics`, `html-rendering`, and `structured-data`.
41
+ - New shared utility `checkContentType` in `src/checks/utils.ts` for consistent Content-Type validation.
42
+
43
+ ### Scoring
44
+
45
+ - Weights redistributed across 14 checks, total still sums to 100. New highest-weight signals are llms-txt and robots-txt (11% each) followed by html-rendering / structured-data / http-headers (9%).
46
+
47
+ ### Tests
48
+
49
+ - 198 tests total (77 new). New suites: html-rendering (14), sitemap (12), seo-basics (19), tls-https (11), well-known-ai (8). Plus expanded meta-tags / agent-json / mcp / openapi / llms-txt suites for the new validations.
50
+
51
+ ### Breaking
52
+
53
+ - Score deltas vs v2.x are expected on the same site because (a) weights were redistributed across 14 checks instead of 9, and (b) Content-Type validation on `/llms.txt` and the `.well-known` JSON manifests now applies a −5 penalty per mismatch. Sites previously scoring 100 may drop a few points until the new signals are addressed. Use `--baseline` to track regressions explicitly.
54
+
5
55
  ## [2.4.0] - 2026-04-16
6
56
 
7
57
  ### Added
package/README.md CHANGED
@@ -19,16 +19,28 @@ npx ax-audit https://your-site.com
19
19
  AX Audit Report
20
20
  https://lucioduran.com
21
21
 
22
- ███████████████████████████████████████░ 98/100 Excellent
22
+ ███████████████████████████████████░░░░░ 88/100 Good
23
23
 
24
24
  LLMs.txt (100/100)
25
25
  PASS /llms.txt exists
26
+ PASS /llms.txt Content-Type OK (text/plain)
26
27
  PASS H1 heading: "Lucio Duran — Personal Portfolio"
27
28
  PASS /llms-full.txt also available (bonus)
28
29
 
29
30
  Robots.txt (100/100)
30
- PASS All 6 core AI crawlers explicitly configured
31
- PASS 31/31 known AI crawlers have explicit rules
31
+ PASS All 8 core AI crawlers explicitly configured
32
+ PASS 32/47 known AI crawlers have explicit rules
33
+
34
+ HTML Rendering (90/100)
35
+ PASS Server-rendered content detected (473 words)
36
+ PASS Semantic landmarks present (main, article, header, footer, nav)
37
+ PASS Single <h1> heading
38
+ PASS 3/3 <img> tags have alt attributes
39
+
40
+ TLS / HTTPS (100/100)
41
+ PASS Site is served over HTTPS
42
+ PASS HTTP requests redirect to HTTPS
43
+ PASS HSTS preload-eligible
32
44
  ...
33
45
  ```
34
46
 
@@ -40,15 +52,23 @@ AI agents and LLMs are increasingly crawling, indexing, and interacting with web
40
52
 
41
53
  | Check | What it audits | Weight |
42
54
  |---|---|---|
43
- | **LLMs.txt** | `/llms.txt` presence and [llmstxt.org](https://llmstxt.org) spec compliance | 15% |
44
- | **Robots.txt** | AI crawler configuration, wildcard detection, partial path restrictions | 15% |
45
- | **Structured Data** | JSON-LD on homepage (schema.org, `@graph`, entity types) | 13% |
46
- | **HTTP Headers** | Security headers + AI discovery `Link` headers + CORS on `.well-known` | 13% |
47
- | **Agent Card** | `/.well-known/agent.json` [A2A protocol](https://a2a-protocol.org) compliance | 10% |
48
- | **MCP** | `/.well-known/mcp.json` [Model Context Protocol](https://modelcontextprotocol.io) server config | 10% |
49
- | **Security.txt** | `/.well-known/security.txt` [RFC 9116](https://www.rfc-editor.org/rfc/rfc9116) compliance | 8% |
50
- | **Meta Tags** | AI meta tags (`ai:*`), `rel="alternate"` to llms.txt, `rel="me"` identity links | 8% |
51
- | **OpenAPI** | `/.well-known/openapi.json` presence and schema validity | 8% |
55
+ | **LLMs.txt** | `/llms.txt` presence, [llmstxt.org](https://llmstxt.org) spec, Content-Type | 11% |
56
+ | **Robots.txt** | AI crawler configuration (40+ known crawlers), wildcard detection, partial path restrictions | 11% |
57
+ | **HTML Rendering** | Server-rendered content, semantic landmarks, SPA-shell detection, alt coverage | 9% |
58
+ | **Structured Data** | JSON-LD on homepage (schema.org, `@graph`, entity types) | 9% |
59
+ | **HTTP Headers** | Security headers + AI discovery `Link` headers + CORS on `.well-known` | 9% |
60
+ | **Agent Card** | `/.well-known/agent.json` [A2A protocol](https://a2a-protocol.org) + same-origin url + skill quality | 7% |
61
+ | **MCP** | `/.well-known/mcp.json` [Model Context Protocol](https://modelcontextprotocol.io) server config | 7% |
62
+ | **SEO Basics** | `<title>`, meta description, canonical, `<html lang>`, charset, viewport, hreflang | 7% |
63
+ | **Security.txt** | `/.well-known/security.txt` [RFC 9116](https://www.rfc-editor.org/rfc/rfc9116) compliance | 6% |
64
+ | **Meta Tags** | AI meta tags (`ai:*`), `rel="alternate"`, `rel="me"`, OpenGraph + Twitter Card completeness | 6% |
65
+ | **OpenAPI** | `/.well-known/openapi.json` presence, schema validity, Content-Type | 6% |
66
+ | **TLS / HTTPS** | HTTPS, HTTP→HTTPS redirect, HSTS with `preload` + `includeSubDomains` | 5% |
67
+ | **Sitemap** | `sitemap.xml` (or `Sitemap:` from robots.txt) — XML validity, `<lastmod>` coverage, freshness, sitemap-index handling | 4% |
68
+ | **AI Well-Known** | Emerging files: `/.well-known/ai.txt`, `genai.txt`, `ai-plugin.json`, `agents.json`, `nlweb.json` | 3% |
69
+ | **Content Negotiation** | Markdown for agents — `Accept: text/markdown` negotiation, `Vary: Accept`, `rel="alternate"` fallback | 0%* |
70
+
71
+ \* **Content Negotiation** is informational in 3.x: it runs and reports findings but does not affect the overall score. It will gain weight in v4.0.
52
72
 
53
73
  ## Install
54
74
 
@@ -232,15 +252,21 @@ Fail on regressions using a committed baseline:
232
252
 
233
253
  | Check ID | Use with `--checks` |
234
254
  |---|---|
235
- | `llms-txt` | LLMs.txt spec compliance |
236
- | `robots-txt` | AI crawler configuration |
255
+ | `llms-txt` | LLMs.txt spec + Content-Type |
256
+ | `robots-txt` | AI crawler configuration (40+ crawlers) |
257
+ | `html-rendering` | SSR / SPA-shell detection + semantic HTML |
237
258
  | `structured-data` | JSON-LD structured data |
238
259
  | `http-headers` | Security + AI discovery headers |
239
- | `agent-json` | A2A Agent Card |
260
+ | `agent-json` | A2A Agent Card + same-origin validation |
240
261
  | `mcp` | MCP server configuration |
262
+ | `seo-basics` | title / description / canonical / lang / hreflang |
241
263
  | `security-txt` | RFC 9116 Security.txt |
242
- | `meta-tags` | AI meta tags and identity links |
264
+ | `meta-tags` | AI meta tags + OpenGraph + Twitter Card |
243
265
  | `openapi` | OpenAPI specification |
266
+ | `tls-https` | HTTPS + HTTP→HTTPS redirect + HSTS preload |
267
+ | `sitemap` | sitemap.xml validation + freshness |
268
+ | `well-known-ai` | Emerging AI discovery files |
269
+ | `content-negotiation` | Markdown via `Accept: text/markdown` (informational) |
244
270
 
245
271
  ## Testing
246
272
 
@@ -248,7 +274,7 @@ Fail on regressions using a committed baseline:
248
274
  npm test
249
275
  ```
250
276
 
251
- 121 tests covering all 9 checks, the scorer, baseline comparison, and edge cases. Uses Node.js built-in test runner (`node:test`), no extra test dependencies.
277
+ 229 tests covering all 15 checks, the scorer, the HTTP fetcher (against a real local server), baseline comparison, HTML parsing utilities, and edge cases. Uses Node.js built-in test runner (`node:test`), no extra test dependencies.
252
278
 
253
279
  ## Tech Stack
254
280
 
@@ -1,4 +1,14 @@
1
1
  import type { CheckContext, CheckResult, CheckMeta } from '../types.js';
2
+ /**
3
+ * "agent-json" — `/.well-known/agent.json` per the A2A (Agent-to-Agent) protocol.
4
+ *
5
+ * Validates the document on three axes:
6
+ * 1. JSON well-formedness
7
+ * 2. Required fields per the A2A spec (`name`, `description`, `url`, `skills`)
8
+ * 3. Field semantics: `url` should resolve to the same origin as the audited site,
9
+ * `skills[]` should each declare an `id` and `description`, and protocol/optional
10
+ * fields are present where expected.
11
+ */
2
12
  export declare const meta: CheckMeta;
3
13
  export default function check(ctx: CheckContext): Promise<CheckResult>;
4
14
  //# sourceMappingURL=agent-json.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-json.d.ts","sourceRoot":"","sources":["../../src/checks/agent-json.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAW,MAAM,aAAa,CAAC;AAGjF,eAAO,MAAM,IAAI,EAAE,SAKlB,CAAC;AAEF,wBAA8B,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+F3E"}
1
+ {"version":3,"file":"agent-json.d.ts","sourceRoot":"","sources":["../../src/checks/agent-json.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAW,MAAM,aAAa,CAAC;AAGjF;;;;;;;;;GASG;AACH,eAAO,MAAM,IAAI,EAAE,SAKlB,CAAC;AAEF,wBAA8B,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA0I3E"}
@@ -1,11 +1,21 @@
1
1
  import { AGENT_JSON_REQUIRED_FIELDS } from '../constants.js';
2
2
  import { guideUrl } from '../guide-urls.js';
3
- import { buildResult } from './utils.js';
3
+ import { buildResult, checkContentType } from './utils.js';
4
+ /**
5
+ * "agent-json" — `/.well-known/agent.json` per the A2A (Agent-to-Agent) protocol.
6
+ *
7
+ * Validates the document on three axes:
8
+ * 1. JSON well-formedness
9
+ * 2. Required fields per the A2A spec (`name`, `description`, `url`, `skills`)
10
+ * 3. Field semantics: `url` should resolve to the same origin as the audited site,
11
+ * `skills[]` should each declare an `id` and `description`, and protocol/optional
12
+ * fields are present where expected.
13
+ */
4
14
  export const meta = {
5
15
  id: 'agent-json',
6
16
  name: 'Agent Card (A2A)',
7
17
  description: 'Checks /.well-known/agent.json A2A protocol compliance',
8
- weight: 10,
18
+ weight: 7,
9
19
  };
10
20
  export default async function check(ctx) {
11
21
  const start = performance.now();
@@ -23,6 +33,15 @@ export default async function check(ctx) {
23
33
  return buildResult(meta, 0, findings, start);
24
34
  }
25
35
  findings.push({ status: 'pass', message: '/.well-known/agent.json exists' });
36
+ const ctFinding = checkContentType(res, ['application/json'], {
37
+ checkId: meta.id,
38
+ resourceLabel: '/.well-known/agent.json',
39
+ anchor: 'wrong-content-type',
40
+ });
41
+ if (ctFinding) {
42
+ findings.push(ctFinding);
43
+ score -= 5;
44
+ }
26
45
  let data;
27
46
  try {
28
47
  data = JSON.parse(res.body);
@@ -51,8 +70,42 @@ export default async function check(ctx) {
51
70
  score -= 15;
52
71
  }
53
72
  }
73
+ if (typeof data.url === 'string' && data.url.length > 0) {
74
+ const sameOrigin = sameHost(data.url, ctx.url);
75
+ if (sameOrigin === false) {
76
+ findings.push({
77
+ status: 'warn',
78
+ message: `agent.json "url" points to a different origin: ${data.url}`,
79
+ hint: 'The url field should match the audited site origin. Pointing it elsewhere can confuse agents about the canonical agent endpoint.',
80
+ learnMoreUrl: guideUrl(meta.id, 'url-mismatch'),
81
+ });
82
+ score -= 5;
83
+ }
84
+ else if (sameOrigin === null) {
85
+ findings.push({
86
+ status: 'warn',
87
+ message: `agent.json "url" is not a valid absolute URL: ${data.url}`,
88
+ hint: 'Provide an absolute https:// URL for the url field.',
89
+ learnMoreUrl: guideUrl(meta.id, 'url-invalid'),
90
+ });
91
+ score -= 5;
92
+ }
93
+ }
54
94
  if (Array.isArray(data.skills) && data.skills.length > 0) {
55
95
  findings.push({ status: 'pass', message: `${data.skills.length} skill(s) defined` });
96
+ const incomplete = data.skills.filter((s) => !s.id || !s.description);
97
+ if (incomplete.length === 0) {
98
+ findings.push({ status: 'pass', message: 'All skills have id + description' });
99
+ }
100
+ else {
101
+ findings.push({
102
+ status: 'warn',
103
+ message: `${incomplete.length}/${data.skills.length} skill(s) missing id or description`,
104
+ hint: 'Each entry in skills[] should include both an id and a description so agents can address it and reason about its purpose.',
105
+ learnMoreUrl: guideUrl(meta.id, 'incomplete-skills'),
106
+ });
107
+ score -= 5;
108
+ }
56
109
  }
57
110
  else if (Array.isArray(data.skills)) {
58
111
  findings.push({
@@ -100,4 +153,15 @@ export default async function check(ctx) {
100
153
  }
101
154
  return buildResult(meta, score, findings, start);
102
155
  }
156
+ /** Returns `true` when the two URLs share host (case-insensitive), `false` if hosts differ, `null` on parse error. */
157
+ function sameHost(a, b) {
158
+ try {
159
+ const ua = new URL(a);
160
+ const ub = new URL(b);
161
+ return ua.host.toLowerCase() === ub.host.toLowerCase();
162
+ }
163
+ catch {
164
+ return null;
165
+ }
166
+ }
103
167
  //# sourceMappingURL=agent-json.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-json.js","sourceRoot":"","sources":["../../src/checks/agent-json.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent-json.js","sourceRoot":"","sources":["../../src/checks/agent-json.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { CheckContext, CheckResult, CheckMeta } from '../types.js';
2
+ export declare const meta: CheckMeta;
3
+ export default function check(ctx: CheckContext): Promise<CheckResult>;
4
+ //# sourceMappingURL=content-negotiation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-negotiation.d.ts","sourceRoot":"","sources":["../../src/checks/content-negotiation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAW,MAAM,aAAa,CAAC;AAIjF,eAAO,MAAM,IAAI,EAAE,SAKlB,CAAC;AAyBF,wBAA8B,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAuH3E"}
@@ -0,0 +1,138 @@
1
+ import { guideUrl } from '../guide-urls.js';
2
+ import { buildResult } from './utils.js';
3
+ import { findLinkTags, getAttribute } from './html-utils.js';
4
+ export const meta = {
5
+ id: 'content-negotiation',
6
+ name: 'Content Negotiation',
7
+ description: 'Checks whether the homepage serves Markdown to AI agents via Accept: text/markdown',
8
+ weight: 0, // Informational in 3.x — will gain weight in v4.0 (score-affecting changes are treated as breaking).
9
+ };
10
+ /** Markdown sent by agents that support content negotiation (Claude Code, Cursor, OpenCode). */
11
+ const MARKDOWN_ACCEPT = 'text/markdown';
12
+ /** Score when Markdown is not negotiated but a `<link rel="alternate" type="text/markdown">` exists. */
13
+ const ALTERNATE_ONLY_SCORE = 40;
14
+ /**
15
+ * Detect an HTML document masquerading as Markdown. Markdown may legally
16
+ * contain inline HTML, so this only flags full documents (doctype / <html> /
17
+ * <head> at the start), not embedded tags.
18
+ */
19
+ function looksLikeHtmlDocument(body) {
20
+ return /^\s*(?:<!doctype\s+html|<html[\s>]|<head[\s>])/i.test(body);
21
+ }
22
+ /** Find `<link rel="alternate" type="text/markdown">` tags in the homepage HTML. */
23
+ function findMarkdownAlternates(html) {
24
+ return findLinkTags(html, 'alternate').filter((tag) => {
25
+ const type = getAttribute(tag, 'type');
26
+ return type !== null && type.toLowerCase().includes('text/markdown');
27
+ });
28
+ }
29
+ export default async function check(ctx) {
30
+ const start = performance.now();
31
+ const findings = [];
32
+ const res = await ctx.fetch(ctx.url, { headers: { Accept: MARKDOWN_ACCEPT } });
33
+ if (res.status === 0) {
34
+ findings.push({
35
+ status: 'fail',
36
+ message: 'Could not fetch homepage with "Accept: text/markdown"',
37
+ detail: res.error ?? 'Network error',
38
+ learnMoreUrl: guideUrl(meta.id, 'fetch-error'),
39
+ });
40
+ return buildResult(meta, 0, findings, start);
41
+ }
42
+ const contentType = (res.headers['content-type'] ?? '').toLowerCase();
43
+ const servesMarkdown = res.ok && contentType.includes(MARKDOWN_ACCEPT);
44
+ if (!servesMarkdown) {
45
+ findings.push({
46
+ status: 'fail',
47
+ message: 'Homepage does not serve Markdown via content negotiation',
48
+ detail: res.status === 406
49
+ ? 'Server responded 406 Not Acceptable to "Accept: text/markdown"'
50
+ : `Got ${contentType.split(';')[0] || 'no Content-Type'} (HTTP ${res.status}) for "Accept: text/markdown"`,
51
+ hint: 'Serve a Markdown representation of your pages when agents request "Accept: text/markdown". ' +
52
+ 'Agents like Claude Code and Cursor ask for it, and Markdown cuts token usage by ~80% vs HTML. ' +
53
+ 'Cloudflare ("Markdown for Agents") and Vercel can enable this without code changes.',
54
+ learnMoreUrl: guideUrl(meta.id, res.status === 406 ? 'http-406' : 'not-supported'),
55
+ });
56
+ const alternates = findMarkdownAlternates(ctx.html);
57
+ if (alternates.length > 0) {
58
+ findings.push({
59
+ status: 'pass',
60
+ message: `Markdown alternate advertised via <link rel="alternate" type="text/markdown"> (${alternates.length} link tag(s))`,
61
+ detail: 'Discoverable, but agents must perform an extra fetch instead of negotiating the same URL.',
62
+ });
63
+ return buildResult(meta, ALTERNATE_ONLY_SCORE, findings, start);
64
+ }
65
+ findings.push({
66
+ status: 'warn',
67
+ message: 'No <link rel="alternate" type="text/markdown"> fallback found on the homepage',
68
+ hint: 'If you cannot enable content negotiation, advertise a Markdown version with ' +
69
+ '<link rel="alternate" type="text/markdown" href="/index.md"> so agents can discover it.',
70
+ learnMoreUrl: guideUrl(meta.id, 'no-alternate'),
71
+ });
72
+ return buildResult(meta, 0, findings, start);
73
+ }
74
+ let score = 100;
75
+ findings.push({
76
+ status: 'pass',
77
+ message: 'Homepage serves Markdown via content negotiation (Accept: text/markdown)',
78
+ });
79
+ if (res.body.trim().length === 0) {
80
+ findings.push({
81
+ status: 'warn',
82
+ message: 'Markdown response body is empty',
83
+ hint: 'Return the page content as Markdown — an empty body gives agents nothing to work with.',
84
+ learnMoreUrl: guideUrl(meta.id, 'empty-body'),
85
+ });
86
+ score -= 30;
87
+ }
88
+ else if (looksLikeHtmlDocument(res.body)) {
89
+ findings.push({
90
+ status: 'warn',
91
+ message: 'Response is labeled text/markdown but the body is an HTML document',
92
+ hint: 'Convert the page to actual Markdown instead of relabeling the HTML response.',
93
+ learnMoreUrl: guideUrl(meta.id, 'mislabeled-content-type'),
94
+ });
95
+ score -= 25;
96
+ }
97
+ else {
98
+ findings.push({ status: 'pass', message: 'Response body is Markdown, not an HTML document' });
99
+ }
100
+ const vary = (res.headers['vary'] ?? '').toLowerCase();
101
+ const variesOnAccept = vary
102
+ .split(',')
103
+ .map((v) => v.trim())
104
+ .some((v) => v === 'accept' || v === '*');
105
+ if (variesOnAccept) {
106
+ findings.push({ status: 'pass', message: 'Vary: Accept present (caches keep HTML and Markdown apart)' });
107
+ }
108
+ else {
109
+ findings.push({
110
+ status: 'warn',
111
+ message: 'Vary header does not include "Accept"',
112
+ detail: vary ? `Vary: ${res.headers['vary']}` : 'No Vary header',
113
+ hint: 'Send "Vary: Accept" when the same URL serves both HTML and Markdown, ' +
114
+ 'otherwise shared caches and CDNs may serve Markdown to browsers (or HTML to agents).',
115
+ learnMoreUrl: guideUrl(meta.id, 'missing-vary'),
116
+ });
117
+ score -= 15;
118
+ }
119
+ if (ctx.html.length > 0 && res.body.length > 0) {
120
+ const reduction = Math.round((1 - res.body.length / ctx.html.length) * 100);
121
+ if (reduction > 0) {
122
+ findings.push({
123
+ status: 'pass',
124
+ message: `Markdown is ~${reduction}% lighter than the HTML representation (${res.body.length} vs ${ctx.html.length} bytes)`,
125
+ });
126
+ }
127
+ else {
128
+ findings.push({
129
+ status: 'warn',
130
+ message: `Markdown response is not smaller than the HTML representation (${res.body.length} vs ${ctx.html.length} bytes)`,
131
+ hint: 'Strip navigation, boilerplate, and markup remnants from the Markdown output — its purpose is token efficiency.',
132
+ learnMoreUrl: guideUrl(meta.id, 'not-smaller'),
133
+ });
134
+ }
135
+ }
136
+ return buildResult(meta, score, findings, start);
137
+ }
138
+ //# sourceMappingURL=content-negotiation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-negotiation.js","sourceRoot":"","sources":["../../src/checks/content-negotiation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { CheckContext, CheckResult, CheckMeta } from '../types.js';
2
+ /**
3
+ * "html-rendering" — does the HTML delivered to a non-JS agent actually contain content?
4
+ *
5
+ * Most AI crawlers (GPTBot, ClaudeBot, CCBot, etc.) do **not** execute JavaScript. A site
6
+ * built as a client-rendered SPA returns an empty `<div id="root"></div>` shell to those
7
+ * agents, and is effectively invisible regardless of how good its `llms.txt` and structured
8
+ * data are. This check estimates server-side rendering by inspecting the static HTML body.
9
+ *
10
+ * Signals (each contributes to the score):
11
+ * - Visible text length and word count (low → likely JS-rendered shell)
12
+ * - Text-to-markup ratio (high JS, no text → SPA)
13
+ * - Presence of semantic landmarks (`<main>`, `<article>`, `<header>`, `<footer>`, `<nav>`)
14
+ * - Single, meaningful `<h1>`
15
+ * - `<noscript>` fallback for JS-only frameworks
16
+ * - Non-empty SPA root containers (`#root`, `#app`, `#__next`)
17
+ * - Image `alt` attribute coverage
18
+ */
19
+ export declare const meta: CheckMeta;
20
+ export default function check(ctx: CheckContext): Promise<CheckResult>;
21
+ //# sourceMappingURL=html-rendering.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-rendering.d.ts","sourceRoot":"","sources":["../../src/checks/html-rendering.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAW,MAAM,aAAa,CAAC;AAIjF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,IAAI,EAAE,SAKlB,CAAC;AAOF,wBAA8B,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA2K3E"}