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

4. Layouts

Layouts — это обёртки для твоих страниц. Header, Footer, Sidebar — всё, что повторяется на множестве страниц, живёт в layout. Меняешь layout — меняется обёртка всех использующих его страниц.


Без layouts каждая страница была бы:

<!-- pages/about.vue — БЕЗ layouts -->
<template>
<div>
<!-- Дублирование на каждой странице! -->
<header>
<nav>...</nav>
</header>
<!-- Уникальный контент -->
<main>О нас</main>
<footer>...</footer>
</div>
</template>

С layouts:

<!-- pages/about.vue — С layouts -->
<template>
<!-- Только уникальный контент! -->
<main>О нас</main>
</template>

Создаём layouts/default.vue:

layouts/default.vue
<template>
<div class="app-wrapper">
<AppHeader />
<!-- Слот для контента страницы -->
<main class="main-content">
<slot />
</main>
<AppFooter />
</div>
</template>
<style scoped>
.app-wrapper {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-content {
flex: 1;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
</style>

Этот layout автоматически применяется ко всем страницам.


app.vue
<template>
<NuxtLayout>
<!-- NuxtPage рендерится внутри <slot> layout -->
<NuxtPage />
</NuxtLayout>
</template>

Создаём несколько layouts для разных страниц:

layouts/
├── default.vue ← Дефолтный (применяется везде)
├── admin.vue ← Для админ-панели
├── auth.vue ← Для страниц входа/регистрации
├── blog.vue ← Для блога
└── blank.vue ← Чистый (без header/footer)
layouts/admin.vue
<template>
<div class="admin-layout">
<AdminSidebar />
<div class="admin-content">
<AdminHeader />
<main>
<slot />
</main>
</div>
</div>
</template>
layouts/auth.vue
<template>
<div class="auth-layout">
<div class="auth-card">
<NuxtLink to="/">
<img src="/logo.svg" alt="Logo" />
</NuxtLink>
<slot />
</div>
</div>
</template>

pages/admin/dashboard.vue
<script setup>
definePageMeta({
layout: 'admin'
})
</script>
pages/login.vue
<script setup>
definePageMeta({
layout: 'auth'
})
</script>
<!-- pages/landing.vue — без layout -->
<script setup>
definePageMeta({
layout: false
})
</script>
<template>
<!-- Полностью кастомная страница без обёртки -->
<div class="landing">...</div>
</template>
<script setup>
const { data: user } = await useFetch('/api/auth/me')
// Меняем layout динамически
definePageMeta({
layout: 'default'
})
// Или через useRoute
const layout = computed(() =>
user.value?.isAdmin ? 'admin' : 'default'
)
</script>

Или через NuxtLayout с пропом:

app.vue
<script setup>
const route = useRoute()
// Берём layout из мета-данных маршрута
const layout = computed(() =>
route.meta.layout || 'default'
)
</script>
<template>
<NuxtLayout :name="layout">
<NuxtPage />
</NuxtLayout>
</template>

Layouts поддерживают несколько слотов:

layouts/blog.vue
<template>
<div class="blog-layout">
<header>
<slot name="header">
<!-- Дефолтный header если не передан -->
<h1>Блог</h1>
</slot>
</header>
<div class="blog-content">
<main>
<!-- Основной контент -->
<slot />
</main>
<aside>
<slot name="sidebar">
<!-- Дефолтный sidebar -->
<BlogSidebar />
</slot>
</aside>
</div>
</div>
</template>
pages/blog/[slug].vue
<script setup>
definePageMeta({ layout: 'blog' })
</script>
<template>
<!-- Передаём контент в именованные слоты -->
<template #header>
<h1>{{ post.title }}</h1>
<p>{{ post.description }}</p>
</template>
<!-- Основной контент (в default slot) -->
<article>
{{ post.content }}
</article>
<!-- Кастомный sidebar -->
<template #sidebar>
<RelatedPosts :posts="relatedPosts" />
</template>
</template>

layouts/default.vue
<template>
<div>
<AppHeader />
<slot />
<AppFooter />
</div>
</template>
nuxt.config.ts
export default defineNuxtConfig({
app: {
// Переход для layout
layoutTransition: {
name: 'layout',
mode: 'out-in'
},
// Переход для страниц
pageTransition: {
name: 'page',
mode: 'out-in'
}
}
})
assets/css/transitions.css
/* Layout переходы */
.layout-enter-active,
.layout-leave-active {
transition: all 0.4s ease;
}
.layout-enter-from {
opacity: 0;
transform: translateX(-20px);
}
.layout-leave-to {
opacity: 0;
transform: translateX(20px);
}
/* Page переходы */
.page-enter-active,
.page-leave-active {
transition: all 0.3s ease;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
transform: translateY(10px);
}

Кастомный переход на конкретной странице:

<script setup>
definePageMeta({
layout: 'admin',
layoutTransition: {
name: 'slide-left'
},
pageTransition: {
name: 'fade'
}
})
</script>

<!-- error.vue — специальный файл (не в layouts/) -->
<script setup lang="ts">
const props = defineProps<{
error: {
statusCode: number
statusMessage: string
message: string
}
}>()
// Перезагрузить приложение
const handleError = () => clearError({ redirect: '/' })
</script>
<template>
<div class="error-page">
<h1>{{ error.statusCode }}</h1>
<p>{{ error.statusMessage }}</p>
<button @click="handleError">На главную</button>
</div>
</template>

<!-- Условный рендеринг layout -->
<template>
<NuxtLayout :name="showHeader ? 'default' : 'blank'">
<NuxtPage />
</NuxtLayout>
</template>
<!-- Передача пропсов в layout -->
<template>
<NuxtLayout name="admin" :sidebar-collapsed="sidebarCollapsed">
<NuxtPage />
</NuxtLayout>
</template>
<!-- layouts/admin.vue — принимаем проп -->
<script setup>
const props = defineProps<{
sidebarCollapsed?: boolean
}>()
</script>
<template>
<div :class="{ 'sidebar-collapsed': sidebarCollapsed }">
<slot />
</div>
</template>

✅ DO:
- Один дефолтный layout для большинства страниц
- Отдельный layout для auth (без навигации)
- Отдельный layout для admin (с sidebar)
- Используй именованные слоты для гибкости
- Держи layouts простыми — только обёртка
❌ DON'T:
- Не дублируй логику между layouts
- Не делай layout слишком сложным
- Не помещай бизнес-логику в layout
- Не забывай про <slot /> — без него контент не отобразится

Реальный пример: Многоуровневое приложение 🏗️

Заголовок раздела «Реальный пример: Многоуровневое приложение 🏗️»
layouts/
├── default.vue ← Публичный сайт
│ └── Header + Footer
├── auth.vue ← Авторизация
│ └── Центрированная форма
└── admin.vue ← Административная панель
└── Sidebar + Top bar
pages/
├── index.vue → layout: 'default'
├── about.vue → layout: 'default'
├── login.vue → layout: 'auth'
├── register.vue → layout: 'auth'
└── admin/
├── dashboard.vue → layout: 'admin'
├── users.vue → layout: 'admin'
└── settings.vue → layout: 'admin'