In Nuxt 4, what does definePageMeta do, and why can it not use a ref declared in the same script setup?
answer
- compiler macro, not a runtime call
- hoisted out of the component
- imports and pure functions only
- validate, middleware, layout, key
- Nuxt 4: name lives on route
basics
~20 sdefinePageMeta is a Nuxt compiler macro that attaches route metadata such as middleware, layout, validate or key to a page in app/pages/. The build hoists its object out of the component, so it can use imports and pure functions but not setup state.
solid answer
~40 s`definePageMeta` looks like a call in a page's `<script setup>`, but Nuxt treats it as a compiler macro: at build time the object is lifted out of the component into a module the route table imports, so the router has the metadata before the page component loads and can run the page's middleware or validate its params during navigation. Because the object lives outside the component, it can reference imported bindings and locally defined pure functions, but a `ref` from the same setup is out of its reach. Nuxt acts on keys such as `middleware`, `layout`, `validate`, `key`, `keepalive`, `alias`, `name`, `path` and `pageTransition`; any other key becomes custom `route.meta`, merged across nested routes. In Nuxt 4, `name` and `path` set this way appear only on the route itself, as `route.name`, not on `route.meta`.
code
vue · 16 lines<!-- app/pages/recipes/[slug].vue -->
<script setup lang="ts">
import { isRecipeSlug } from '~/utils/slugs'
definePageMeta({
// runs during navigation; false -> 404
validate: route => isRecipeSlug(route.params.slug),
pageType: 'recipe',
})
const route = useRoute()
</script>
<template>
<article>{{ route.params.slug }}</article>
</template>go deeper
Recall that definePageMeta sets route metadata such as middleware and layout, and that it only works in page components under app/pages/.
Explain the macro model: the object is hoisted out of the component, which is why only imports and pure functions work, and how meta merges across nested routes.
Use validate and middleware to reject bad params before a page renders or fetches, and type custom meta by augmenting PageMeta so wrong values fail type-checking.
Decide what belongs in per-page meta versus central configuration such as a pages:extend hook, trading locality in each page against one reviewable place for cross-cutting policy.
## A macro, not a function call **`definePageMeta`** is how a Nuxt 4 page in `app/pages/` attaches **route metadata** to its own route. It looks like an ordinary call at the top of `<script setup>` (it also works in a plain `<script>` block), but it is a **compiler macro**, in the same family as Vue's `defineProps` and `defineEmits`. The build reads its argument, compiles the call away, and emits the object in a separate module that the generated route table imports. The page component itself is code-split and may load later; its metadata is available as soon as the route table is. That design is what lets Nuxt act on the metadata **during navigation, before the page renders**: run the page's middleware, validate its params, pick its layout. ## What the hoisted object can reference Because the object is moved out of the component, it no longer sits in the scope of `setup`: | Reference inside `definePageMeta` | Works? | Why | |---|---|---| | Literals, object and array expressions | yes | nothing outside is needed | | Imported bindings (`import { isRecipeSlug } from '~/utils/slugs'`) | yes | imports travel with the hoisted code | | Locally defined **pure** functions | yes | they can be moved along with the object | | A `ref`, `computed` or other state created in the same setup | no | that state belongs to a component instance that does not exist when the meta is read | | Functions with side effects | no | the Nuxt docs warn they lead to unexpected behaviour once hoisted | So `const title = ref('')` followed by `definePageMeta({ title })` is wrong: the ref is hoisted out of the component instead of being part of it. A dynamic value belongs in the component itself, for a title through `useHead`, not in route meta. ## Keys Nuxt acts on Any key is allowed, but these have built-in meaning: - **`middleware`**: a named middleware string, an inline function, or an array of both; combined with middleware from the other matched routes. - **`layout`**: a layout name, or `false` for no layout. - **`validate`**: a function of the route; `true` accepts, `false` becomes a 404, and an object with `status` and `statusText` sets its own error. - **`key`**: the `<NuxtPage>` key for this page, which decides when it remounts. - **`keepalive`**: wrap the page in Vue's `KeepAlive`; off by default. - **`name`**, **`path`**, **`alias`**, **`redirect`**, **`props`**: route-record options, including a custom path matcher when the filename cannot express the pattern. - **`pageTransition`**, **`layoutTransition`**, **`scrollToTop`**: presentation during navigation. Keys Nuxt does not recognise become custom **`route.meta`** fields. When routes nest, the meta of all matched levels is merged into one object. ## Validating the recipe slug `validate` runs inside a **global route middleware that Nuxt registers ahead of yours**, so it executes during navigation, before your middleware and before the page's setup: 1. The router starts navigating to a URL such as `/recipes/Lemon Tart!`. 2. Nuxt's validate middleware calls the page's `validate(route)`. 3. `false` produces a 404 error (`Page Not Found`); an object such as `{ status: 400, statusText: 'Bad slug' }` sets its own status instead. 4. Only `true` lets navigation continue to the remaining middleware and the page. That makes `validate` the place for **shape** checks on params, such as a slug pattern or a numeric id. Whether the recipe exists is still the page's job once it fetches. ## Custom meta and typing Custom keys are read back through `useRoute().meta`, for example a `pageType: 'recipe'` flag that a layout reads. To type them, augment the `PageMeta` interface in a declaration file under `declare module '#app'`; values of the wrong type then fail type-checking. The `middleware` key is already typed from the files in `app/middleware/`, so a misspelled middleware name is a type error too. ## Nuxt 4 details and pitfalls - **Route meta was deduplicated in Nuxt 4.** `name` and `path` set here are available only on the route object (`route.name`), not on `route.meta` as in Nuxt 3. - **Only pages count.** In a regular component or a composable the macro is not extracted and does nothing; in development Nuxt warns with `NUXT_E1007`, a compile-time helper called at runtime. - **It is not Vue Router's `definePage`.** That macro belongs to Vue Router's own file-based routing plugin; a Nuxt page uses `definePageMeta`. - **Named-view files are ignored.** Meta declared in a `[email protected]` sibling has no effect; only the default page file's meta applies.
- What happens if a RecipeCard component, not a page, calls definePageMeta({ layout: 'wide' })?Nothing useful. The macro only takes effect in page components, because that is where the build extracts it onto a route. Anywhere else it runs as a no-op at runtime, and in development Nuxt warns with `NUXT_E1007`. The layout has to be set in the page's own meta, or changed at runtime with `setPageLayout`.
- Why is validate a better place than the page's setup to reject a malformed slug?`validate` runs during navigation, in a global middleware Nuxt registers ahead of the app's own, so a malformed slug is rejected before the page's setup runs or fetches anything. Returning `false` yields a 404, and returning `{ status, statusText }` customises it. A check in setup runs only once the page has started rendering, so its requests may already be in flight.
saying these in an interview costs you the question
- definePageMeta runs each time the page's setup runs, like a composable
- A ref from the same script setup makes a reactive page-meta value
- definePageMeta works in any component, not only in pages
- In Nuxt 4, route.meta.name holds the name set by definePageMeta
- Custom keys are rejected; only Nuxt's special keys are allowed
- definePageMeta is Vue Router's definePage under another name