feat(audit): AI readiness checks — robots.txt AI crawlers, llms.txt, and Markdown alternates (part 1 of #99) - #122
Conversation
sorcerai
left a comment
There was a problem hiding this comment.
Reviewed exact head against upstream f569726: there is no duplicate ai-crawler-analysis implementation on upstream main. Three blockers remain. (1) /llms.txt uses redirect-following fetch with no final-origin validation and reads response.text() unbounded; prevent cross-origin redirects and use the existing bounded reader. (2) no-markdown is emitted when every crawled page is redirect/non-HTML/blocked/error because those states collapse to hasMarkdownAlternate=false; evaluate only analyzable successful HTML pages or preserve unknown. (3) blockedAgents is an array of objects, but IssuesView joins arrays and renders [object Object]; make details UI-compatible or update the renderer. Add integration coverage for redirects/body bounds and non-analyzable markdown cases, then rerun CI on current main.
…ve, UI) Resolve the three blockers from @sorcerai's review of every-app#122: 1. llms.txt fetch hardened against SSRF: use redirect:"manual", reject cross-origin redirects, and re-validate same-origin hops through normalizeAndValidateStartUrl. Read the body via the shared bounded reader (readBoundedText) instead of an unbounded response.text(). 2. no-markdown-alternates no longer fires on sites we never read: redirect / blocked / error / non-HTML pages default hasMarkdownAlternate to false, so the check now counts only analyzed HTML pages (new isHtml flag on StepPageSummary) and stays silent when none were analyzable. 3. blockedAgents is now a string[] ("GPTBot (OpenAI)") so IssuesView's generic array renderer shows names, not [object Object]. Add integration coverage for cross-origin/same-origin redirects, the HTML app-shell case, the body-size bound, and the non-analyzable markdown cases. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: WAHIB-EL-KHADIRI <wahibelkhadiri06@gmail.com>
|
Thanks for the thorough review @sorcerai — all three addressed in abb9fc6:
Added integration coverage for cross-origin/same-origin redirects, the HTML app-shell case, the body-size bound, and the non-analyzable markdown cases. Typecheck + lint clean; server audit/workflow tests green. |
Rebased onto current main and reworked per review: - Split off the Markdown-alternates check (it depended on StepPageSummary, which main has since removed); this PR is now the two self-contained checks: robots.txt treatment of known AI user agents, and llms.txt. - fetchLlmsTxt now uses redirect:"manual" with a same-origin guard and re-validates the hop through normalizeAndValidateStartUrl (no SSRF), and reads the body through the shared bounded reader (no unbounded buffering). - blockedAgents detail is now a flat string, so the issues UI renders the agent list instead of "[object Object]". - Integration tests cover the redirect (same/cross-origin), body-bound, and HTML-catch-all paths. Co-Authored-By: Claude <noreply@anthropic.com>
abb9fc6 to
853f585
Compare
|
Thanks for the thorough review — reworked and rebased onto current Rebase / scope: Blocker (1) — SSRF + unbounded read on Blocker (3) — Tests: added integration coverage for the redirect paths (same-origin follow, cross-origin refusal), the body-bound cap, and the HTML catch-all. Locally: the module's tests pass, |
|
Hey, Thanks for contributing! For the next few months, I'm going to stop accepting external PRs. Reviewing and testing them has been slowing down progress against the roadmap. Right now, the best way to contribute is through creating quality "Issues" which are easier for me to review and prioritize right now. Here is our updated contributing guide: https://github.com/every-app/open-seo/blob/main/docs/CONTRIBUTING.md Converting, this PR to an issue would be the best way to get it prioritized. You can use the the |
|
Understood - closing rather than leaving it open against the new process. Not filing a separate issue for this one: #99 already describes the feature and is still open, and this PR was only part 1 of it. The branch stays up if the implementation is useful as a reference for that issue. |
Part of #99 — now covers three of the four priority checks from the issue: AI crawlers in robots.txt, llms.txt validation, and Markdown alternates. JSON-LD graph quality can follow as a separate PR (it's the lowest-priority item and touches page-analyzer structured-data parsing, a bigger diff on its own).
What this adds
Six new issue types in the shared registry, all site-level (
pageId: null— the schema already allows it):ai-search-crawlers-blockedai-user-fetchers-blockedai-training-crawlers-blockedmissing-llms-txtllms-txt-invalidno-markdown-alternates<link rel="alternate" type="text/markdown">(absence is common — heads-up, not an error)Design decisions
*rules (checked via a probe UA that falls through to*). A site with a blanketDisallow: /made a site-wide choice — flagging every AI agent there would be noise.details.blockedAgentswith vendor names, so a site blocking 7 training crawlers gets 1 row, not 7.<!doctypesniff) and treated as missing, not invalid; unreachable fetches stay silent instead of mis-reporting. Structure validation is deliberately lenient — the spec's only hard requirement is the H1 title; optional blockquote/sections are not flagged.page-analyzer.tsdetects the<link rel=alternate type="text/markdown">tag during the existing per-page cheerio pass (same pattern as the neighboring hreflang extraction) — no extra fetches. The flag flows transiently throughCrawledPageResult→ the crawl step's slimStepPageSummary(same path already used fortitle/statusCode), so it needed no new persisted column or schema migration — the finding is only ever recorded as anaudit_issuesrow.multipage-checksfinalize step and reuses the robots.txt text the discovery phase already checkpoints (replay-safe). The only new network cost is a single llms.txt fetch. No external API credits — the shared registry automatically covers the issues UI, CSV export, and MCP.Testing
ai-readiness.test.ts: purpose grouping, blanket-block silence, subpath-only blocks ignored, dedupe keys, llms.txt valid/empty/no-H1/missing/unreachable cases, markdown-alternates empty-crawl/found/none-found cases.tsc --noEmit,oxlint,knip,prettierall clean. Full vitest suite (src/server/lib/audit): 58/58 passing.🤖 Generated with Claude Code