diff --git a/docs/advanced-features/middleware.md b/docs/advanced-features/middleware.md index 8f65f84a22f11..3a53fa2232355 100644 --- a/docs/advanced-features/middleware.md +++ b/docs/advanced-features/middleware.md @@ -90,6 +90,19 @@ export const config = { > **Note:** The `matcher` values need to be constants so they can be statically analyzed at build-time. Dynamic values such as variables will be ignored. +Configured matchers: + +1. MUST start with `/` +1. can include named parameters: `/about/:path` matches `/about/a` and `/about/b` but not `/about/a/c` +1. can have modifiers on named parameters (starting with `:`): `/about/:path*` matches `/about/a/b/c` because `*` is _zero or more_. `?` is _zero or one_ and `+` _one or more_ +1. can use regular expression enclosed in parenthesis: `/about/(.*)` is the same as `/about/:path*` + +Read more details on [path-to-regexp](https://github.com/pillarjs/path-to-regexp#path-to-regexp-1) documentation. + +> **Note:** For backward compatibility, Next.js always considers `/public` as `/public/index`. Therefore, a matcher of `/public/:path` will match. + +> **Note:** It is not possible to exclude middleware from matching static path starting with `_next/`. This allow enforcing security with middleware. + ### Conditional Statements ```typescript diff --git a/examples/middleware-matcher/.gitignore b/examples/middleware-matcher/.gitignore new file mode 100755 index 0000000000000..c87c9b392c020 --- /dev/null +++ b/examples/middleware-matcher/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/middleware-matcher/README.md b/examples/middleware-matcher/README.md new file mode 100755 index 0000000000000..6eed41451fef7 --- /dev/null +++ b/examples/middleware-matcher/README.md @@ -0,0 +1,37 @@ +# Middleware + +This example shows how to configure your [Next.js Middleware](https://nextjs.org/docs/advanced-features/middleware) to only match specific pages. + +The index page ([`pages/index.js`](pages/index.js)) has a list of links to dynamic pages, which will tell whether they were matched or not. + +The Middleware file ([`middleware.js`](middleware.js)) has a special `matcher` configuration key, allowing you to fine-grained control [matched pages](https://nextjs.org/docs/advanced-features/middleware#matcher). + +Please keep in mind that: + +1. Middleware always runs first +1. Middleware always matches `_next` routes on server side +1. matcher must always starts with a '/' + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/middleware-matcher&project-name=middleware-matcher&repository-name=middleware-matcher) + +## How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example: + +```bash +npx create-next-app --example middleware-matcher middleware-matcher-app +``` + +```bash +yarn create next-app --example middleware-matcher middleware-matcher-app +``` + +```bash +pnpm create next-app --example middleware-matcher middleware-matcher-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). diff --git a/examples/middleware-matcher/middleware.js b/examples/middleware-matcher/middleware.js new file mode 100644 index 0000000000000..2d555ddfa7739 --- /dev/null +++ b/examples/middleware-matcher/middleware.js @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server' + +export default function middleware(req) { + const { pathname } = new URL(req.url) + const response = NextResponse.next() + response.headers.set( + 'set-cookie', + `middleware-slug=${pathname.slice(1)}; Path=${pathname}` + ) + return response +} + +export const config = { + matcher: ['/public/disclaimer', '/((?!public|static).*)'], +} diff --git a/examples/middleware-matcher/package.json b/examples/middleware-matcher/package.json new file mode 100644 index 0000000000000..0607e92b35c2e --- /dev/null +++ b/examples/middleware-matcher/package.json @@ -0,0 +1,13 @@ +{ + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "latest", + "react": "latest", + "react-dom": "latest" + } +} diff --git a/examples/middleware-matcher/pages/[...slug].jsx b/examples/middleware-matcher/pages/[...slug].jsx new file mode 100644 index 0000000000000..20b053f7f066a --- /dev/null +++ b/examples/middleware-matcher/pages/[...slug].jsx @@ -0,0 +1,29 @@ +import { useRouter } from 'next/router' + +function hasMiddlewareMatched(slug) { + const values = + (typeof document !== 'undefined' ? document.cookie : '') + .split(';') + .map((pair) => pair.split('=')) + .filter(([key]) => key === 'middleware-slug') + .map(([, value]) => value.trim()) ?? [] + return values.some((value) => value === slug?.join('/')) +} + +export const ContentPage = (props) => { + const { + query: { slug }, // slug is an array of path segments + } = useRouter() + return ( + <> +
The current middleware configuration is:
++ export const config = {'{'} ++
+ {' '}matcher: [
+ {' '}'/public/disclaimer', // match a single, specific page +
+ {' '}'/((?!public|static).*) // match all pages not starting with + 'public' or 'static'
+ {' '}]
+ {'}'} +