Custom Pages

Add custom pages to your admin panel for features beyond standard CRUD operations.

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 canAccess Function

For complex permission logic:

{
  name: 'analytics',
  label: 'Analytics',
  path: 'analytics',
  icon: 'i-heroicons-chart-bar',
  canAccess: async (user) => {
    // Check if user has permission
    return user?.role === 'admin' || user?.permissions?.includes('analytics.view')
  },
}

The function receives the current user and should return a boolean (or Promise).

Using Permission Strings

For simple permission checks:

{
  name: 'settings',
  label: 'Settings',
  path: 'settings',
  icon: 'i-heroicons-cog-6-tooth',
  permissions: 'admin',  // Single permission
}

Or require multiple permissions:

{
  permissions: ['admin', 'settings.manage'],  // User needs ALL
}

Note: Permission string validation is a planned feature. Currently, use canAccess for actual permission checking.

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

The global middleware automatically protects custom pages:

  • Checks canAccess function before 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',
    canAccess: async (user) => {
      return user?.permissions?.includes('analytics.view')
    },
  },
]

Settings Page

// nuxt.config.ts
customPages: [
  {
    name: 'settings',
    label: 'Settings',
    path: 'settings',
    icon: 'i-heroicons-cog-6-tooth',
    order: 999,  // Show at bottom
    canAccess: async (user) => {
      return user?.role === 'admin'
    },
  },
]
<!-- 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

// nuxt.config.ts
customPages: [
  {
    name: 'export',
    label: 'Export Data',
    path: 'export',
    icon: 'i-heroicons-arrow-down-tray',
    group: 'Tools',
    canAccess: async (user) => {
      return user?.permissions?.includes('data.export')
    },
  },
]
<!-- 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', 'Excel']"
          />
        </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 {
    const blob = await $fetch(`/api/export/${selectedResource.value}`, {
      query: { format: format.value },
    })
    // Download file
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `${selectedResource.value}.${format.value.toLowerCase()}`
    a.click()
  } finally {
    isExporting.value = false
  }
}
</script>

Best Practices

  1. Always use admin layout - Include definePageMeta({ layout: 'admin' })
  2. Implement permission checks - Use canAccess for security
  3. Follow naming conventions - Use kebab-case for paths
  4. Group related pages - Use groups for better organization
  5. Provide clear labels - Make purpose obvious in sidebar
  6. Handle loading states - Show loading indicators
  7. 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

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.