feat(layer): add Vercel markdown-rewrite module#32
Merged
Conversation
Adds a Nuxt module that injects Vercel build-output rewrite rules so
that AI agents (anything sending `Accept: text/markdown` or
`User-Agent: curl/*`) get served raw markdown instead of the SPA shell.
Behaviour:
- No-op on every non-Vercel preset (matches `vercel`, `vercel-edge`,
`vercel-static`, etc. via `preset.startsWith('vercel')`).
- On Vercel: read `<output.publicDir>/../config.json`, validate that
`llms.txt` was emitted, and unshift route pairs onto `routes` so
they fire before the SPA fallback.
- Rules:
- `/` -> `/llms.txt`
- `/<locale>` -> `/llms.txt` (per `runtimeConfig.public.i18n.locales`)
- `/<page>` -> `/raw<page>.md` for every `/raw/...md` link found
in `llms.txt`
- Vercel's `has` array is AND-ed, so OR semantics between the
`Accept` and `User-Agent` matchers require emitting two rules per
`src` -> `dest` pair.
- Locale codes are regex-escaped before being joined into the
alternation so exotic codes can't break the pattern.
Ports upstream docus commits `6fd8686b` and `9ceafe6f` -- see
`docs/docus-upstream-changes.md` item #9.
Verification:
- `bun lint` -> clean
- `bun typecheck` -> no new errors in `markdown-rewrite.ts`
- `NITRO_PRESET=vercel bun --filter @pleaseai/docs-site build`
injects two routes (homepage / Accept + User-Agent) into
`.vercel/output/config.json`.
- Default (cloudflare) build is unchanged; module bails silently.
There was a problem hiding this comment.
Code Review
This pull request introduces a new Nuxt module, markdown-rewrite.ts, which automatically injects Vercel rewrite routes to serve raw markdown files (such as llms.txt and raw documentation pages) to AI agents requesting markdown content via specific headers or user agents. The feedback suggests improving the robustness of these routes by decoding URL-encoded characters in the paths using decodeURIComponent and allowing optional trailing slashes in the regex patterns for docs pages.
There was a problem hiding this comment.
2 issues found across 2 files
Architecture diagram
sequenceDiagram
participant Nuxt as Nuxt Build Process
participant MDModule as markdown-rewrite Module
participant FS as File System
participant Vercel as Vercel Build Output Config
Note over Nuxt,Vercel: Build-time Module Flow (Vercel Preset Only)
Nuxt->>MDModule: nitro:init hook
MDModule->>MDModule: Check preset startsWith('vercel')
alt Non-Vercel preset
MDModule-->>Nuxt: No-op (return early)
else Vercel preset
MDModule->>FS: Read <publicDir>/../config.json
alt Read fails
FS-->>MDModule: Error
MDModule->>MDModule: log.warn (skip rewrites)
else Parse fails
MDModule->>MDModule: log.warn (skip rewrites)
else Success
FS-->>MDModule: raw JSON
MDModule->>FS: Read <publicDir>/llms.txt
alt llms.txt missing
FS-->>MDModule: Error
MDModule->>MDModule: log.warn (skip rewrites)
else llms.txt found
FS-->>MDModule: llms.txt content
MDModule->>MDModule: Build route definitions
Note over MDModule: For each src→dest pair,<br/>create 2 routes (Accept AND curl)
MDModule->>MDModule: Rule 1: ^/$ → /llms.txt
MDModule->>MDModule: Rule 2: ^/(locale)/?$ → /llms.txt (per locale)
MDModule->>MDModule: Rule 3: ^<page>$ → /raw<page>.md (from llms.txt links)
MDModule->>MDModule: Escape regex in locale codes & page paths
MDModule->>Vercel: unshift routes into vcConfig.routes
alt Success
Vercel-->>MDModule: Updated config
MDModule->>FS: Write config.json
FS-->>MDModule: Done
MDModule->>MDModule: log.info (injected N routes)
end
end
end
end
MDModule-->>Nuxt: Module setup complete
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Apply review feedback from cubic-dev-ai and gemini-code-assist on the markdown-rewrite module: - Read locale config from `nuxt.options.i18n` instead of `runtimeConfig.public.i18n` (matching the pattern used by the `nitro:config` hook in nuxt.config.ts). The i18n module does not always populate `runtimeConfig.public.i18n`, so the previous source could silently miss locale routes. (cubic) - Decode the URL pathname with `decodeURIComponent` so that paths with URL-encoded characters (e.g. `%20`) match Vercel's router, which compares against decoded request pathnames. (gemini) - Allow an optional trailing slash on docs page route patterns (`/?$` instead of `$`) so requests like `/en/getting-started/installation/` are matched consistently with the per-locale homepage routes. (cubic, gemini)
Resolve modules-array conflict in packages/layer/nuxt.config.ts (#33 landed @nuxtjs/mcp-toolkit; keep ./modules/markdown-rewrite from this branch alongside it).
|
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.


Summary
Adds
packages/layer/modules/markdown-rewrite.ts— a build-time Nuxt module that injects rewrite rules into Vercel's build-outputconfig.jsonso AI agents get raw markdown instead of the SPA shell.Ports docus upstream commits:
6fd8686b—feat(llms): redirect homepage to /llms.txt9ceafe6f—feat(llms): add docs page redirection to raw markdown for agentsSee
docs/docus-upstream-changes.mditem #9.Behaviour
preset.startsWith('vercel'), so it coversvercel,vercel-edge,vercel-static, etc.<output.publicDir>/../config.json(the Vercel build-output config), confirmllms.txtwas emitted, thenunshiftroute pairs ontoroutesso they fire before the SPA fallback.Accept: text/markdownorUser-Agent: curl/*:^/$→/llms.txt^/<locale>/?$→/llms.txt(one perruntimeConfig.public.i18n.localesentry)^<page>$→/raw<page>.md(one per/raw/...mdlink discovered inllms.txt)hasarray is AND-ed, so OR semantics between theAcceptandUser-Agentmatchers require emitting two rule entries persrc → destpair.Conventions
packages/layer/modules/{config,shadcn}.ts(defineNuxtModule+ named module).VercelBuildOutputConfig/VercelRoute/VercelHeaderHasinterface describes the parts we touch — noany.packages/layer/nuxt.config.tsimmediately after./modules/shadcn.Verification
Output (homepage rules only, because the current
nuxt-llmsconfig emits links to canonical/docs/...URLs rather than/raw/...md— the per-page rules will start being emitted as soon asllms.txtcarries raw-md links):{ "src": "^/$", "dest": "/llms.txt", "headers": { "content-type": "text/markdown; charset=utf-8" }, "has": [{ "type": "header", "key": "accept", "value": "(.*)text/markdown(.*)" }], "continue": true } { "src": "^/$", "dest": "/llms.txt", "headers": { "content-type": "text/markdown; charset=utf-8" }, "has": [{ "type": "header", "key": "user-agent", "value": "curl/.*" }], "continue": true }Default (cloudflare) build is unaffected — the module bails silently and
dist/is produced as before.Notes
llms.txtto enumerate/raw/...mdURLs. The current site config doesn't, so only the homepage routes are emitted today. This matches the upstream behaviour and avoids accidentally rewriting asset URLs. Wiringnuxt-llmsto also emit raw-md URLs is tracked separately.config.json.Follow-up to #27.