feat(pages): add validate hook for definePageMeta (#7870)

This commit is contained in:
Daniel Roe 2022-10-10 11:18:20 +01:00 committed by GitHub
parent 75f4a54f7e
commit 829a550580
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 78 additions and 23 deletions

View File

@ -117,3 +117,25 @@ definePageMeta({
:: ::
:ReadMore{link="/guide/directory-structure/middleware"} :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]
<script setup>
definePageMeta({
validate: async (route) => {
const nuxtApp = useNuxtApp()
// Check if the id is made up of digits
return /^\d+$/.test(params.id)
}
})
</script>
```

View File

@ -23,6 +23,7 @@ title: "definePageMeta"
definePageMeta(meta: PageMeta) => void definePageMeta(meta: PageMeta) => void
interface PageMeta { interface PageMeta {
validate?: (route: RouteLocationNormalized) => boolean | Promise<boolean> | Partial<NuxtError> | Promise<Partial<NuxtError>>
redirect?: RouteRecordRedirectOption redirect?: RouteRecordRedirectOption
alias?: string | string[] alias?: string | string[]
pageTransition?: boolean | TransitionProps 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). 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<boolean> | Partial<NuxtError> | Promise<Partial<NuxtError>>`
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 :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]`** **`[key: string]`**
- **Type**: `any` - **Type**: `any`

View File

@ -112,25 +112,23 @@ See [layout migration](/migration/pages-and-layouts).
## `validate` ## `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 [pages/users/[id].vue]
- <script> - <script>
- export default { - export default {
- async validate({ params, query, store }) { - async validate({ params }) {
- return true // if valid - return /^\d+$/.test(params.id)
- } - }
- } - }
+ <script setup> + <script setup>
+ definePageMeta({ + definePageMeta({
+ middleware: [ + validate: async (route) => {
+ async function (to, from) { + const nuxtApp = useNuxtApp()
+ const nuxtApp = useNuxtApp() + return /^\d+$/.test(params.id)
+ if (!valid) { + }
+ return abortNavigation('Page not found')
+ }
+ }
+ ]
+ }) + })
</script> </script>
``` ```

View File

@ -234,7 +234,8 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => {
if (process.server) { if (process.server) {
if (result === false || result instanceof Error) { if (result === false || result instanceof Error) {
const error = result || createError({ const error = result || createError({
statusMessage: `Route navigation aborted: ${initialURL}` statusCode: 404,
statusMessage: `Page Not Found: ${initialURL}`
}) })
return callWithNuxt(nuxtApp, showError, [error]) return callWithNuxt(nuxtApp, showError, [error])
} }

View File

@ -50,6 +50,11 @@ export default defineNuxtModule({
if (app.mainComponent!.includes('@nuxt/ui-templates')) { if (app.mainComponent!.includes('@nuxt/ui-templates')) {
app.mainComponent = resolve(runtimeDir, 'app.vue') 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 // Prerender all non-dynamic page routes when generating app

View File

@ -1,8 +1,18 @@
import { KeepAliveProps, TransitionProps, UnwrapRef } from 'vue' 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 { export interface PageMeta {
[key: string]: any [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<boolean> | Partial<NuxtError> | Promise<Partial<NuxtError>>
/** /**
* Where to redirect if the route is directly matched. The redirection happens * Where to redirect if the route is directly matched. The redirection happens
* before any navigation guard and triggers a new navigation with the new * before any navigation guard and triggers a new navigation with the new

View File

@ -159,7 +159,8 @@ export default defineNuxtPlugin(async (nuxtApp) => {
if (process.server || (!nuxtApp.payload.serverRendered && nuxtApp.isHydrating)) { if (process.server || (!nuxtApp.payload.serverRendered && nuxtApp.isHydrating)) {
if (result === false || result instanceof Error) { if (result === false || result instanceof Error) {
const error = result || createError({ const error = result || createError({
statusMessage: `Route navigation aborted: ${initialURL}` statusCode: 404,
statusMessage: `Page Not Found: ${initialURL}`
}) })
return callWithNuxt(nuxtApp, showError, [error]) return callWithNuxt(nuxtApp, showError, [error])
} }

View File

@ -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)
})

View File

@ -65,6 +65,11 @@ describe('pages', () => {
expect(headers.get('location')).toEqual('/') expect(headers.get('location')).toEqual('/')
}) })
it('validates routes', async () => {
const { status } = await fetch('/forbidden')
expect(status).toEqual(404)
})
it('render 404', async () => { it('render 404', async () => {
const html = await $fetch('/not-found') const html = await $fetch('/not-found')

View File

@ -4,3 +4,9 @@
<div>404 at {{ $route.params.slug[0] }}</div> <div>404 at {{ $route.params.slug[0] }}</div>
</div> </div>
</template> </template>
<script setup lang="ts">
definePageMeta({
validate: to => to.path !== '/forbidden'
})
</script>