This is a demo site.Purchase now

Pages and routing

File-based routing, authentication middleware, and page creation in Nuxt

This guide covers creating pages with file-based routing, configuring authentication protection via middleware, and integrating pages into your navigation.

File-based routing

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/about
  • app/pages/blog/index.vue/blog
  • app/pages/blog/[slug].vue/blog/:slug (dynamic route)
  • app/pages/user/[id]/settings.vue/user/:id/settings
No route configuration needed! Just create the file and Nuxt handles the routing.

Creating a basic page

Let's create a simple "About" page. Create a new file at app/pages/about.vue:

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.

We're using layout: 'public' to make this page accessible without authentication. Without this, the global auth middleware would require users to log in first.

Creating a protected page

Protected pages require authentication. Let's create a team page that only authenticated users can access.

Create app/pages/team.vue:

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>
Pages are automatically protected by the global auth middleware (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.

Authentication protection rules

The boilerplate uses a global middleware that automatically protects routes. Here's how it works:

Public routes (no authentication required):

  • Routes with /auth/, /blog/, /checkout/, /docs prefixes
  • Exact routes: /, /pricing, /download
  • Any route with layout: 'public' in definePageMeta

Protected routes (authentication required):

  • All other routes (like /app/dashboard, /app/settings) → Protected (auth required)

Route organization: The /app prefix pattern

This 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

Why use the /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 authentication
  • Everything else = Public by default
  • Instant clarity for developers and search engines

3. 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:

  • Vercel uses /dashboard
  • Linear uses /team
  • GitHub uses Settings under authenticated area
  • Most modern SaaS apps group authenticated routes
When adding new authenticated features, create them under /app (e.g., /app/billing, /app/team). They'll automatically be protected and excluded from search engines.
Public demo/template pages go under /templates (e.g., /templates/dashboard), not /app, so they're accessible without authentication.
  • Unauthenticated users are automatically redirected to /auth/login

Making a route public

If 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']

Adding to navigation

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:

app/components/header/MainHeader.vue
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 menu
  • icon - Lucide icon component
  • to - Route path
  • requiresAuth - Whether the link should only show to authenticated users

Dynamic routes

Create pages with dynamic parameters using square brackets in the filename.

Single parameter

Create app/pages/blog/[slug].vue:

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-awesome

Multiple parameters

Create 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.

Catch-all routes

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/authentication

Page layouts

You 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)

Page metadata and SEO

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>
The useSeoMeta composable automatically handles meta tags for SEO and social media sharing.

Using shadcn-vue components

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>
Learn more about available UI components in the UI components guide.

Best practices

Page structure

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>

Naming conventions

  • Files: Use kebab-case for page files: user-profile.vue, about-us.vue
  • Components: Use PascalCase for component names: UserProfile, AboutUs
  • Titles: Use sentence case for headings: "User profile" not "User Profile"

Container classes

Use the base-container class for consistent page width and padding:

<template>
  <div class="base-container py-8">
    <!-- Your content -->
  </div>
</template>

Examples

Simple public page

app/pages/pricing.vue
<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>

Protected page with data fetching

app/pages/app/dashboard.vue
<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>

Dynamic route with data

app/pages/products/[id].vue
<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>

Next steps

Now that you know how to create pages, explore related topics:

File-based routing

Deep dive into Nuxt's routing system.

Layouts

Learn about using and creating layouts.

UI components

Explore the shadcn-vue component library.

Authentication

Understand how authentication protection works.