Skip to content

fix: stop encoded probes from poisoning the robots.txt cache - #15

Merged
josemontesdeoca merged 3 commits into
mainfrom
op-28-robots-txt-500
Sep 18, 2026
Merged

josemontesdeoca merged 3 commits into
mainfrom
op-28-robots-txt-500

Conversation

@josemontesdeoca

Copy link
Copy Markdown
Member

Why the change

docs.prose.md/robots.txt returns 500 because a request for /robots%2Etxt overwrote its cache entry, and a 5xx on robots.txt stops Google from crawling the site; this blocks that write and checks robots.txt over HTTP in CI and after every deploy.

Special things to note

  • Both guards are needed. With dynamicParams = false alone, /robots.txt stayed healthy, but the encoded probe itself still returned 500.
  • Any percent-encoded path now gets an empty 404 from the proxy, not the styled not-found page. No docs slug or static asset uses encoded characters.
  • Production recovers when this deploys: the bad cache files live in the running container, not in the image.

Change outline

The root catch-all page decodes its slug before using it as a cache key, and route handlers share that cache:

 GET /robots%2Etxt
   proxy.ts
+    encoded pathname → 404, request ends here
   [[...slug]] page: decodes slug, cache key becomes "/robots.txt"
-    stores its 404 page under "/robots.txt"

 GET /robots.txt
-  reads a page entry → "app-route received invalid cache entry APP_PAGE" → 500
+  reads its own entry → 200 text/plain

 GET /any-unknown-slug
-  renders and writes the 404 page to disk, never evicted
+  dynamicParams = false → 404, nothing rendered or written
 app/[[...slug]]/page.tsx            ~ dynamicParams = false
 proxy.ts                            + hasEncodedPathname() guard, checked first
 __tests__/proxy.test.ts             + encoded and plain path cases
+scripts/smoke-standalone.ts         starts the standalone server, sends the probe, checks
+                                      robots.txt, sitemap, pages, and the cache directory on disk
+scripts/check-live-robots.sh        checks a live robots.txt with retries
 .github/workflows/verify.yml        builds public and preview mode, smoke-tests each
 .github/workflows/deploy-docs.yml   + runs check-live-robots.sh after flyctl deploy

A request for /robots%2Etxt missed the static robots route, fell through
to the docs catch-all, and had its slug decoded before the page was
cached. The resulting 404 render was written under the /robots.txt key,
so every later robots.txt request failed with "app-route received
invalid cache entry APP_PAGE" and returned 500 to Google.

Two guards close this. The catch-all now sets dynamicParams = false, so
unknown slugs 404 without rendering or writing anything to disk, which
also ends the unbounded growth of 404 pages in the server cache. The
proxy answers any percent-encoded path with a 404 before routing,
because on its own the segment config still left encoded aliases of
route handlers returning 500.

A new smoke:standalone script boots the standalone server, replays the
probe cold and warm, and checks robots.txt, sitemap, docs pages,
Markdown negotiation and the cache directory over HTTP and on disk.
Verify built preview mode once and never made a request, so a broken
robots route in the public build that production ships could pass CI
behind a healthy build step. The job now builds public mode and runs
the standalone smoke check against it, then does the same for preview
mode. That checks both what crawlers may do and what they may not on
every pull request. The timeout grows to 20 minutes to cover the
second build.
The Fly health check only probes /, so the deploy stayed green while
robots.txt returned 500 and Google stopped crawling the site. A new
check-live-robots.sh fetches /robots.txt as Googlebot, retrying while a
stopped machine starts, and fails unless it gets a 200 text/plain
response that allows Googlebot and advertises the canonical sitemap.
The deploy workflow runs it against docs.prose.md after flyctl deploy,
so a broken robots route turns the deploy run red instead of going
unnoticed. Run against production before this fix, it fails with 500
on every attempt.
@josemontesdeoca josemontesdeoca self-assigned this Sep 18, 2026
@josemontesdeoca
josemontesdeoca merged commit 0c3a71f into main Sep 18, 2026
4 checks passed
@josemontesdeoca
josemontesdeoca deleted the op-28-robots-txt-500 branch September 18, 2026 01:59
josemontesdeoca added a commit that referenced this pull request Sep 18, 2026
Both handlers looked a page up with the last slug segment dropped, so
any file name under a real page (/og/setup/anything.png,
/llms.mdx/setup/other.md) rendered that page and saved a copy under
the requested name. Next never deletes those files, so scanner traffic
could grow the disk without limit.

Every image and markdown file is generated at build time, so both
routes now export dynamicParams = false, the guard the page catch-all
got in #15. Anything the build did not generate, including bare
/llms.mdx, now 404s before the handler runs and nothing is written.

The standalone smoke run now fails if a request adds a file to the
cache directory, or if any dynamic route in the prerender manifest
can render params on demand, so a new route cannot reopen this.
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