Skip to content

Commit f56b14d

Browse files
jokullclaude
andauthored
fix: matchConfigPattern incorrectly matches :param with literal suffix (#191)
Patterns like `/:slug.md` were falling through to the simple segment matcher, which treated `slug.md` as the entire parameter name and matched any single-segment path (including `/`). This caused rewrites like `{ source: "/:slug.md", destination: "/api/markdown/:slug" }` to rewrite every page request, resulting in 404s. Add `/:[\w-]+\./.test(pattern)` to the condition that triggers the regex-based matcher, which already tokenizes `:slug` + `.` + `md` correctly. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e075adf commit f56b14d

4 files changed

Lines changed: 32 additions & 3 deletions

File tree

packages/vinext/src/config/config-matchers.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,10 +372,14 @@ export function matchConfigPattern(
372372
// followed by a literal suffix (e.g. "/:path*.md"). Without this, the suffix
373373
// pattern falls through to the simple segment matcher which incorrectly treats
374374
// the whole segment (":path*.md") as a named parameter and matches everything.
375+
// The last condition catches simple params with literal suffixes (e.g. "/:slug.md")
376+
// where the param name is followed by a dot — the simple matcher would treat
377+
// "slug.md" as the param name and match any single segment regardless of suffix.
375378
if (
376379
pattern.includes("(") ||
377380
pattern.includes("\\") ||
378-
/:[\w-]+[*+][^/]/.test(pattern)
381+
/:[\w-]+[*+][^/]/.test(pattern) ||
382+
/:[\w-]+\./.test(pattern)
379383
) {
380384
try {
381385
// Param names may contain hyphens (e.g. :auth-method, :sign-in).

packages/vinext/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3335,7 +3335,8 @@ export function matchConfigPattern(
33353335
if (
33363336
pattern.includes("(") ||
33373337
pattern.includes("\\") ||
3338-
/:[\w-]+[*+][^/]/.test(pattern)
3338+
/:[\w-]+[*+][^/]/.test(pattern) ||
3339+
/:[\w-]+\./.test(pattern)
33393340
) {
33403341
try {
33413342
// Extract named params and their constraints from the pattern.

packages/vinext/src/server/app-dev-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -916,7 +916,7 @@ ${generateNormalizePathCode("modern")}
916916
917917
// ── Config pattern matching (redirects, rewrites, headers) ──────────────
918918
function __matchConfigPattern(pathname, pattern) {
919-
if (pattern.includes("(") || pattern.includes("\\\\") || /:[\\w-]+[*+][^/]/.test(pattern)) {
919+
if (pattern.includes("(") || pattern.includes("\\\\") || /:[\\w-]+[*+][^/]/.test(pattern) || /:[\\w-]+\\./.test(pattern)) {
920920
try {
921921
const paramNames = [];
922922
const regexStr = pattern

tests/shims.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2810,6 +2810,30 @@ describe("matchConfigPattern", () => {
28102810
expect(matchConfigPattern("/blog/intro.md", "/docs/:path*.md")).toBeNull();
28112811
});
28122812

2813+
it("matches :param with literal suffix (e.g. /:slug.md)", async () => {
2814+
const { matchConfigPattern } = await import(
2815+
"../packages/vinext/src/index.js"
2816+
);
2817+
// Should match URLs with the .md suffix and extract the param
2818+
expect(matchConfigPattern("/hello-world.md", "/:slug.md")).toEqual({ slug: "hello-world" });
2819+
expect(matchConfigPattern("/my-post.md", "/:slug.md")).toEqual({ slug: "my-post" });
2820+
// Should NOT match URLs without .md suffix
2821+
expect(matchConfigPattern("/", "/:slug.md")).toBeNull();
2822+
expect(matchConfigPattern("/hello-world", "/:slug.md")).toBeNull();
2823+
expect(matchConfigPattern("/hello-world.txt", "/:slug.md")).toBeNull();
2824+
// Should NOT match paths with extra segments
2825+
expect(matchConfigPattern("/blog/hello-world.md", "/:slug.md")).toBeNull();
2826+
});
2827+
2828+
it("matches :param with literal suffix via config-matchers module", async () => {
2829+
const { matchConfigPattern } = await import(
2830+
"../packages/vinext/src/config/config-matchers.js"
2831+
);
2832+
expect(matchConfigPattern("/hello-world.md", "/:slug.md")).toEqual({ slug: "hello-world" });
2833+
expect(matchConfigPattern("/", "/:slug.md")).toBeNull();
2834+
expect(matchConfigPattern("/hello-world", "/:slug.md")).toBeNull();
2835+
});
2836+
28132837
it("still matches plain :path* catch-all (no suffix) correctly", async () => {
28142838
const { matchConfigPattern } = await import(
28152839
"../packages/vinext/src/index.js"

0 commit comments

Comments
 (0)