Перейти к содержимому

3. Файловая маршрутизация

Одна из самых мощных фич Nuxt — файловая маршрутизация. Создал файл в pages/ — получил маршрут. Никаких настроек, никаких createRouter. Структура папок = структура URL.


Nuxt сканирует директорию pages/ и автоматически создаёт конфигурацию Vue Router:

pages/
├── index.vue → /
├── about.vue → /about
├── contact.vue → /contact
└── blog/
├── index.vue → /blog
└── first-post.vue → /blog/first-post

Ни строки настройки роутера не нужно. Всё работает автоматически.


app.vue
<template>
<div>
<!-- NuxtPage — это <RouterView> от Nuxt -->
<NuxtPage />
</div>
</template>

pages/index.vue → /
pages/about.vue → /about
pages/blog/index.vue → /blog
pages/users/profile.vue → /users/profile
pages/blog/[slug].vue → /blog/:slug
pages/users/[id].vue → /users/:id
pages/[category]/[id].vue → /:category/:id
pages/blog/[slug].vue
<script setup lang="ts">
const route = useRoute()
// route.params.slug — текущий slug
const slug = route.params.slug as string
// Загружаем пост по slug
const { data: post } = await useFetch(\`/api/posts/\${slug}\`)
</script>
<template>
<article>
<h1>{{ post?.title }}</h1>
<p>slug: {{ $route.params.slug }}</p>
</article>
</template>
pages/[...slug].vue → /anything/deep/path
pages/docs/[...path].vue → /docs/guide/intro/setup
pages/[...slug].vue
<script setup>
const route = useRoute()
// route.params.slug — массив сегментов
// URL: /a/b/c → ['a', 'b', 'c']
const breadcrumbs = computed(() =>
(route.params.slug as string[]).map((s, i, arr) => ({
name: s,
path: '/' + arr.slice(0, i + 1).join('/')
}))
)
</script>
pages/[[...slug]].vue → / и /anything/deep/path

Для вложенных роутов нужна директория с файлом .vue того же имени:

pages/
└── users/
├── index.vue → /users (список)
└── [id]/
├── index.vue → /users/:id (профиль)
├── edit.vue → /users/:id/edit
└── settings.vue → /users/:id/settings

Или с вложенным NuxtPage (настоящие nested routes):

pages/
├── parent.vue → /parent (с <NuxtPage> внутри)
└── parent/
├── child-a.vue → /parent/child-a
└── child-b.vue → /parent/child-b
pages/parent.vue
<template>
<div>
<h1>Родительская страница</h1>
<nav>
<NuxtLink to="/parent/child-a">Child A</NuxtLink>
<NuxtLink to="/parent/child-b">Child B</NuxtLink>
</nav>
<!-- Дочерний роут рендерится здесь -->
<NuxtPage />
</div>
</template>

<template>
<!-- Базовый NuxtLink -->
<NuxtLink to="/">Главная</NuxtLink>
<NuxtLink to="/about">О нас</NuxtLink>
<!-- Динамический маршрут -->
<NuxtLink :to="\`/blog/\${post.slug}\`">{{ post.title }}</NuxtLink>
<!-- Именованный маршрут -->
<NuxtLink :to="{ name: 'blog-slug', params: { slug: 'hello' } }">
Статья
</NuxtLink>
<!-- С query параметрами -->
<NuxtLink :to="{ path: '/search', query: { q: 'nuxt' } }">
Поиск
</NuxtLink>
<!-- Внешняя ссылка -->
<NuxtLink to="https://nuxt.com" external>Сайт Nuxt</NuxtLink>
<!-- Активный класс -->
<NuxtLink
to="/about"
active-class="text-green-500"
exact-active-class="font-bold"
>
О нас
</NuxtLink>
<!-- Prefetch (предзагрузка по умолчанию) -->
<NuxtLink to="/heavy-page" :prefetch="false">
Тяжёлая страница
</NuxtLink>
</template>

pages/admin/dashboard.vue
<script setup lang="ts">
definePageMeta({
// Название маршрута
name: 'admin-dashboard',
// Layout для этой страницы
layout: 'admin',
// Middleware для этой страницы
middleware: ['auth', 'admin-only'],
// Мета-данные (для кастомного использования)
requiresAuth: true,
roles: ['admin', 'superadmin'],
// Алиас
alias: ['/dashboard', '/admin'],
// Переходы
pageTransition: {
name: 'slide',
mode: 'out-in'
},
// Keepalive
keepalive: true,
})
</script>
pages/public/landing.vue
<script setup>
definePageMeta({
// Отключить layout
layout: false,
})
</script>

<script setup>
const router = useRouter()
const route = useRoute()
// Переход на страницу
await router.push('/about')
await router.push({ name: 'users-id', params: { id: 1 } })
// Замена текущей записи в истории
await router.replace('/login')
// Назад/Вперёд
router.back()
router.forward()
router.go(-2)
// Текущий маршрут
console.log(route.path) // '/blog/hello-world'
console.log(route.params) // { slug: 'hello-world' }
console.log(route.query) // { page: '2' }
console.log(route.hash) // '#section'
console.log(route.name) // 'blog-slug'
console.log(route.fullPath) // '/blog/hello-world?page=2#section'
</script>

pages/
├── index.vue → / (главная)
├── [...slug].vue → catch-all
├── [[slug]].vue → опциональный параметр
└── (group)/ ← группа (без влияния на URL)
├── page-a.vue → /page-a
└── page-b.vue → /page-b

Игнорирование файлов:

pages/
└── -drafts/ ← файлы с - в начале игнорируются
└── new-post.vue ← не создаёт роут

С typedPages: true в nuxt.config.ts:

nuxt.config.ts
export default defineNuxtConfig({
experimental: {
typedPages: true
}
})
<script setup lang="ts">
// Автоматически типизированные параметры!
const route = useRoute('blog-slug')
// route.params.slug — TypeScript знает, что это string
const router = useRouter()
// TypeScript подскажет доступные роуты
await router.push({ name: 'users-id', params: { id: '123' } })
</script>

app.vue
<template>
<NuxtPage :page-key="route => route.fullPath" />
</template>
<style>
.page-enter-active,
.page-leave-active {
transition: all 0.3s ease;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
transform: translateY(20px);
}
</style>

Или через конфиг:

nuxt.config.ts
export default defineNuxtConfig({
app: {
pageTransition: { name: 'page', mode: 'out-in' },
}
})

pages/users/[id].vue
<script setup lang="ts">
const route = useRoute()
// Реактивно обновляется при смене :id
const { data: user, refresh } = await useFetch(
() => \`/api/users/\${route.params.id}\`
)
// Следим за сменой параметров
watch(() => route.params.id, () => refresh())
</script>
<script setup>
const route = useRoute()
const breadcrumbs = computed(() => {
const paths = route.path.split('/').filter(Boolean)
return paths.map((segment, index) => ({
label: segment,
to: '/' + paths.slice(0, index + 1).join('/')
}))
})
</script>