From 866de416819ff3442e914a4f68a2c36b785e2c56 Mon Sep 17 00:00:00 2001 From: Damien Simonin Feugas Date: Mon, 5 Sep 2022 15:36:09 +0200 Subject: [PATCH] docs: documents middleware matcher (#40180) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### ๐Ÿ“– What's in there? Middleware matchers are powerful, but very few people realized it, because they are not really documented. This PR tries to bring more clarity, and includes a more advanced example. The example shows how to exclude several pages (no `/static`, no `/public`), but also allow specific page in excluded paths (`/public/disclaimer`) ### ๐Ÿงช How to test? Run the example: `pnpm next dev examples/middleware-matcher`, then browse to http://localhost:3000 The first 3 links should not match, the last 3 ones should. Don't forget to clear your localhost cookies if you change the middleware code. ### ๐Ÿ†™ Note to reviewers Using session cookies to pass information from middleware to the rendered page is not great, because `document.cookie` is not available during SSR, and because cookies persist when refreshing the page (making it hard to try different matchers) However, I couldn't find a simpler way to convey the information from the middleware to the page, and I meant to have something visual. The other option is to use response headers and curl commands, but... --- docs/advanced-features/middleware.md | 13 +++++++ examples/middleware-matcher/.gitignore | 36 ++++++++++++++++++ examples/middleware-matcher/README.md | 37 +++++++++++++++++++ examples/middleware-matcher/middleware.js | 15 ++++++++ examples/middleware-matcher/package.json | 13 +++++++ .../middleware-matcher/pages/[...slug].jsx | 29 +++++++++++++++ examples/middleware-matcher/pages/index.jsx | 30 +++++++++++++++ 7 files changed, 173 insertions(+) create mode 100755 examples/middleware-matcher/.gitignore create mode 100755 examples/middleware-matcher/README.md create mode 100644 examples/middleware-matcher/middleware.js create mode 100644 examples/middleware-matcher/package.json create mode 100644 examples/middleware-matcher/pages/[...slug].jsx create mode 100755 examples/middleware-matcher/pages/index.jsx 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 ( + <> +

+ {hasMiddlewareMatched(slug) + ? 'Middleware matched!' + : 'Middleware ignored me'} +

+ {'<-'} back + + ) +} + +export default ContentPage diff --git a/examples/middleware-matcher/pages/index.jsx b/examples/middleware-matcher/pages/index.jsx new file mode 100755 index 0000000000000..e99713f6d504e --- /dev/null +++ b/examples/middleware-matcher/pages/index.jsx @@ -0,0 +1,30 @@ +const Home = () => { + const matching = ['/about', '/about/topic/cats', '/public/disclaimer'] + const notMatching = ['/public', '/public/disclaimer/nested', '/static'] + return ( +
+

Middleware matching

+

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'
+ {' '}]
+ {'}'} +
+ +
+ ) +} + +export default Home