feat(agents): publish a machine-readable surface for AI agents - #227
Open
naveen-bandela-au2 wants to merge 3 commits into
Open
feat(agents): publish a machine-readable surface for AI agents#227naveen-bandela-au2 wants to merge 3 commits into
naveen-bandela-au2 wants to merge 3 commits into
Conversation
gofr.dev scores 53/100 on orank's agent-readiness scan. Agents landing
here from web search get a React shell of navigation chrome, and there
is no way for them to discover what the site offers or how to read it.
Adds the machine surface:
- A Markdown twin for every content route, generated at build time from
the same page.md the HTML is rendered from. Markdoc tags are rendered
down to plain Markdown (fence-aware, so code samples containing `{%`
survive), internal links are made absolute, and each twin opens with a
top-level heading. Discoverable via <link rel="alternate"> in the page
head, and served on `Accept: text/markdown` once static-server is
bumped to v1.0.0 in gofr-dev/gofr.
- Scoped /docs/llms.txt and /docs/{section}/llms.txt, so an agent working
on one area can skip the whole-site dump.
- /openapi.json describing the read surface that actually exists — the
indexes, the twins, the sitemap, the changelog feed. The `page`
parameter is enumerated from the generated twin list, so an agent
doing function-calling cannot ask for a path that 404s. Validates
clean under redocly lint.
- /auth.md and /.well-known/oauth-protected-resource stating the real
access model: anonymous, no registration, no token. Every URI they
advertise resolves.
- /.well-known/{ard.json,agent-card.json,agent-skills/index.json,
mcp/server-card.json,api-catalog}, /agent/identity, and a Markdown
homepage at /index.md for cold arrivals.
- /schemamap.xml + /feeds/docs.jsonl (NLWeb Schema Feeds), referenced
from robots.txt.
- public/_headers for site-wide Link, nosniff, Referrer-Policy, and the
media types the extensionless well-known files need.
Also:
- /privacy — the site loads Google Tag Manager and had no policy at all,
which is a GDPR exposure independent of any scan.
- /contact — routes bug reports, security disclosures, and questions to
the channels that already exist, and makes the new Organization
contactPoint JSON-LD truthful.
- Organization gains contactPoint; a WebSite node is added. `address`
and SearchAction are deliberately omitted: there is no public postal
address and no /search route, and inventing either to satisfy a
validator would be worse than leaving them out.
- robots.txt gains per-group Content-Signal (search, AI input, and
training all permitted — the docs are Apache-2.0 and we want them in
open corpora) and a schemamap directive.
- Route lists shared by the generators are extracted to
utils/lib/doc-sections.mjs so they cannot drift.
Twins and generated indexes are gitignored: they derive from the docs
that gofr-dev/gofr overlays at build time, so a copy committed here
would be permanently stale.
Adversarial review of this PR found seven defects. Fixes: generate-llms-full.mjs kept its own copy of the section list while doc-sections.mjs claimed to be the single source of truth. The lists had already drifted: /privacy and /contact were missing from llms-full.txt (103 pages, now 105). Wired it to the shared list and corrected the module header, which also named sitemap.js as a consumer when sitemap.js deliberately globs instead (it must cover .jsx routes that carry no Markdoc content). contact/page.md pointed at discord.gg/wsaSkQTdgq. Every other reference in the repo (page.jsx, Footer.jsx, humans.txt) uses 5ACeSKGt37. The invite on the new page was wrong. agent-card.json advertised /img/gofr-logo.png, which 404s. So does the pre-existing Organization.logo at page.jsx:98, which points at a .svg that has never existed and which Google fetches for the knowledge panel. The real logo was only in src/images, where Next bundles it under a hashed path. Copied it to public/img/ and pointed both at it. sitemap.js listed /index.md, the markdown twin of /, contradicting this PR's own rule that twins stay out of the sitemap because they duplicate a canonical HTML URL. /auth.md and /openapi.json have no HTML equivalent and stay. generate-openapi.mjs emitted `enum: []` when the twin list was absent (a build without the docs overlay). An empty enum matches nothing, making getPageMarkdown uncallable. Omit the enum instead. Also documented that slashes in the page parameter are literal, not percent-encoded, so a generator does not produce %2F and a 404. absolutiseLinks rewrote markdown links inside fenced code blocks. renderMarkdocTags is fence-aware for exactly this reason; this one was not, so a code sample containing `](/path)` would have been silently edited. No current doc triggers it. Fixed before one does. EXCLUDED_ROUTE_PATTERNS was exported but only read by isExcludedRoute.
…, orphan pages Three fabricated URLs. ard.json and agent-skills/index.json carried $schema values I wrote from memory: the ARD one 404s and agentskills.dev does not resolve at all. A $schema is a machine-resolvable pointer, so a dead one is worse than none. Dropped both. This is the same class of mistake the PR claims to avoid, and I only caught it by probing the URLs rather than trusting my own file. auth.md told agents that only GET, HEAD and OPTIONS are served and that anything else returns 405. Verified against static-server: POST, PUT, PATCH and DELETE all return 200 with the page body. Nothing mutates, so it is not a hole, but it was a false statement in the one document whose job is describing the access model truthfully. Replaced with what the server actually does. /privacy and /contact were linked from nowhere. Both were in sitemap.xml and llms.txt, but no page on the site pointed at either, so a visitor could not reach the privacy notice at all. Since every page loads Google Tag Manager, an unreachable notice does not disclose anything. Added both to the footer Project column, which renders site-wide. generate-md-twins.mjs could silently overwrite a hand-authored public file. Twins are gitignored, so adding src/app/auth/page.md would have destroyed public/auth.md without showing up in git status. Now refuses loudly. Also normalises CRLF so a Windows-authored page.md does not leave a carriage return on every line of its twin.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
gofr.dev scores 53/100 (grade C) on orank's agent-readiness scan. Agents arriving from web search get a React shell of navigation chrome, and there is no way for them to discover what the site offers or how to read it.
This adds the machine surface. A companion PR in
gofr-dev/gofrcarries the half that lives there — it must land first, because it bumpszopdev/static-serverto v1.0.0, which is what actually serves any of this.What changed
Markdown twins. A
.mdtwin for every content route, generated at build time from the samepage.mdthe HTML renders from. Markdoc tags are rendered down to plain Markdown — fence-aware, so code samples containing{%survive — internal links are made absolute, and each twin opens with a top-level heading. Discoverable via<link rel="alternate">in the page head, and served onAccept: text/markdownonce static-server is bumped.Scoped indexes.
/docs/llms.txtand/docs/{section}/llms.txt, so an agent working on one area can skip the 660 KB whole-site dump./openapi.json. Describes the read surface that actually exists — the indexes, the twins, the sitemap, the changelog feed. Thepageparameter is enumerated from the generated twin list, so an agent doing function-calling cannot ask for a path that 404s. Validates clean underredocly lint.Access model.
/auth.mdand/.well-known/oauth-protected-resourcestate the real one: anonymous, no registration, no token. Every URI they advertise resolves (verified below). No fabricated endpoints.Agent discovery.
/.well-known/{ard.json, agent-card.json, agent-skills/index.json, mcp/server-card.json, api-catalog},/agent/identity, and a Markdown homepage at/index.mdfor cold arrivals.NLWeb Schema Feeds.
/schemamap.xml+/feeds/docs.jsonl, referenced fromrobots.txt.public/_headers. Site-wideLink,nosniff,Referrer-Policy, and the media types the extensionless.well-knowndocuments need. This replaces what would otherwise have been nginx config — see "One correction" below.Two pages that are overdue independently of any scan
/privacy—src/app/layout.jsx:114loads Google Tag Manager (GTM-5G6KD5VJ). Every visitor, including EU visitors, is tracked with no disclosure and no policy page anywhere on the site. That is a GDPR exposure, not a scoring gap./contact— routes bug reports, security disclosures, and questions to the channels that already exist. It is also what makes the newOrganization.contactPointJSON-LD truthful.No
/about: orank already verified one (it matched/why-gofr), and a second would compete with it in search.Structured data
OrganizationgainscontactPoint; aWebSitenode is added. Two things are deliberately omitted:address— GoFr has no public postal address, and inventing aPostalAddressto satisfy a validator would be worse than leaving it out.potentialAction: SearchAction— site search is a client-side FlexSearch index with no/search?q=route, so it would point at a URL that does not resolve.FAQPagewas not added to the home page, despite it being worth points: the home page has no visible FAQ, and marking up invisible content violates Google's structured-data policy and risks a manual action.One correction worth flagging
The obvious plan was to put content negotiation in
nginix.conf. That file never reaches a user — it only backs the source-bundle image, which prod uses as a--target builderbase and discards. Production iszopdev/static-server, a Go binary built with GoFr. Hence_headersrather than nginx, and hence the dependency on the companion PR.Verification
Built with the docs overlay from
gofr-dev/gofr(a standalone build of this repo has empty doc routes and produces 2 twins, which it does gracefully — no crash), then served withzopdev/static-server:v1.0.0:Twins and generated indexes are gitignored: they derive from docs that
gofr-dev/gofroverlays at build time, so a copy committed here would be permanently stale.What this does NOT close
gofrCLI to npm/Homebrew./ask, JSON error responses, sandbox (~13 pts)?mode=agent(3 pts)Vary: User-Agentis a cache-fragmentation anti-pattern). Would need an upstream PR.WWW-Authenticate: Bearer401 hint (1 pt)/auth.md.Realistic outcome once both PRs land and a
v*tag deploys: ~72–80/100, not the ~90 a naive reading of the gap list suggests.