skip to content

In Vue Router 5 file routing, what can definePage() set on a page's route, and why can't it read variables from <script setup>?

level: middleimportance: should knowfreq 26%

answer

  1. a compiler macro, not a runtime call
  2. extracted at build time
  3. name, path, alias, meta
  4. static values only
  5. name false and guards

basics

~20 s

definePage() is a build-time macro that overrides a page's route record: name, path, alias, meta and other record fields. The plugin extracts its argument and removes it from the component, so it cannot reference bindings declared in script setup.

solid answer

~40 s

`definePage()` is a macro, globally available in page components, that lets a file override its generated route: a custom `name` (or `false` to make the route anonymous), a `path`, an `alias` and `meta`, among other record fields. The plugin extracts the object at build time into the route table and strips the call from `<script setup>`, so the argument must be static: literals or imported values, never a binding declared in the same `<script setup>`, which triggers a build diagnostic. For the same reason `beforeEnter` is not supported there; set `meta` and check it in a global guard. The `<route>` custom block (JSON5 by default) is the declarative alternative. It is Vue Router's API; `definePageMeta()` is Nuxt's and does not exist here.

code

vue · 15 lines
vue
<script setup lang="ts">
import { POST_META } from '@/config/pages'

const title = 'Blog post' // a <script setup> binding

definePage({
  name: 'post',
  meta: { ...POST_META },   // ok: imported value
  // meta: { title },       // rejected: binding from this script setup
})
</script>

<template>
  <article><h1>{{ title }}</h1></article>
</template>

go deeper

for a junior

Recall that definePage() lets a page change its own name, alias or meta, and that it is written at the top of script setup.

for a middle

Explain build-time extraction: why local bindings are rejected, why imports are fine, and why name and path must be literals.

for a senior

Show the production pattern: meta set per page plus one global guard, and know that runtime edits to routes never reach the generated types.

for a principal

Weigh per-page macros against central extendRoute rules: local overrides are discoverable in the file, central rules keep policy in one reviewed place.

## What definePage() is In Vue Router 5 file-based routing, every page's route record is generated from its file path. `definePage()` is how a page adjusts its own record from inside the component. It is a **compiler macro**: the plugin reads the call while building the route table, merges the object into that page's record, and removes the call from the component's runtime code. It is declared globally for page components (its type lives in `vue-router/experimental`), so no import is needed. ```ts // inside <script setup lang="ts"> of src/pages/posts/[slug].vue definePage({ name: 'post', alias: ['/p/:slug'], meta: { requiresAuth: false, layoutWidth: 'wide' }, }) ``` ## What it can set | Field | Effect | Constraint | |---|---|---| | `name` | replaces the file-derived name such as `/posts/[slug]` | a string literal, or `false` | | `path` | replaces the generated path | a string literal | | `alias` | extra URLs that render the same page | a string literal or an array of them | | `meta` | route meta read by guards and layouts | static values | The type accepts most other route-record fields too; it excludes `component`, `components` and `children`, because those come from the file tree. Setting `name: false` makes the route **anonymous**: it disappears from the generated types and cannot be matched, which is a way to keep a helper file in the routes folder without exposing it. ## Why it cannot see your variables The object passed to `definePage()` is lifted out of the component and evaluated with the route table, not inside `setup()`. At that point no component instance exists, so local state cannot exist either. The plugin checks this: - A **binding declared in the same `<script setup>`** (a `const`, a `ref`, a function) inside the object is rejected with a build diagnostic that tells you to pass static values. - **Imported values** are allowed: the plugin keeps the static imports the object uses, so a constant from a shared module works. - **A second `definePage()` call** in one file is reported and ignored; only the first is used. - **`name` and `path` must be string literals**, because they are read statically to build names and types. ## Guards and the alternatives The docs state that `beforeEnter` is **not supported** in `definePage()`: a function literal looks as if it could close over component variables, and it cannot. The supported patterns are: 1. Put a flag in `meta` with `definePage()` and check it in a global `router.beforeEach`. 2. Add the guard at runtime by editing the imported `routes` array before `createRouter()`. Two other ways to change generated routes exist: - **The `<route>` custom block** in the SFC, JSON5 by default (`<route lang="yaml">` also works), declares the same overrides as data. The `vue-router/volar/sfc-route-blocks` Volar plugin gives it editor support. - **The `extendRoute` option** of the plugin edits routes centrally in `vite.config`, for rules that apply to many pages. Changes made by `definePage()`, `<route>` or `extendRoute` land in the generated types; changes made by mutating `routes` at runtime do not. ## Choosing where an override lives | Mechanism | Where it is written | Reflected in types | Good for | |---|---|---|---| | `definePage()` | the page's `<script setup>` | yes | per-page name, alias, meta | | `<route>` block | the page's SFC, as JSON5 or YAML | yes | the same overrides as plain data | | `extendRoute` | `vite.config`, once | yes | rules that apply to many pages | | editing `routes` | before `createRouter()` | no | redirects and runtime-only records | In a blog, the practical split is usually: - per-post-page facts such as `meta.requiresAuth` or an `alias` for short links go in `definePage()`, next to the component they describe; - a rule such as "every page under `/admin` requires a session" goes in `extendRoute`, so it cannot be forgotten on a new page; - legacy redirects go on the runtime array, where they stay out of autocompletion. ## Not to be confused with Nuxt Nuxt's `definePageMeta()` looks similar and is also a macro, but it belongs to Nuxt's own pages system. In a plain Vue app on Vue Router 5 the macro is `definePage()`, and code copied from a Nuxt project has to be translated rather than pasted.

  • How do you protect a page that needs a signed-in user if beforeEnter is not allowed in definePage()?
    Set a flag such as `meta: { requiresAuth: true }` in `definePage()`, then read `to.meta.requiresAuth` in a global `router.beforeEach` and redirect when there is no session. The meta travels with the generated route, while the guard lives in ordinary runtime code that can reach your auth store.
  • When would you use the <route> block instead of definePage()?
    When the overrides are plain data and you prefer them outside the script, or when migrating from a setup that already used `<route>` blocks. It accepts JSON5 by default or YAML with `lang="yaml"`, and needs the `vue-router/volar/sfc-route-blocks` plugin for editor support.

saying these in an interview costs you the question

  • definePage() runs inside setup(), so it can read the component's refs.
  • definePage() cannot use any variable, not even an imported constant.
  • A beforeEnter function inside definePage() is the way to guard a page.
  • definePageMeta() is Vue Router's name for the same macro.
  • Calling definePage() twice in one file merges both objects.