Skip to content

Commit 4072f1b

Browse files
sunibclaude
andauthored
Publish a real robots.txt, and welcome crawlers explicitly (#27)
* feat: Publish a real robots.txt, and welcome crawlers explicitly layouts/robots.txt has been in this repo for ages without ever being rendered: Hugo only emits it when enableRobotsTXT is set, and it was not. So the origin published nothing, and Cloudflare filled the gap with its own file — the Content Signals legal preamble and no directives at all. No User-agent line, no Allow, no actual signal. An agent-readiness scan called it invalid, and it was. Now Hugo emits the file, and it says what we actually mean: every crawler allowed, search=yes, ai-input=yes, ai-train=yes. The named AI crawlers repeat the same permissive group, because a crawler that matches its own group stops reading the wildcard one. Alongside that: llms.txt as a custom output format that ranges over the site's own pages and posts, so it follows the content instead of being maintained by hand, an ARD capability manifest, and discovery links in the head. The sitemap Hugo already generated was the one thing the scan did not complain about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: Record the open agent-readiness work The remaining items all need response headers or DNS, which GitHub Pages does not give us. Worth writing down: this repo already has an nginx config and a Dockerfile, but only for PR previews — the preview stack can set headers and production cannot. Also records why the API-shaped checks are deliberately absent, and one pre-existing quirk found while working here: the non-production guard in layouts/robots.txt cannot fire, because params.env is hardcoded to production, so previews serve the permissive rules too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 3269e0c commit 4072f1b

6 files changed

Lines changed: 390 additions & 5 deletions

File tree

‎AGENT-READINESS.md‎

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Agent readiness: what is done, what is open
2+
3+
This site should be easy for crawlers, language models, and agents to read. It is a
4+
public personal site; the whole point is to be found and quoted. So the default answer
5+
to "may I read this?" is yes, and it should be stated out loud rather than left to be
6+
inferred.
7+
8+
This file records what is in place, what is still open, and why the open items are
9+
blocked on hosting decisions rather than on writing code.
10+
11+
## Background: two problems, stacked
12+
13+
An agent-readiness scan reported `robots.txt exists but appears invalid (no User-agent
14+
directive)`. That turned out to be two separate faults sitting on top of each other.
15+
16+
**1. Hugo was never publishing a robots.txt.** `layouts/robots.txt` has been in this
17+
repo for a long time, but Hugo only renders it when `enableRobotsTXT` is set, and it
18+
was not. The template was dead code. The `gh-pages` branch contained no `robots.txt`
19+
at all — confirmed with `git ls-tree origin/gh-pages`.
20+
21+
**2. Cloudflare answered in our place.** With nothing at the origin, the edge served
22+
its own file: the Content Signals legal preamble and nothing else. Twenty-odd lines of
23+
comments explaining what content signals mean, followed by no `User-agent` line, no
24+
`Allow`, no `Disallow`, and no actual signal. Not a valid robots.txt, and not something
25+
anyone here wrote. Meanwhile Cloudflare's managed bot preferences were blocking AI
26+
crawlers by a default nobody chose.
27+
28+
Both are now fixed: `enableRobotsTXT: true` in `config.yml`, and Bot Preference Sync
29+
disabled at the edge so the origin's file is the one that gets served.
30+
31+
The lesson worth keeping: an empty origin is not a neutral origin. Something else will
32+
fill the gap and speak on your behalf, and what it says may not be what you would have
33+
said — or even be valid.
34+
35+
## Done (in this repo)
36+
37+
| What | Where |
38+
| --- | --- |
39+
| `enableRobotsTXT`, so Hugo actually emits the file | `config.yml` |
40+
| `robots.txt` — everyone allowed, Content Signals `search=yes, ai-input=yes, ai-train=yes` | `layouts/robots.txt` |
41+
| Named AI crawler groups repeating the same permissive rules | `layouts/robots.txt` |
42+
| `llms.txt`, generated from the site's own pages and posts | `config.yml` (`outputFormats.LLMS`) + `layouts/_default/index.llms.txt` |
43+
| ARD capability manifest | `static/.well-known/ai-catalog.json` |
44+
| `<link rel="sitemap">` and `<link rel="alternate">` to `llms.txt` | `layouts/partials/extend_head.html` |
45+
46+
Two things worth knowing about how this is wired:
47+
48+
- A crawler that matches one of the named groups in `robots.txt` ignores the wildcard
49+
group entirely (RFC 9309), which is why the permissive rules are repeated rather
50+
than inherited.
51+
- `llms.txt` ranges over `.Site.RegularPages`, so new posts and pages appear in it
52+
automatically. Nothing to remember on publish. The sitemap was already generated by
53+
Hugo and was the one thing the scan did not complain about.
54+
55+
## Open: blocked on the hosting setup
56+
57+
These need control over HTTP response headers or DNS. Production publishes to
58+
`gh-pages` (`.github/workflows/publish-stuff.yml`), and GitHub Pages serves static
59+
files without letting us set headers.
60+
61+
Note the irony: this repo *already contains* an nginx config and a Dockerfile
62+
(`nginx/default.conf`), but only for per-PR preview environments. The preview stack can
63+
set response headers; production cannot.
64+
65+
### 1. `Link` response headers (RFC 8288)
66+
67+
Agents look for discovery hints in headers, not just in HTML. The homepage should send
68+
something like:
69+
70+
```
71+
Link: </llms.txt>; rel="alternate"; type="text/plain",
72+
</sitemap.xml>; rel="sitemap",
73+
</.well-known/ai-catalog.json>; rel="service-desc"
74+
```
75+
76+
The `<link>` tags in `extend_head.html` are the in-page equivalent and cover agents
77+
that parse HTML, but header-based checks will not see them.
78+
79+
*Blocked by:* no header control on GitHub Pages. Three lines of `add_header` in
80+
`nginx/default.conf` the moment production runs that container.
81+
82+
### 2. Markdown for Agents
83+
84+
Requests sending `Accept: text/markdown` should get markdown back, with HTML staying
85+
the default for browsers. Verified as not happening today.
86+
87+
This site is a particularly silly place to be missing it, because everything in
88+
`content/` *is already markdown*. We render it to HTML at build time and then have no
89+
way to hand back the source. Hugo can emit a markdown output format alongside HTML
90+
without much trouble; what is missing is the content negotiation, which is a server
91+
concern.
92+
93+
*Blocked by:* a Cloudflare feature on this account, where an origin we run could do the
94+
negotiation directly against markdown we already have.
95+
96+
### 3. DNS for AI Discovery (DNS-AID)
97+
98+
SVCB/HTTPS records under `_index._agents.koudijs.dev` pointing at the discovery
99+
documents, with DNSSEC on the zone.
100+
101+
*Blocked by:* nothing expensive — this is DNS and is achievable wherever the zone
102+
lives. Parked because the spec is still an IETF draft
103+
(`draft-mozleywilliams-dnsop-dnsaid`) and it is the lowest-value item here.
104+
105+
### 4. Check what the edge blocks
106+
107+
Independently of `robots.txt`: if any managed AI-bot blocking is still active in
108+
Cloudflare's AI Crawl Control, it overrides our permissive rules at the edge and a
109+
crawler never reaches the origin to read the welcome. Re-verify after any change to the
110+
proxy setup.
111+
112+
## Observation: previews are crawlable
113+
114+
`layouts/robots.txt` has a guard meant to serve `Disallow: /` for non-production
115+
builds. It cannot currently fire: the condition is
116+
`hugo.IsProduction | or (eq .Site.Params.env "production")`, and `params.env` is
117+
hardcoded to `production` in `config.yml`, so the production branch always wins.
118+
Preview builds at `pr<N>.preview.koudijs.dev` therefore serve the permissive rules.
119+
120+
This predates the changes here and the risk is low, since preview URLs are not linked
121+
from anywhere. Left alone deliberately, because the obvious fix — setting
122+
`HUGO_PARAMS_env: development` in `preview-image.yml` — would also switch off
123+
OpenGraph, analytics, and schema.org output in previews, making them a less faithful
124+
copy of production. If it is worth fixing, the cleaner route is to set
125+
`HUGO_ENVIRONMENT: development` for previews and drop the `params.env` half of the
126+
condition, so `hugo.IsProduction` alone decides.
127+
128+
## Not applicable
129+
130+
Several agent-readiness checks assume a site with an API behind it. This is a personal
131+
site and blog, so these are deliberately absent rather than missing:
132+
133+
- `/.well-known/api-catalog` (RFC 9727)
134+
- `/.well-known/openid-configuration`, `/.well-known/oauth-authorization-server`
135+
- `/.well-known/oauth-protected-resource` (RFC 9728)
136+
- `auth.md`
137+
- `/.well-known/mcp/server-card.json`
138+
- `/.well-known/agent-skills/index.json` — no skills to publish
139+
- WebMCP tools
140+
141+
Publishing empty versions of these would satisfy a checker while pointing agents at
142+
nothing, which is worse than not publishing them.
143+
144+
## The direction
145+
146+
The intent is to move the public sites onto our own cluster and stop depending on
147+
GitHub Pages. Cloudflare may stay as a proxy, or go entirely. Either way, once there is
148+
an origin we control:
149+
150+
- `Link` headers become a few lines of nginx config — in a file this repo already has.
151+
- Markdown content negotiation becomes part of the build, using markdown that is
152+
already sitting in `content/`.
153+
- Nothing about discovery is gated behind someone else's pricing tier, or their default
154+
opinion about who may read this.
155+
156+
## How to verify
157+
158+
```sh
159+
curl -sS https://koudijs.dev/robots.txt
160+
curl -sS https://koudijs.dev/llms.txt
161+
curl -sS https://koudijs.dev/sitemap.xml
162+
curl -sS https://koudijs.dev/.well-known/ai-catalog.json | python3 -m json.tool
163+
curl -sS -D - -o /dev/null https://koudijs.dev/ | grep -i '^link:'
164+
curl -sS -D - -o /dev/null -H 'Accept: text/markdown' https://koudijs.dev/ | grep -i '^content-type:'
165+
```
166+
167+
The last two are the open items: today they return nothing and `text/html`.
168+
169+
Locally, `hugo --minify` writes all of the above into `public/`. To check the preview
170+
branch of the robots template: `hugo -e development --minify` with
171+
`HUGO_PARAMS_env=development`.

‎config.yml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
baseURL: https://koudijs.dev/
22
languageCode: en-us
33
title: koudijs.dev
4+
# Renders layouts/robots.txt. Without this Hugo publishes no robots.txt at all,
5+
# which left the edge free to answer for us.
6+
enableRobotsTXT: true
47
pagination:
58
pagerSize: 5
69

@@ -54,11 +57,21 @@ params:
5457
url: https://simonkoudijs.substack.com/
5558

5659

60+
# llms.txt is generated from the site's own pages (see
61+
# layouts/_default/index.llms.txt), so it cannot drift from what is published.
62+
outputFormats:
63+
LLMS:
64+
mediaType: text/plain
65+
baseName: llms
66+
isPlainText: true
67+
notAlternative: true
68+
5769
outputs:
5870
home:
5971
- HTML
6072
- RSS
6173
- JSON
74+
- LLMS
6275

6376
menu:
6477
main:

‎layouts/_default/index.llms.txt‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{{- /* llms.txt — a plain-text summary of this site for language models and agents.
2+
Pages and posts are ranged over, so this file follows the content instead of
3+
being maintained by hand. Spec: https://llmstxt.org/ */ -}}
4+
{{- $trim := "\\s+" -}}
5+
# {{ .Site.Title }}
6+
7+
> {{ .Site.Params.description }}
8+
9+
Simon Koudijs is a consultant, trainer, and speaker based in Amersfoort, Netherlands,
10+
trading as Koudijs IT (KvK 97788708). The work spans software delivery, engineering
11+
workflows and CI/CD, platform engineering, Kubernetes, GitOps, and the practical use of
12+
AI in engineering — with a bias towards what teams can actually apply over what sounds
13+
impressive. He is also the founder of ConfigButler (https://configbutler.ai) and the
14+
author of the Reverse GitOps manifesto (https://reversegitops.dev).
15+
16+
Available for consulting, in-house and public training, and conference talks. To make
17+
contact, book a slot directly: https://calendar.app.google/c2jMcQgREVgbKEj18
18+
19+
## Pages
20+
{{ range $p := (where (where .Site.RegularPages "Section" "") "Layout" "!=" "search") -}}
21+
- [{{ $p.Title }}]({{ $p.Permalink }}): {{ $p.Summary | plainify | replaceRE $trim " " | htmlUnescape | truncate 240 }}
22+
{{ end }}
23+
## Posts
24+
{{ range (where .Site.RegularPages "Section" "posts").ByDate.Reverse -}}
25+
- [{{ .Title }}]({{ .Permalink }}) — {{ .Date.Format "2 January 2006" }}{{ with .Params.tags }}, tagged {{ delimit . ", " }}{{ end }}: {{ .Summary | plainify | replaceRE $trim " " | htmlUnescape | truncate 240 }}
26+
{{ end }}
27+
## Notes for crawlers and agents
28+
29+
- Crawling, indexing, quoting, and training on this content are all permitted. See {{ "robots.txt" | absURL }} for the Content Signals.
30+
- Machine-readable index of this site: {{ "sitemap.xml" | absURL }}
31+
- Full-text search index used by the site's own search page: {{ "index.json" | absURL }}
32+
- RSS feed: {{ "index.xml" | absURL }}
33+
- Capability manifest: {{ "/.well-known/ai-catalog.json" | absURL }}
34+
- There is no API to call on this domain; the site is a personal site and blog. Talk slides are published as code at https://github.com/reverse-gitops/talks

‎layouts/partials/extend_head.html‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
{{- /* Head custom content area start */ -}}
22
{{- /* Insert any custom code (web-analytics, resources, etc.) - it will appear in the <head></head> section of every page. */ -}}
3-
{{- /* Can be overwritten by partial with the same name in the global layouts. */ -}}
3+
4+
{{- /* Discovery for crawlers and agents. The HTTP Link header would be the
5+
stronger signal, but GitHub Pages cannot set response headers — see
6+
AGENT-READINESS.md. */ -}}
7+
<link rel="sitemap" type="application/xml" href="{{ "sitemap.xml" | absURL }}" />
8+
<link rel="alternate" type="text/plain" href="{{ "llms.txt" | absURL }}" title="llms.txt summary of this site" />
9+
410
{{- /* Head custom content area end */ -}}

‎layouts/robots.txt‎

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,48 @@
1+
{{- if hugo.IsProduction | or (eq .Site.Params.env "production") -}}
2+
# koudijs.dev — Simon Koudijs
3+
# Every crawler is welcome here: read the site, index it, quote it, train on it.
4+
# Content Signals: https://contentsignals.org/ (RFC 9309 for the rest)
5+
6+
# As the operator of this site, I consent to the uses below.
7+
# search=yes: building a search index and linking back here.
8+
# ai-input=yes: using this content as input to an AI answer or agent.
9+
# ai-train=yes: using this content to train or fine-tune a model.
10+
11+
User-agent: *
12+
Content-Signal: search=yes, ai-input=yes, ai-train=yes
13+
Allow: /
14+
15+
# --- AI crawlers and agents, called out explicitly ---
16+
# A crawler that matches a group below ignores the wildcard group, so each one
17+
# repeats the same permissive rules.
18+
19+
User-agent: GPTBot
20+
User-agent: OAI-SearchBot
21+
User-agent: ChatGPT-User
22+
User-agent: ClaudeBot
23+
User-agent: Claude-Web
24+
User-agent: Claude-User
25+
User-agent: Claude-SearchBot
26+
User-agent: Google-Extended
27+
User-agent: GoogleOther
28+
User-agent: PerplexityBot
29+
User-agent: Perplexity-User
30+
User-agent: Applebot-Extended
31+
User-agent: meta-externalagent
32+
User-agent: Amazonbot
33+
User-agent: Bytespider
34+
User-agent: CCBot
35+
User-agent: cohere-ai
36+
User-agent: Diffbot
37+
User-agent: DuckAssistBot
38+
User-agent: MistralAI-User
39+
Content-Signal: search=yes, ai-input=yes, ai-train=yes
40+
Allow: /
41+
42+
Sitemap: {{ "sitemap.xml" | absURL }}
43+
{{- else -}}
44+
# Non-production build (PR preview). Keep these out of every index — the
45+
# production site at https://koudijs.dev/ is the one that should be crawled.
146
User-agent: *
2-
{{- if hugo.IsProduction | or (eq .Site.Params.env "production") }}
3-
Disallow:
4-
{{- else }}
547
Disallow: /
648
{{- end }}
7-
Sitemap: {{ "sitemap.xml" | absURL }}

0 commit comments

Comments
 (0)