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.
- package/CHANGELOG.md +50 -0
- package/README.md +43 -17
- package/dist/checks/agent-json.d.ts +10 -0
- package/dist/checks/agent-json.d.ts.map +1 -1
- package/dist/checks/agent-json.js +66 -2
- package/dist/checks/agent-json.js.map +1 -1
- package/dist/checks/content-negotiation.d.ts +4 -0
- package/dist/checks/content-negotiation.d.ts.map +1 -0
- package/dist/checks/content-negotiation.js +138 -0
- package/dist/checks/content-negotiation.js.map +1 -0
- package/dist/checks/html-rendering.d.ts +21 -0
- package/dist/checks/html-rendering.d.ts.map +1 -0
- package/dist/checks/html-rendering.js +204 -0
- package/dist/checks/html-rendering.js.map +1 -0
- package/dist/checks/html-utils.d.ts +37 -0
- package/dist/checks/html-utils.d.ts.map +1 -0
- package/dist/checks/html-utils.js +93 -0
- package/dist/checks/html-utils.js.map +1 -0
- package/dist/checks/http-headers.js +1 -1
- package/dist/checks/http-headers.js.map +1 -1
- package/dist/checks/index.d.ts.map +1 -1
- package/dist/checks/index.js +12 -0
- package/dist/checks/index.js.map +1 -1
- package/dist/checks/llms-txt.d.ts.map +1 -1
- package/dist/checks/llms-txt.js +17 -2
- package/dist/checks/llms-txt.js.map +1 -1
- package/dist/checks/mcp.d.ts.map +1 -1
- package/dist/checks/mcp.js +11 -2
- package/dist/checks/mcp.js.map +1 -1
- package/dist/checks/meta-tags.d.ts +13 -0
- package/dist/checks/meta-tags.d.ts.map +1 -1
- package/dist/checks/meta-tags.js +104 -24
- package/dist/checks/meta-tags.js.map +1 -1
- package/dist/checks/openapi.d.ts.map +1 -1
- package/dist/checks/openapi.js +11 -2
- package/dist/checks/openapi.js.map +1 -1
- package/dist/checks/robots-txt.js +1 -1
- package/dist/checks/security-txt.js +1 -1
- package/dist/checks/seo-basics.d.ts +13 -0
- package/dist/checks/seo-basics.d.ts.map +1 -0
- package/dist/checks/seo-basics.js +222 -0
- package/dist/checks/seo-basics.js.map +1 -0
- package/dist/checks/sitemap.d.ts +12 -0
- package/dist/checks/sitemap.d.ts.map +1 -0
- package/dist/checks/sitemap.js +241 -0
- package/dist/checks/sitemap.js.map +1 -0
- package/dist/checks/structured-data.js +1 -1
- package/dist/checks/structured-data.js.map +1 -1
- package/dist/checks/tls-https.d.ts +13 -0
- package/dist/checks/tls-https.d.ts.map +1 -0
- package/dist/checks/tls-https.js +164 -0
- package/dist/checks/tls-https.js.map +1 -0
- package/dist/checks/utils.d.ts +13 -1
- package/dist/checks/utils.d.ts.map +1 -1
- package/dist/checks/utils.js +28 -0
- package/dist/checks/utils.js.map +1 -1
- package/dist/checks/well-known-ai.d.ts +17 -0
- package/dist/checks/well-known-ai.d.ts.map +1 -0
- package/dist/checks/well-known-ai.js +123 -0
- package/dist/checks/well-known-ai.js.map +1 -0
- package/dist/constants.d.ts +19 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +50 -10
- package/dist/constants.js.map +1 -1
- package/dist/fetcher.d.ts +2 -2
- package/dist/fetcher.d.ts.map +1 -1
- package/dist/fetcher.js +42 -9
- package/dist/fetcher.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/scorer.d.ts.map +1 -1
- package/dist/scorer.js +8 -0
- package/dist/scorer.js.map +1 -1
- package/dist/types.d.ts +10 -1
- package/dist/types.d.ts.map +1 -1
- 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
|
-
|
|
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
|
|
31
|
-
PASS
|
|
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
|
|
44
|
-
| **Robots.txt** | AI crawler configuration, wildcard detection, partial path restrictions |
|
|
45
|
-
| **
|
|
46
|
-
| **
|
|
47
|
-
| **
|
|
48
|
-
| **
|
|
49
|
-
| **
|
|
50
|
-
| **
|
|
51
|
-
| **
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-json.js","sourceRoot":"","sources":["../../src/checks/agent-json.ts"],"names":[],"mappings":"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"}
|
|
@@ -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"}
|