Nuxt Guide
Pages & Routing
File-based routing, definePageMeta, and data fetching conventions.
File Conventions
Pages live in app/pages/. Nuxt file-based routing applies:
| File | Route |
|---|---|
app/pages/index.vue | / |
app/pages/login.vue | /login |
app/pages/items/index.vue | /items |
app/pages/items/[id].vue | /items/:id |
app/pages/[slug]/index.vue | /:slug (dynamic segment) |
Page Structure
Pages follow the same <script setup> convention as components. The order inside <script setup> is:
definePageMeta— always first- Composables
- Reactive state
- Data fetching (called from
onMounted) - Handler functions
<script lang="ts" setup>
// 1. Page meta — always first in <script setup>
definePageMeta({
layout: 'default',
middleware: ['auth'],
})
// 2. Composables
const { getAll, items, page, pages } = useResource()
const { handleApiAction, loading } = useFormHandler()
// 3. Reactive state
const dialogs = reactive({ view: false, edit: false, delete: false })
const selected = ref<IResource | null>(null)
// 4. Data fetching
onMounted(async () => {
await fetchItems()
})
async function fetchItems() {
await handleApiAction(
async () => {
const res = await getAll({ page: page.value })
items.value = res.items
pages.value = res.pages
},
{ skipAlert: true },
)
}
</script>
<template>
<div class="space-y-6">
<h1 class="text-2xl font-bold">Items</h1>
<ResourceTable
:items="items"
@view="(item) => { selected = item; dialogs.view = true }"
@delete="(item) => { selected = item; dialogs.delete = true }"
/>
</div>
</template>
definePageMeta
Always declared at the top of <script setup>:
definePageMeta({
layout: 'default', // matches app/layouts/default.vue
middleware: ['auth'], // named middleware file(s) to run
})
Common middleware values:
| Value | File | Effect |
|---|---|---|
'auth' | app/middleware/auth.ts | Redirects to /login if no token |
'admin' | app/middleware/admin.ts | Throws 403 if not admin role |
'org' | app/middleware/01.org.ts | Loads org into state |
Dynamic Routes
Dynamic segments use [param] in the file name, accessed with useRoute():
// app/pages/items/[id].vue
const route = useRoute()
const itemId = route.params.id as string
Data Fetching Pattern
All data fetching is done manually inside onMounted — not with useAsyncData / useFetch at the top level — because SSR is disabled and explicit control is preferred:
onMounted(async () => {
await loadData()
})
async function loadData() {
await handleApiAction(
async () => {
const res = await resource.getAll({ page: page.value })
items.value = res.items
pages.value = res.pages
},
{ skipAlert: true }, // suppress toast on initial load
)
}
Pair with a watch when a filter or page number changes:
watch(page, () => loadData())
watch(searchQuery, useDebounceFn(() => loadData(), 400))