Skip to content

Commit cd4e5e4

Browse files
authored
docs: Fix suggested middleware matcher regex and add matcher docs (#505)
Fixes #504
1 parent fc051fd commit cd4e5e4

File tree

7 files changed

+1479
-1139
lines changed

7 files changed

+1479
-1139
lines changed

docs/pages/docs/getting-started/app-router-client-components.mdx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,14 @@ export default createMiddleware({
5656
});
5757

5858
export const config = {
59-
// Skip all paths that should not be internationalized. This example skips the
60-
// folders "api", "_next" and all files with an extension (e.g. favicon.ico)
61-
matcher: ['/((?!api|_next|.*\\..*).*)']
59+
// Skip all paths that should not be internationalized. This example skips
60+
// certain folders and all pathnames with a dot (e.g. favicon.ico)
61+
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
6262
};
6363
```
6464

65+
**Note:** If you have pages that contain the character `.` in the pathname (e.g. `/users/jane.doe`), you might want to consider them in your [matcher config](/docs/routing/middleware#matcher-config).
66+
6567
### `app/[locale]/layout.tsx` [#next-intl-client-provider]
6668

6769
Provide the document layout and set up `NextIntlClientProvider`.

docs/pages/docs/getting-started/app-router-server-components.mdx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,14 @@ export default createMiddleware({
113113
});
114114

115115
export const config = {
116-
// Skip all paths that should not be internationalized. This example skips the
117-
// folders "api", "_next" and all files with an extension (e.g. favicon.ico)
118-
matcher: ['/((?!api|_next|.*\\..*).*)']
116+
// Skip all paths that should not be internationalized. This example skips
117+
// certain folders and all pathnames with a dot (e.g. favicon.ico)
118+
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
119119
};
120120
```
121121

122+
**Note:** If you have pages that contain the character `.` in the pathname (e.g. `/users/jane.doe`), you might want to consider them in your [matcher config](/docs/routing/middleware#matcher-config).
123+
122124
### `app/[locale]/layout.tsx`
123125

124126
The `locale` that was matched by the middleware is available via `useLocale` and can be used to configure the document language.

docs/pages/docs/routing/middleware.mdx

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ export default createMiddleware({
1616
});
1717

1818
export const config = {
19-
// Skip all paths that should not be internationalized. This example skips the
20-
// folders "api", "_next" and all files with an extension (e.g. favicon.ico)
21-
matcher: ['/((?!api|_next|.*\\..*).*)']
19+
// Skip all paths that should not be internationalized. This example skips
20+
// certain folders and all pathnames with a dot (e.g. favicon.ico)
21+
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
2222
};
2323
```
2424

@@ -261,6 +261,39 @@ export default createMiddleware({
261261
APIs](/docs/routing/navigation#localized-pathnames) in your components.
262262
</Callout>
263263

264+
### Matcher config
265+
266+
The middleware is intended to only run on pages, not on arbitrary files that you serve independently of the user locale (e.g. `/favicon.ico`).
267+
268+
Because of this, the following config is generally recommended:
269+
270+
```tsx filename="middleware.ts"
271+
export const config = {
272+
// Skip all paths that should not be internationalized. This example skips
273+
// certain folders and all pathnames with a dot (e.g. favicon.ico)
274+
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
275+
};
276+
```
277+
278+
However, this can lead to false negatives if you have pages that contain the character `.` (e.g. `/users/jane.doe`). To make sure these are processed by the middleware, you can add corresponding entries to the matcher config:
279+
280+
```tsx filename="middleware.ts"
281+
export const config = {
282+
// Matcher entries are linked with a logical "or", therefore
283+
// if one of them matches, the middleware will be invoked.
284+
matcher: [
285+
// Match all pathnames without `.`
286+
'/((?!api|_next|_vercel|.*\\..*).*)',
287+
// Match all pathnames within `/users`, optionally with a locale prefix
288+
'/(.+)?/users/(.+)'
289+
]
290+
};
291+
```
292+
293+
{/* Keep this in sync with `packages/next-intl/test/middleware/middleware.test.tsx` */}
294+
295+
Additionally, some third-party providers like [Vercel Analytics](https://vercel.com/analytics) and [umami](https://umami.is/docs/running-on-vercel) typically use internal endpoints that are then rewritten to an external URL (e.g. `/_vercel/insights/view`). Make sure to exclude such requests from your middleware matcher so they aren't accidentally rewritten.
296+
264297
## Composing other middlewares
265298

266299
By calling `createMiddleware`, you'll receive a function of the following type:

examples/example-next-13/src/middleware.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ export default createMiddleware({
77

88
export const config = {
99
// Skip all paths that should not be internationalized
10-
matcher: ['/((?!api|_next|.*\\..*).*)']
10+
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
1111
};

packages/next-intl/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"eslint-config-molindo": "^7.0.0",
8686
"eslint-plugin-deprecation": "^1.4.1",
8787
"next": "13.5.1",
88+
"path-to-regexp": "^6.2.1",
8889
"react": "^18.2.0",
8990
"react-dom": "^18.2.0",
9091
"size-limit": "^8.2.6",

packages/next-intl/test/middleware/middleware.test.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {RequestCookies} from 'next/dist/compiled/@edge-runtime/cookies';
22
import {NextRequest, NextResponse} from 'next/server';
3+
import {pathToRegexp} from 'path-to-regexp';
34
import {it, describe, vi, beforeEach, expect, Mock} from 'vitest';
45
import createIntlMiddleware from '../../src/middleware';
56
import {COOKIE_LOCALE_NAME} from '../../src/shared/constants';
@@ -68,6 +69,59 @@ beforeEach(() => {
6869
vi.clearAllMocks();
6970
});
7071

72+
it('has docs that suggest a reasonable matcher', () => {
73+
const matcherFromDocs = [
74+
// Match all pathnames without `.`
75+
'/((?!api|_next|_vercel|.*\\..*).*)',
76+
// Match all pathnames within `/users`, optionally with a locale prefix
77+
'/(.+)?/users/(.+)'
78+
];
79+
80+
const test = [
81+
['/', true],
82+
['/test', true],
83+
['/de/test', true],
84+
['/something/else', true],
85+
['/encoded%20BV', true],
86+
['/users/jane.doe', true],
87+
['/de/users/jane.doe', true],
88+
['/users/jane.doe/profile', true],
89+
90+
['/favicon.ico', false],
91+
['/icon.ico', false],
92+
['/icon.png', false],
93+
['/icon.jpg', false],
94+
['/icon.jpeg', false],
95+
['/icon.svg', false],
96+
['/apple-icon.png', false],
97+
['/manifest.json', false],
98+
['/manifest.webmanifest', false],
99+
['/opengraph-image.gif', false],
100+
['/twitter-image.png', false],
101+
['/robots.txt', false],
102+
['/sitemap.xml', false],
103+
['/portraits/jane.webp', false],
104+
['/something/dot.', false],
105+
['/.leading-dot', false],
106+
['/api/auth', false],
107+
['/_vercel/insights/script.js', false],
108+
['/_vercel/insights/view', false],
109+
['/test.html', false],
110+
['/_next/static/chunks/main-app-123.js?23', false],
111+
['/test.html?searchParam=2', false],
112+
['/hello/text.txt', false]
113+
] as const;
114+
115+
expect(
116+
test.map(([pathname]) => {
117+
const matches = matcherFromDocs.some((pattern) =>
118+
pathname.match(pathToRegexp(pattern))
119+
);
120+
return pathname + ': ' + matches;
121+
})
122+
).toEqual(test.map(([pathname, expected]) => pathname + ': ' + expected));
123+
});
124+
71125
describe('prefix-based routing', () => {
72126
describe('localePrefix: as-needed', () => {
73127
const middleware = createIntlMiddleware({

0 commit comments

Comments
 (0)