diff --git a/docs/content/1.getting-started/5.routing.md b/docs/content/1.getting-started/5.routing.md index 4973eebab4..e59a122036 100644 --- a/docs/content/1.getting-started/5.routing.md +++ b/docs/content/1.getting-started/5.routing.md @@ -117,3 +117,25 @@ definePageMeta({ :: :ReadMore{link="/guide/directory-structure/middleware"} + +## Route Validation + +Nuxt offers route validation via the `validate` property in [`definePageMeta`](/api/utils/define-page-meta) in each page you wish to validate. + +The `validate` property accepts the `route` as an argument. You can return a boolean value to determine whether or not this is a valid route to be rendered with this page. If you return false and another match can't be found, this will mean a 404. You can also directly return an object with `statusCode`/`statusMessage` to respond immediately with an error (other matches will not be checked). + +If you have a more complex use case, then you can use anonymous route middleware instead. + +:StabilityEdge + +```vue [pages/post/[id].vue] + +``` diff --git a/docs/content/3.api/3.utils/define-page-meta.md b/docs/content/3.api/3.utils/define-page-meta.md index ef4982ca76..c8b835f056 100644 --- a/docs/content/3.api/3.utils/define-page-meta.md +++ b/docs/content/3.api/3.utils/define-page-meta.md @@ -23,6 +23,7 @@ title: "definePageMeta" definePageMeta(meta: PageMeta) => void interface PageMeta { + validate?: (route: RouteLocationNormalized) => boolean | Promise | Partial | Promise> redirect?: RouteRecordRedirectOption alias?: string | string[] pageTransition?: boolean | TransitionProps @@ -79,20 +80,14 @@ interface PageMeta { Define anonymous or named middleware directly within `definePageMeta`. Learn more about [route middleware](/docs/directory-structure/middleware). - **`redirect`** + **`validate`** - - **Type**: [`RouteRecordRedirectOption`](https://router.vuejs.org/guide/essentials/redirect-and-alias.html#redirect-and-alias) + - **Type**: `(route: RouteLocationNormalized) => boolean | Promise | Partial | Promise>` - Where to redirect if the route is directly matched. The redirection happens before any navigation guard and triggers a new navigation with the new target location. + Validate whether a given route can validly be rendered with this page. Return true if it is valid, or false if not. If another match can't be found, this will mean a 404. You can also directly return an object with `statusCode`/`statusMessage` to respond immediately with an error (other matches will not be checked). :StabilityEdge - **`alias`** - - - **Type**: `string | string[]` - - Aliases for the record. Allows defining extra paths that will behave like a copy of the record. Allows having paths shorthands like `/users/:id` and `/u/:id`. All `alias` and `path` values must share the same params. - **`[key: string]`** - **Type**: `any` diff --git a/docs/content/7.migration/7.component-options.md b/docs/content/7.migration/7.component-options.md index f6e725690f..15fbedd864 100644 --- a/docs/content/7.migration/7.component-options.md +++ b/docs/content/7.migration/7.component-options.md @@ -112,25 +112,23 @@ See [layout migration](/migration/pages-and-layouts). ## `validate` -There is no longer a validate hook in Nuxt 3. Instead, you can create a custom middleware function, or directly throw an error in the setup function of the page. +The validate hook in Nuxt 3 only accepts a single argument, the `route`. Just as in Nuxt 2, you can return a boolean value. If you return false and another match can't be found, this will mean a 404. You can also directly return an object with `statusCode`/`statusMessage` to respond immediately with an error (other matches will not be checked). + +:StabilityEdge ```diff [pages/users/[id].vue] - ``` diff --git a/packages/nuxt/src/app/plugins/router.ts b/packages/nuxt/src/app/plugins/router.ts index 46ed26c289..abb3ad4f77 100644 --- a/packages/nuxt/src/app/plugins/router.ts +++ b/packages/nuxt/src/app/plugins/router.ts @@ -234,7 +234,8 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => { if (process.server) { if (result === false || result instanceof Error) { const error = result || createError({ - statusMessage: `Route navigation aborted: ${initialURL}` + statusCode: 404, + statusMessage: `Page Not Found: ${initialURL}` }) return callWithNuxt(nuxtApp, showError, [error]) } diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts index 0388c3a032..2975d76e53 100644 --- a/packages/nuxt/src/pages/module.ts +++ b/packages/nuxt/src/pages/module.ts @@ -50,6 +50,11 @@ export default defineNuxtModule({ if (app.mainComponent!.includes('@nuxt/ui-templates')) { app.mainComponent = resolve(runtimeDir, 'app.vue') } + app.middleware.unshift({ + name: 'validate', + path: resolve(runtimeDir, 'validate'), + global: true + }) }) // Prerender all non-dynamic page routes when generating app diff --git a/packages/nuxt/src/pages/runtime/composables.ts b/packages/nuxt/src/pages/runtime/composables.ts index 2b6e0e6730..ca977dd23e 100644 --- a/packages/nuxt/src/pages/runtime/composables.ts +++ b/packages/nuxt/src/pages/runtime/composables.ts @@ -1,8 +1,18 @@ import { KeepAliveProps, TransitionProps, UnwrapRef } from 'vue' -import type { RouteLocationNormalizedLoaded, RouteRecordRedirectOption } from 'vue-router' +import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteRecordRedirectOption } from 'vue-router' +import type { NuxtError } from '#app' export interface PageMeta { [key: string]: any + /** + * Validate whether a given route can validly be rendered with this page. + * + * Return true if it is valid, or false if not. If another match can't be found, + * this will mean a 404. You can also directly return an object with + * statusCode/statusMessage to respond immediately with an error (other matches + * will not be checked). + */ + validate?: (route: RouteLocationNormalized) => boolean | Promise | Partial | Promise> /** * Where to redirect if the route is directly matched. The redirection happens * before any navigation guard and triggers a new navigation with the new diff --git a/packages/nuxt/src/pages/runtime/router.ts b/packages/nuxt/src/pages/runtime/router.ts index 03bc591227..15dc5bc074 100644 --- a/packages/nuxt/src/pages/runtime/router.ts +++ b/packages/nuxt/src/pages/runtime/router.ts @@ -159,7 +159,8 @@ export default defineNuxtPlugin(async (nuxtApp) => { if (process.server || (!nuxtApp.payload.serverRendered && nuxtApp.isHydrating)) { if (result === false || result instanceof Error) { const error = result || createError({ - statusMessage: `Route navigation aborted: ${initialURL}` + statusCode: 404, + statusMessage: `Page Not Found: ${initialURL}` }) return callWithNuxt(nuxtApp, showError, [error]) } diff --git a/packages/nuxt/src/pages/runtime/validate.ts b/packages/nuxt/src/pages/runtime/validate.ts new file mode 100644 index 0000000000..14d5a8aedd --- /dev/null +++ b/packages/nuxt/src/pages/runtime/validate.ts @@ -0,0 +1,12 @@ +import { createError, defineNuxtRouteMiddleware } from '#app' + +export default defineNuxtRouteMiddleware(async (to) => { + if (!to.meta?.validate) { return } + + const result = await Promise.resolve(to.meta.validate(to)) + if (typeof result === 'boolean') { + return result + } + + return createError(result) +}) diff --git a/test/basic.test.ts b/test/basic.test.ts index 9a590cafdb..3a40675be7 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -65,6 +65,11 @@ describe('pages', () => { expect(headers.get('location')).toEqual('/') }) + it('validates routes', async () => { + const { status } = await fetch('/forbidden') + expect(status).toEqual(404) + }) + it('render 404', async () => { const html = await $fetch('/not-found') diff --git a/test/fixtures/basic/pages/[...slug].vue b/test/fixtures/basic/pages/[...slug].vue index 47a02c1123..a805ed7aea 100644 --- a/test/fixtures/basic/pages/[...slug].vue +++ b/test/fixtures/basic/pages/[...slug].vue @@ -4,3 +4,9 @@
404 at {{ $route.params.slug[0] }}
+ +