This guide covers creating pages with file-based routing, configuring authentication protection via middleware, and integrating pages into your navigation.
Nuxt uses file-based routing, which means every .vue file in the app/pages/ directory automatically becomes a route:
app/pages/index.vue → /app/pages/about.vue → /aboutapp/pages/blog/index.vue → /blogapp/pages/blog/[slug].vue → /blog/:slug (dynamic route)app/pages/user/[id]/settings.vue → /user/:id/settingsLet's create a simple "About" page. Create a new file at app/pages/about.vue:
<script setup lang="ts">
definePageMeta({
layout: 'public',
})
useSeoMeta({
title: 'About',
description: 'Learn more about our application',
})
</script>
<template>
<div class="base-container py-8">
<h1 class="text-4xl font-bold mb-4">About us</h1>
<p class="text-lg text-muted-foreground">
This is a simple about page created with Nuxt's file-based routing.
</p>
</div>
</template>
That's it! Your page is now accessible at http://localhost:3000/about.
layout: 'public' to make this page accessible without authentication. Without this, the global auth middleware would require users to log in first.Protected pages require authentication. Let's create a team page that only authenticated users can access.
Create app/pages/team.vue:
<script setup lang="ts">
// This page is automatically protected by the global auth middleware
// Only authenticated users can access routes that aren't in the public list
const userStore = useUserStore()
const { user } = storeToRefs(userStore)
useSeoMeta({
title: 'Team',
description: 'Manage your team',
})
</script>
<template>
<div class="base-container py-8">
<h1 class="text-4xl font-bold mb-2">Team</h1>
<p class="text-muted-foreground mb-8">Manage your team members</p>
<Card v-if="user" class="max-w-2xl">
<CardHeader>
<CardTitle>Team owner</CardTitle>
<CardDescription>Current account information</CardDescription>
</CardHeader>
<CardContent>
<div class="space-y-2">
<p><strong>Name:</strong> {{ user.name }}</p>
<p><strong>Email:</strong> {{ user.email }}</p>
</div>
</CardContent>
</Card>
</div>
</template>
app/middleware/auth.global.ts). Any route that isn't in the public routes list requires authentication. Unauthenticated users will be redirected to the login page.The boilerplate uses a global middleware that automatically protects routes. Here's how it works:
Public routes (no authentication required):
/auth/, /blog/, /checkout/, /docs prefixes/, /pricing, /downloadlayout: 'public' in definePageMetaProtected routes (authentication required):
/app/dashboard, /app/settings) → Protected (auth required)/app prefix patternThis boilerplate follows a common industry pattern where all authenticated user routes are grouped under the /app prefix:
/ → Public homepage
/blog → Public blog
/docs → Public documentation
/auth/login → Public authentication
/templates/dashboard → Public demo/template
/app/dashboard → Private: User dashboard
/app/settings → Private: User settings
/app/projects → Private: User projects
/app/* → Private: All authenticated routes
/app prefix?1. Single exclusion pattern
// In nuxt.config.ts sitemap
exclude: ['/app/**'] // One pattern protects everything
// In robots.txt
Disallow: /app/ // Simple and clear
2. Clear mental model
/app/* = Requires authentication3. Simplified middleware
// Easy check: does path start with /app/?
if (to.path.startsWith('/app/') && !session.value) {
return navigateTo('/auth/login')
}
4. Better organization All authenticated functionality is grouped together in your codebase:
app/pages/
├── app/ ← All private routes together
│ ├── dashboard/
│ ├── settings/
│ └── projects/
├── auth/ ← Authentication flows
├── blog/ ← Public content
└── index.vue ← Public pages
5. Industry standard This pattern is used by major SaaS platforms:
/dashboard/team/app (e.g., /app/billing, /app/team). They'll automatically be protected and excluded from search engines./templates (e.g., /templates/dashboard), not /app, so they're accessible without authentication./auth/loginIf you want to make a new route public, you have two options:
Option 1: Use the public layout in your page:
<script setup lang="ts">
definePageMeta({
layout: 'public',
})
</script>
Option 2: Add it to the public routes list in app/middleware/auth.global.ts:
const publicPrefixes = ['/auth/', '/blog/', '/checkout/', '/docs', '/your-route/']
// or
const exactPublicRoutes = ['/', '/pricing', '/download', '/your-route']
Once you've created a page, you'll likely want to add it to the navigation menu. Open app/components/header/MainHeader.vue and add your route to the navigationItems array:
import { User, Home, Users /* other icons */ } from 'lucide-vue-next'
const navigationItems = [
{
label: 'Dashboard',
icon: Home,
to: '/app/dashboard',
requiresAuth: true,
},
{
label: 'Team',
icon: Users,
to: '/team',
requiresAuth: true,
},
{
label: 'About',
icon: Info,
to: '/about',
requiresAuth: false,
},
// ... other items
]
Properties:
label - Text displayed in the navigation menuicon - Lucide icon componentto - Route pathrequiresAuth - Whether the link should only show to authenticated usersCreate pages with dynamic parameters using square brackets in the filename.
Create app/pages/blog/[slug].vue:
<script setup lang="ts">
const route = useRoute()
const slug = route.params.slug
// Fetch post data using the slug
// const post = await fetchPost(slug)
useSeoMeta({
title: `Blog post: ${slug}`,
})
</script>
<template>
<div class="base-container py-8">
<h1 class="text-4xl font-bold mb-4">Blog post: {{ slug }}</h1>
<p>Content for slug: {{ slug }}</p>
</div>
</template>
This creates routes like:
/blog/my-first-post/blog/nuxt-is-awesomeCreate app/pages/users/[id]/posts/[postId].vue:
<script setup lang="ts">
const route = useRoute()
const userId = route.params.id
const postId = route.params.postId
</script>
<template>
<div class="base-container py-8">
<h1>User {{ userId }}'s Post {{ postId }}</h1>
</div>
</template>
This creates routes like /users/123/posts/456.
Create app/pages/docs/[...slug].vue to match multiple path segments:
<script setup lang="ts">
const route = useRoute()
// slug will be an array: ['getting-started', 'installation']
const slug = route.params.slug
</script>
This matches:
/docs/getting-started/docs/getting-started/installation/docs/core-features/authenticationYou can specify which layout to use for a page:
<script setup lang="ts">
definePageMeta({
layout: 'public', // or 'default', 'auth', etc.
})
</script>
Available layouts in this boilerplate:
default - Standard layout with header and footer (used by default)public - Public pages layout (also has header/footer, but marks route as public)auth - Authentication pages layout (centered content, no header/footer)Always include SEO metadata for your pages:
<script setup lang="ts">
useSeoMeta({
title: 'Page title',
description: 'Page description for SEO',
ogTitle: 'Social media title',
ogDescription: 'Social media description',
ogImage: '/images/og-image.jpg',
twitterCard: 'summary_large_image',
})
</script>
useSeoMeta composable automatically handles meta tags for SEO and social media sharing.The boilerplate includes 170+ pre-built UI components from shadcn-vue. They're automatically imported:
<template>
<div class="base-container py-8">
<Card>
<CardHeader>
<CardTitle>My card</CardTitle>
<CardDescription>Card description</CardDescription>
</CardHeader>
<CardContent>
<p>Card content goes here</p>
</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
</div>
</template>
Keep your page components clean and focused:
<script setup lang="ts">
// 1. Composables and stores
const userStore = useUserStore()
const { user } = storeToRefs(userStore)
// 2. Data fetching
const { data: posts } = await useFetch('/api/posts')
// 3. Computed properties and reactive state
const greeting = computed(() => `Hello, ${user.value?.name}!`)
// 4. Methods
function handleAction() {
// ...
}
// 5. SEO metadata
useSeoMeta({
title: 'My page',
description: 'Page description',
})
</script>
<template>
<!-- Clean, semantic template -->
</template>
user-profile.vue, about-us.vueUserProfile, AboutUsUse the base-container class for consistent page width and padding:
<template>
<div class="base-container py-8">
<!-- Your content -->
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'public',
})
useSeoMeta({
title: 'Pricing',
description: 'View our pricing plans',
})
</script>
<template>
<div class="base-container py-12">
<h1 class="text-4xl font-bold text-center mb-8">Pricing plans</h1>
<!-- Pricing content -->
</div>
</template>
<script setup lang="ts">
const userStore = useUserStore()
const { user } = storeToRefs(userStore)
// Fetch user-specific data
const { data: stats } = await useFetch('/api/user/stats')
useSeoMeta({
title: 'Dashboard',
description: 'Your personal dashboard',
})
</script>
<template>
<div class="base-container py-8">
<h1 class="text-4xl font-bold mb-2">Welcome back, {{ user?.name }}!</h1>
<p class="text-muted-foreground mb-8">Here's your activity overview</p>
<!-- Dashboard content -->
<div class="grid grid-cols-3 gap-4">
<!-- Stats cards -->
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const productId = route.params.id
// Fetch product data
const { data: product } = await useFetch(`/api/products/${productId}`)
// Handle not found
if (!product.value) {
throw createError({ statusCode: 404, statusMessage: 'Product not found' })
}
useSeoMeta({
title: product.value.name,
description: product.value.description,
})
</script>
<template>
<div class="base-container py-8">
<h1 class="text-4xl font-bold mb-4">{{ product.name }}</h1>
<p class="text-lg text-muted-foreground mb-8">{{ product.description }}</p>
<!-- Product details -->
</div>
</template>
Now that you know how to create pages, explore related topics: