Custom Pages
Custom Pages
Add custom pages to your admin panel for features beyond standard CRUD operations.
Overview
Custom pages allow you to add specialized admin functionality like:
- Analytics dashboards
- Report generators
- Settings pages
- Custom workflows
- Data import/export interfaces
Configuration
Add custom pages in your nuxt.config.ts:
export default defineNuxtConfig({
autoAdmin: {
customPages: [
{
name: 'analytics',
label: 'Analytics',
path: 'analytics',
icon: 'i-heroicons-chart-bar',
group: 'Reports',
order: 1,
},
{
name: 'settings',
label: 'Settings',
path: 'settings',
icon: 'i-heroicons-cog-6-tooth',
order: 999,
},
],
},
})
Page Options
name
Unique identifier for the page:
{
name: 'analytics', // Must be unique
}
label
Display text shown in the sidebar:
{
label: 'Analytics Dashboard',
}
path
URL path relative to admin prefix:
{
path: 'analytics', // Accessible at /admin/analytics
}
For absolute paths, use a leading slash:
{
path: '/custom-admin/analytics', // Absolute path
}
icon
Heroicon name for sidebar:
{
icon: 'i-heroicons-chart-bar',
}
Browse icons at: https://heroicons.com/
group
Organize custom pages into sidebar groups:
{
group: 'Reports', // Grouped with other report pages
}
Pages without a group appear in the ungrouped section.
order
Control display order within groups:
{
order: 1, // Lower numbers appear first
}
Creating Page Components
Create a Vue component in your Nuxt pages directory:
# For path: 'analytics'
touch app/pages/admin/analytics.vue
<!-- app/pages/admin/analytics.vue -->
<template>
<div class="space-y-4">
<div>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
Analytics Dashboard
</h1>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
View your site statistics
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<UCard>
<div class="text-center">
<div class="text-3xl font-bold">{{ stats.users }}</div>
<div class="text-sm text-gray-500">Total Users</div>
</div>
</UCard>
<UCard>
<div class="text-center">
<div class="text-3xl font-bold">{{ stats.posts }}</div>
<div class="text-sm text-gray-500">Total Posts</div>
</div>
</UCard>
<UCard>
<div class="text-center">
<div class="text-3xl font-bold">{{ stats.comments }}</div>
<div class="text-sm text-gray-500">Total Comments</div>
</div>
</UCard>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'admin', // Use admin layout
})
const stats = ref({
users: 0,
posts: 0,
comments: 0,
})
onMounted(async () => {
// Fetch stats from API
const [users, posts, comments] = await Promise.all([
$fetch('/api/users', { query: { aggregate: 'count' } }),
$fetch('/api/posts', { query: { aggregate: 'count' } }),
$fetch('/api/comments', { query: { aggregate: 'count' } }),
])
stats.value = {
users: users.meta.aggregates.count,
posts: posts.meta.aggregates.count,
comments: comments.meta.aggregates.count,
}
})
</script>
Important: Always include definePageMeta({ layout: 'admin' }) to use the admin layout.
Permission Control
Using permission strings
permissions names what the caller must be allowed to do through the API, as '<resource>:<action>'
(create, read, update, delete, restore, purge) — checked against GET /api/permissions. The
<resource> must be a registered resource; for anything else use a route middleware (below):
{
name: 'settings',
label: 'Settings',
path: 'settings',
icon: 'i-heroicons-cog-6-tooth',
permissions: 'users:update', // callers who may update users
}
Or require several:
{
permissions: ['orders:read', 'customers:read'], // the caller needs ALL
}
An entry in any other form grants nothing — a typo hides the page instead of opening it.
Any other rule: a route middleware
For a rule that is not an API permission (a role, a feature flag), put a route middleware on the page itself — the page is yours:
<script setup lang="ts">
definePageMeta({ layout: 'admin', middleware: 'staff-only' })
</script>
A canAccess: (user) => … function in nuxt.config could never reach the running app (runtime config is
JSON), so it was never applied; it now fails the build.
Sidebar Behavior
Custom pages respect the unauthorizedSidebarItems setting:
autoAdmin: {
permissions: {
unauthorizedSidebarItems: 'hide', // or 'disable'
},
}
- 'hide' - Pages without access are removed from sidebar
- 'disable' - Pages without access appear grayed out
Navigation Protection
The global middleware automatically protects custom pages:
- Checks the page's
permissionsbefore allowing access - Returns 403 Forbidden if unauthorized
- Shows nice error page with navigation options
Complete Examples
Analytics Dashboard
// nuxt.config.ts
customPages: [
{
name: 'analytics',
label: 'Analytics',
path: 'analytics',
icon: 'i-heroicons-chart-bar',
group: 'Reports',
permissions: ['orders:read', 'customers:read'],
},
]
Settings Page
// nuxt.config.ts
customPages: [
{
name: 'settings',
label: 'Settings',
path: 'settings',
icon: 'i-heroicons-cog-6-tooth',
order: 999, // Show at bottom
permissions: 'users:update',
},
]
<!-- app/pages/admin/settings.vue -->
<template>
<div class="space-y-4">
<h1 class="text-2xl font-semibold">Settings</h1>
<UCard>
<UForm :state="settings" @submit="saveSettings">
<div class="space-y-4">
<UFormGroup label="Site Name">
<UInput v-model="settings.siteName" />
</UFormGroup>
<UFormGroup label="Contact Email">
<UInput v-model="settings.contactEmail" type="email" />
</UFormGroup>
<UFormGroup label="Maintenance Mode">
<UToggle v-model="settings.maintenanceMode" />
</UFormGroup>
<UButton type="submit">Save Settings</UButton>
</div>
</UForm>
</UCard>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'admin',
})
const settings = ref({
siteName: '',
contactEmail: '',
maintenanceMode: false,
})
onMounted(async () => {
const data = await $fetch('/api/settings')
settings.value = data
})
async function saveSettings() {
await $fetch('/api/settings', {
method: 'PUT',
body: settings.value,
})
// Show success notification
}
</script>
Data Export Page
(Every resource list already has an Export button — see Features. A page like this one exports several resources from one place.)
// nuxt.config.ts
customPages: [
{
name: 'export',
label: 'Export Data',
path: 'export',
icon: 'i-heroicons-arrow-down-tray',
group: 'Tools',
permissions: 'orders:read',
},
]
<!-- app/pages/admin/export.vue -->
<template>
<div class="space-y-4">
<h1 class="text-2xl font-semibold">Export Data</h1>
<UCard>
<div class="space-y-4">
<UFormGroup label="Select Resource">
<USelect
v-model="selectedResource"
:options="resources"
/>
</UFormGroup>
<UFormGroup label="Format">
<USelect
v-model="format"
:options="['csv', 'json']"
/>
</UFormGroup>
<UButton
:loading="isExporting"
@click="exportData"
>
Export
</UButton>
</div>
</UCard>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'admin',
})
const selectedResource = ref('users')
const format = ref('csv')
const isExporting = ref(false)
const resources = ['users', 'posts', 'comments']
async function exportData() {
isExporting.value = true
try {
// GET /api/{resource}/export — added by createExportPlugin
const blob = await $fetch<Blob>(`/api/${selectedResource.value}/export`, {
query: { format: format.value },
responseType: 'blob',
})
// Download file
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${selectedResource.value}.${format.value}`
a.click()
} finally {
isExporting.value = false
}
}
</script>
Best Practices
- Always use admin layout - Include
definePageMeta({ layout: 'admin' }) - Declare permissions -
permissions: ['<resource>:<action>'], or a route middleware on the page - Follow naming conventions - Use kebab-case for paths
- Group related pages - Use groups for better organization
- Provide clear labels - Make purpose obvious in sidebar
- Handle loading states - Show loading indicators
- Error handling - Handle API errors gracefully
Accessing Admin Data
Custom pages can access all admin composables:
<script setup>
// Get current user permissions
const { canCreate, canUpdate } = useAdminPermissions('posts')
// Access admin configuration
const { branding } = useAdminConfig()
// Get resource schemas
const { resource } = useAdminResource('users')
// Use admin actions
const { goToList, goToCreate } = useAdminActions('posts')
</script>
Next Steps
- M2M Relationships - Manage complex relations
- Custom Actions - Add custom resource actions
- Composables - Available composables reference
Permissions
This document explains the comprehensive permission system implemented in Nuxt Auto Admin.
M2M Relationships
This guide covers how M2M relationships work in the auto-admin interface.