Skip to content

WordPress: stop /llms.txt bouncing to /llms.txt/, and prove it in a real install - #6

Merged
zordhalo merged 1 commit into
mainfrom
fix/wp-canonical-redirect
Sep 24, 2026
Merged

zordhalo merged 1 commit into
mainfrom
fix/wp-canonical-redirect

Conversation

@zordhalo

Copy link
Copy Markdown
Contributor

Running the plugin in an actual WordPress found a bug that linting and the stub test could not.

The bug

With a trailing-slash permalink structure, redirect_canonical 301s /llms.txt → /llms.txt/.

llms.txt is a file path by convention. Every tool asks for the unslashed one — including this project's own component. Most clients follow the 301, so it worked by luck, which is exactly the kind of thing that breaks against a fetcher that doesn't.

redirect_canonical is now filtered off for our query var only.

An integration suite, because that's what caught it

wordpress/test/integration.mjs boots WordPress Playground with the plugin mounted and asserts the eleven things that only break in a real install:

routing
  ok  /llms.txt is served directly, with no canonical redirect
  ok  served as plain text
  ok  nosniff is set
content
  ok  built from the site's own pages
  ok  password-protected pages are excluded
  ok  carries the owner's About
front end
  ok  declares the discovery link
  ok  mounts the element
  ok  enqueues the :defined guard
  ok  pins the script with an integrity hash
  ok  loads a version-pinned url

Not in CI: it downloads WordPress and takes minutes, a poor trade against a fast suite. Run before a release.

Two things that suite had to learn the hard way

It seeds through a mounted mu-plugin, not a blueprint runPHP step. That step was silently skipped on some boots — and a skipped seed is worse than a failed one, because the suite then tests an unseeded site and reports whatever that produces. A cached Playground directory made exactly that look like a pass.

The mu-plugin guards on function_exists. mu-plugins load before regular plugins, so on the activation request the plugin's own functions don't exist yet, and calling one is a fatal that Playground reports as a blueprint step failure rather than anything resembling the cause.

Listing assets

assets/ carries the banner (1544×500 + 772×250), icon (256 + 128) and three screenshots. Banner and icon are rendered from HTML at 2x and downsampled so each pair cannot drift. Screenshots are from a real WordPress, not mocked.

🤖 Generated with Claude Code

…eal install

Running the plugin in an actual WordPress found a bug that linting and the stub
test could not: with a trailing-slash permalink structure, redirect_canonical 301s
/llms.txt to /llms.txt/. llms.txt is a FILE path by convention. Every tool asks
for the unslashed one, including this project's own component. Most clients follow
the 301, so it worked by luck, which is the kind of thing that breaks against a
fetcher that does not.

redirect_canonical is now filtered off for our query var only.

wordpress/test/integration.mjs boots WordPress Playground with the plugin mounted
and asserts the eleven things that only break in a real install: no canonical
redirect, plain text with nosniff, built from the site's own pages, password
protected pages excluded, and a front end that declares the link, mounts the
element, enqueues the :defined guard and pins the script with an integrity hash.
Not in CI: it downloads WordPress and takes minutes, a poor trade against a fast
suite. Run before a release.

Two things that suite had to learn the hard way:

- It seeds through a MOUNTED MU-PLUGIN, not a blueprint runPHP step. That step was
  silently skipped on some boots, and a skipped seed is worse than a failed one:
  the suite then tests an unseeded site and reports whatever that produces. A
  cached Playground directory made exactly that look like a pass.
- The mu-plugin guards on function_exists. mu-plugins load BEFORE regular plugins,
  so on the activation request the plugin's own functions do not exist yet, and
  calling one is a fatal that Playground reports as a blueprint step failure
  rather than as anything resembling the cause.

assets/ carries the listing artwork: banner and icon rendered from HTML at 2x and
downsampled so the pairs cannot drift, and three screenshots taken against a real
WordPress rather than mocked.
@zordhalo
zordhalo merged commit f0aef29 into main Sep 24, 2026
2 checks passed
@zordhalo
zordhalo deleted the fix/wp-canonical-redirect branch September 24, 2026 19:53
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