Adding Pages
How to create new pages, add routes, and integrate them into the sidebar navigation.
How File-Based Routing Works
Nuxt uses file-based routing — every .vue file you create in the app/pages/ directory automatically becomes a route. There is no manual router configuration. The file path determines the URL:
| File Path | URL |
|---|---|
| pages/dashboard/reports.vue | /dashboard/reports |
| pages/orders/[id].vue | /orders/123 |
| pages/orders/[id]/edit.vue | /orders/123/edit |
| pages/index.vue | / |
Creating a New Dashboard Page
Create a new file at app/pages/dashboard/reports.vue. The page will immediately be available at /dashboard/reports and will automatically receive the default layout (sidebar + header):
<template>
<div>
<SharedPageHeader title="Reports" description="View analytics reports" />
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<SharedGlassCard title="Monthly Revenue" hoverable>
<p class="text-sm text-[var(--haze-text-secondary)]">
Revenue breakdown for the current month.
</p>
</SharedGlassCard>
<SharedGlassCard title="User Growth" hoverable>
<p class="text-sm text-[var(--haze-text-secondary)]">
New user signups over time.
</p>
</SharedGlassCard>
</div>
</div>
</template> That is all you need. The SharedPageHeader and SharedGlassCard components are auto-imported — no import statements required.
Dynamic Routes
Use square brackets in filenames for dynamic route segments. The parameter is accessible via useRoute().params:
<!-- File: app/pages/orders/[id].vue -->
<!-- URL: /orders/123 -->
<template>
<div>
<SharedPageHeader :title="'Order #' + route.params.id" />
<SharedGlassCard>
<p class="text-sm text-[var(--haze-text-secondary)]">
Order details for {{ route.params.id }}
</p>
</SharedGlassCard>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
</script> For nested dynamic routes like /orders/123/edit, create the file at pages/orders/[id]/edit.vue.
Page Meta & Layout Assignment
All pages use the default layout (sidebar + header) unless you specify otherwise. Use definePageMeta to assign a different layout or add middleware:
<script setup lang="ts">
// Use the auth layout instead of default (sidebar)
definePageMeta({
layout: 'auth',
})
</script>
<template>
<div class="mx-auto max-w-md">
<h1>Login</h1>
<!-- login form -->
</div>
</template>Available Layouts
| Layout | When to Use |
|---|---|
| default | Dashboard pages, CRUD pages, settings (automatic, no config needed) |
| horizontal | Alternative dashboard layout with top navigation bar |
| auth | Classic login, register, forgot password |
| auth-split | Login/register with a split branding panel |
| auth-cover | Login/register with full-screen gradient background |
| marketing | Public-facing pages (landing, about, contact, FAQ, blog) |
| blank | Error pages, maintenance, coming soon |
Adding to Sidebar Navigation
To show your new page in the sidebar, open app/utils/navigation.ts and add an entry to the appropriate navigation group:
// app/utils/navigation.ts
export const navigation: NavGroup[] = [
{
label: 'Dashboards',
items: [
{ label: 'Overview', icon: 'i-lucide-layout-dashboard', to: '/dashboard' },
{ label: 'Analytics', icon: 'i-lucide-bar-chart-3', to: '/dashboard/analytics' },
// Add your new page here:
{ label: 'Reports', icon: 'i-lucide-file-bar-chart', to: '/dashboard/reports' },
],
},
// ... other groups
] Icons use the i-lucide-* prefix from the Lucide icon set. The sidebar renders all groups and items automatically.
Fetching Data
Use Nuxt's useFetch composable to load data from server API routes. This works during both SSR and client-side navigation:
<script setup lang="ts">
// Fetch paginated orders from the mock API
const page = ref(1)
const search = ref('')
const { data, pending } = useFetch('/api/orders', {
query: { page, per_page: 10, search },
watch: [page, search],
})
</script>
<template>
<div>
<input v-model="search" placeholder="Search orders..." />
<div v-if="pending">Loading...</div>
<div v-else>
<div v-for="order in data?.data" :key="order.id">
{{ order.orderNumber }}
</div>
<p>Page {{ data?.meta.page }} of {{ data?.meta.lastPage }}</p>
</div>
</div>
</template> The mock API routes at /api/* support query parameters for page, per_page, search, status, sort, and order.
Full Example: Adding a Tasks Page
Here is a complete example that creates a tasks page with a data table, fetches from an API, and includes search and status filtering:
<!-- File: app/pages/tasks.vue -->
<!-- URL: /tasks -->
<template>
<div>
<SharedPageHeader title="Tasks" description="Manage your team's tasks">
<template #actions>
<UButton icon="i-lucide-plus" label="New Task" />
</template>
</SharedPageHeader>
<SharedGlassCard>
<div class="flex gap-3 mb-4">
<UInput v-model="search" placeholder="Search tasks..." icon="i-lucide-search" class="flex-1" />
<USelect v-model="status" :options="['all', 'pending', 'in_progress', 'completed']" />
</div>
<div v-if="pending" class="py-8 text-center text-sm text-[var(--haze-text-secondary)]">
Loading...
</div>
<div v-else-if="!data?.data.length">
<SharedEmptyState title="No tasks found" description="Try adjusting your search or filters." />
</div>
<div v-else class="space-y-2">
<div v-for="task in data.data" :key="task.id"
class="flex items-center justify-between rounded-lg border border-divider p-3">
<span>{{ task.title }}</span>
<SharedStatusBadge :status="task.status" />
</div>
</div>
</SharedGlassCard>
</div>
</template>
<script setup lang="ts">
const search = ref('')
const status = ref('all')
const { data, pending } = useFetch('/api/tasks', {
query: {
search,
status: computed(() => status.value === 'all' ? undefined : status.value),
},
watch: [search, status],
})
</script>Tip
You can scaffold a new page quickly with the Nuxt CLI: npx nuxi add page dashboard/reports. This creates the file with a basic template. Then just add your content and sidebar navigation entry.