feat: serve each page's markdown at its own URL plus .md - #640
Merged
Merged
Conversation
…dpoint An MCP client configured with the legacy SSE URL (/api/sse) retries in a loop, and each cycle failed in production: - `GET /.well-known/oauth-protected-resource/api/sse` fell through to the `[...slug]` catch-all, rendered at runtime where `MDX` is unset (it is only a build env on Vercel), and threw a 500. `/sitemap.xml` and any mistyped URL did the same. - `GET /api/sse` made mcp-handler reach for Redis, which we do not run: it threw `redisUrl is required` as an unhandled rejection and never ended the response, so the function hung until Vercel timed it out. `dynamicParams = false` on the catch-all makes every path that `generateStaticParams` did not list a plain 404, which static export already assumes. `disableSse: true` makes the SSE endpoints a plain 404; clients use the streamable HTTP transport at /api/mcp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The markdown route had the same flaw as the page catch-all: a path that `generateStaticParams` did not list was handled at runtime, where `MDX` is unset on Vercel, and threw `MDX env var not set` (500). `dynamicParams = false` next to `force-static` makes it a plain 404; the listed pages are still served as `text/markdown`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The markdown of a page moves from `/md/<path>.md` to `/<path>.md`, e.g. `/getting-started/introduction.md`. The route handler stays at `src/app/md/[...slug]/route.ts`, since it cannot share a segment with `[...slug]/page.tsx`, but `/md` is no longer a public URL: - Server: an afterFiles rewrite maps `/:path+.md` to `/md/:path+.md`; it is checked before dynamic routes, so the catch-all never sees these paths. The old `/md/:path+.md` URLs permanently redirect to the new ones; redirects only apply to the incoming request, so this does not loop with the rewrite. - Static export: no rewrites there, so `next-build.sh` and the CLI's website build move the exported `md/` files beside the pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # .changeset/unknown-path-404.md
Merged
This branch was successfully deployed
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.
Stacked on #639.
The markdown of a page moves from
/md/<path>.mdto the page URL plus.md, e.g./getting-started/introduction.md. The "Copy Page" and "View as Markdown" actions use the new URL.Why
/mdexistedThe markdown is served by a route handler, and a route handler cannot share a segment with
[...slug]/page.tsx(nor sit in a sibling catch-all with another slug name). #615 therefore gave it its own/mdprefix. The handler stays where it is,src/app/md/[...slug]/route.ts(withdynamicParams = falsefrom #639), but/mdis now an internal path, exposed differently in each mode:Server mode (Vercel,
next start)next.config.mjsmaps/:path+.mdto/md/:path+.md. A default (afterFiles) rewrite is checked after public files and static pages but before dynamic routes, so the[...slug]catch-all never sees these paths.basePathis prefixed onto both sides by Next.:path+rather than:path*, so/.mdis not rewritten to/md.md.output: 'export'Next would warn about it./md/:path+.mdto/:path+.md. Redirects only apply to the incoming request, never to a rewrite's destination, so it cannot loop with the rewrite (checked: one 308, then 200)./<path>.mdis a 404, withMDXunset at runtime: the rewritten path hitsdynamicParams = false.Static export (
OUTPUT=export, CLI--format website, GitHub Pages)There are no rewrites in a static export, so the files are moved instead: the export writes
md/<path>.md, and both export entry points move them beside the pages, at<path>.md, leaving nomd/behind:next-build.sh(pnpm run build, used bystart.shand Playwright), in${DIST_DIR:-out};src/cli/website.ts(npx @pmndrs/docs build ... --format website, used by the reusablebuild.yml), when copying the export to the out dir.The base path is not part of the exported file layout, so it needs no handling there. Old
/md/<path>.mdURLs are a 404 on a static export.Verification (local)
pnpm run lint,pnpm exec tsc --noEmit,pnpm exec vitest run(390 tests): pass.next startwithMDXunset:/getting-started/introduction.md: 200text/markdown; charset=utf-8, the page's markdown/getting-started/nope.md,/nope.md,/md.md: 404/md/getting-started/introduction.md: 308 to/getting-started/introduction.md, then 200text/markdown/getting-started/introduction: 200, its HTML carries/getting-started/introduction.mdPOST /api/mcptools/list: 200next-build.sh: 20.mdfiles beside the pages, noout/md; same withDIST_DIR=build-distdir BASE_PATH=/sub(links read/sub/getting-started/introduction.md). The CLI's--format websitewithBASE_PATH=/sub: same. Served withserve:/getting-started/introduction.md200text/markdown,/md/...404.npx playwright test --update-snapshotsagainststart.sh: 74 passed.🤖 Generated with Claude Code