Skip to content

feat(agents): publish a machine-readable surface for AI agents - #227

Open
naveen-bandela-au2 wants to merge 3 commits into
mainfrom
feat/agent-readiness
Open

feat(agents): publish a machine-readable surface for AI agents#227
naveen-bandela-au2 wants to merge 3 commits into
mainfrom
feat/agent-readiness

Conversation

@naveen-bandela-au2

Copy link
Copy Markdown
Collaborator

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/gofr carries the half that lives there — it must land first, because it bumps zopdev/static-server to v1.0.0, which is what actually serves any of this.

What changed

Markdown twins. A .md twin for every content route, generated at build time from the same page.md the 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 on Accept: text/markdown once static-server is bumped.

Scoped indexes. /docs/llms.txt and /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. 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.

Access model. /auth.md and /.well-known/oauth-protected-resource state 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.md for cold arrivals.

NLWeb Schema Feeds. /schemamap.xml + /feeds/docs.jsonl, referenced from robots.txt.

public/_headers. Site-wide Link, nosniff, Referrer-Policy, and the media types the extensionless .well-known documents need. This replaces what would otherwise have been nginx config — see "One correction" below.

Two pages that are overdue independently of any scan

  • /privacysrc/app/layout.jsx:114 loads 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 new Organization.contactPoint JSON-LD truthful.

No /about: orank already verified one (it matched /why-gofr), and a second would compete with it in search.

Structured data

Organization gains contactPoint; a WebSite node is added. Two things are deliberately omitted:

  • address — GoFr has no public postal address, and inventing a PostalAddress to 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.

FAQPage was 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 builder base and discards. Production is zopdev/static-server, a Go binary built with GoFr. Hence _headers rather 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 with zopdev/static-server:v1.0.0:

106 twins, 6 scoped llms.txt, 106 schema feed entries, openapi lint clean

PASS  Accept: text/markdown → text/markdown + Vary: Accept
PASS  body starts with '# '
PASS  browser Accept: text/html still gets HTML
PASS  direct .md → text/markdown
PASS  /no-such-path → real 404; markdown 404 body pointing at /sitemap.xml
PASS  Link: sitemap / service-desc / api-catalog
PASS  /.well-known/api-catalog → application/linkset+json
PASS  /agent/identity → application/json
PASS  all 24 URIs advertised in auth.md + PRM + ard.json resolve (GET 200, OPTIONS 200)
PASS  all 106 enumerated openapi pages exist
PASS  twins absent from sitemap.xml; /privacy and /contact present

Twins and generated indexes are gitignored: they derive from docs that gofr-dev/gofr overlays at build time, so a copy committed here would be permanently stale.

What this does NOT close

Gap Why
Wikipedia / Wikidata entity (4 pts) Needs third-party press for notability. Not a code change.
ChatGPT app directory (2 pts) External submission.
npm / PyPI SDKs (4 pts) GoFr is a Go framework; an npm "SDK" would be a fake package. The honest version is publishing the gofr CLI to npm/Homebrew.
Hosted MCP, NLWeb /ask, JSON error responses, sandbox (~13 pts) All need a running service; the site is a static export.
Bot-UA → markdown, ?mode=agent (3 pts) static-server matches on path only and deliberately refuses UA switching (Vary: User-Agent is a cache-fragmentation anti-pattern). Would need an upstream PR.
WWW-Authenticate: Bearer 401 hint (1 pt) Would mean faking a 401 on a genuinely public path, contradicting /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.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant