Creating Custom Pages in Filament v4 to Display Single Records
Overview
This guide explains how to create custom pages in Filament v4 that display a single, predetermined record. This is useful when you need to show specific data that doesn't fit the standard CRUD pattern, such as:
- Featured employee/user profiles
- System configuration displays
- Dashboard-style pages with specific data
- Report pages for particular records
Prerequisites
- Laravel application with Filament v4 installed
- Basic understanding of Laravel Blade templates
- Familiarity with Eloquent models
Step 1: Generate the Custom Page
Use Artisan to generate a new Filament page:
php artisan make:filament-page BestEmployeeEver
This command creates two files:
app/Filament/Pages/BestEmployeeEver.php- The page classresources/views/filament/pages/best-employee-ever.blade.php- The view template
<note tip> If you're using a custom panel (not the default admin panel), add the panel name:
php artisan make:filament-page BestEmployeeEver --panel=admin
</note>
Step 2: Configure the Page Class
Open app/Filament/Pages/BestEmployeeEver.php and configure it:
<?php namespace App\Filament\Pages; use Filament\Pages\Page; use App\Models\User; class BestEmployeeEver extends Page { // Icon displayed in navigation (uses Heroicons) protected static ?string $navigationIcon = 'heroicon-o-star'; // Path to the Blade view file protected static string $view = 'filament.pages.best-employee-ever'; // Label shown in navigation menu protected static ?string $navigationLabel = 'Best Employee'; // Page title displayed at the top protected static ?string $title = 'Best Employee Ever'; // Optional: Group pages together in navigation protected static ?string $navigationGroup = 'Employees'; // Optional: Control navigation order (lower numbers appear first) protected static ?int $navigationSort = 1; // Public property to hold the employee data public ?User $employee = null; /** * Mount method runs when the page loads * This is where you fetch your data */ public function mount(): void { // Fetch the specific employee by ID $this->employee = User::find(42); // Replace with your ID // Handle case where employee doesn't exist if (!$this->employee) { abort(404, 'Employee not found'); } } /** * Optional: Pass data to the view * Use this if you need additional processing */ protected function getViewData(): array { return [ 'employee' => $this->employee, // Add any other computed data here ]; } }
Understanding the Page Class Properties
| Property | Type | Purpose |
|---|---|---|
$navigationIcon | string | Icon from Heroicons (heroicon-o-* for outline, heroicon-s-* for solid) |
$view | string | Path to Blade template (dots replace slashes) |
$navigationLabel | string | Text shown in sidebar navigation |
$title | string | Page heading displayed at top |
$navigationGroup | string | Groups related pages in navigation |
$navigationSort | int | Controls order in navigation (ascending) |
Step 3: Create the Blade View
Edit resources/views/filament/pages/best-employee-ever.blade.php:
<x-filament-panels::page> <div class="space-y-6"> {{-- Hero Section with Employee Photo/Avatar --}} <div class="fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10"> <div class="fi-section-content p-6"> <div class="flex items-center gap-4"> @if($employee->avatar ?? null) <img src="{{ $employee->avatar }}" alt="{{ $employee->name }}" class="h-24 w-24 rounded-full object-cover"> @else <div class="flex h-24 w-24 items-center justify-center rounded-full bg-primary-500 text-3xl font-bold text-white"> {{ substr($employee->name, 0, 1) }} </div> @endif <div class="flex-1"> <h2 class="text-2xl font-bold text-gray-950 dark:text-white"> {{ $employee->name }} </h2> <p class="text-sm text-gray-500 dark:text-gray-400"> {{ $employee->email }} </p> </div> </div> </div> </div> {{-- Two Column Layout for Details --}} <div class="grid gap-6 md:grid-cols-2"> {{-- Personal Information Card --}} <div class="fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10"> <div class="fi-section-header flex items-center gap-x-3 px-6 py-4"> <h3 class="text-base font-semibold text-gray-950 dark:text-white"> Personal Information </h3> </div> <div class="fi-section-content p-6 pt-0"> <dl class="space-y-4"> <div> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Full Name</dt> <dd class="mt-1 text-sm text-gray-950 dark:text-white">{{ $employee->name }}</dd> </div> <div> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Email</dt> <dd class="mt-1 text-sm text-gray-950 dark:text-white">{{ $employee->email }}</dd> </div> @if($employee->phone ?? null) <div> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Phone</dt> <dd class="mt-1 text-sm text-gray-950 dark:text-white">{{ $employee->phone }}</dd> </div> @endif @if($employee->department ?? null) <div> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Department</dt> <dd class="mt-1 text-sm text-gray-950 dark:text-white">{{ $employee->department }}</dd> </div> @endif </dl> </div> </div> {{-- Statistics Card --}} <div class="fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10"> <div class="fi-section-header flex items-center gap-x-3 px-6 py-4"> <h3 class="text-base font-semibold text-gray-950 dark:text-white"> Statistics </h3> </div> <div class="fi-section-content p-6 pt-0"> <dl class="space-y-4"> <div> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Member Since</dt> <dd class="mt-1 text-sm text-gray-950 dark:text-white"> {{ $employee->created_at->format('F j, Y') }} </dd> </div> <div> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Years of Service</dt> <dd class="mt-1 text-sm text-gray-950 dark:text-white"> {{ $employee->created_at->diffInYears(now()) }} years </dd> </div> <div> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Projects</dt> <dd class="mt-1 text-sm text-gray-950 dark:text-white"> {{ $employee->projects->count() ?? 0 }} </dd> </div> </dl> </div> </div> </div> {{-- Full Width Description Section --}} <div class="fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10"> <div class="fi-section-header flex items-center gap-x-3 px-6 py-4"> <h3 class="text-base font-semibold text-gray-950 dark:text-white"> Why This Employee is the Best </h3> </div> <div class="fi-section-content p-6 pt-0"> <p class="text-sm text-gray-700 dark:text-gray-300"> {{ $employee->bio ?? 'This employee has demonstrated exceptional performance, dedication, and commitment to excellence.' }} </p> </div> </div> </div> </x-filament-panels::page>
Understanding the Blade Template
Main Wrapper
<x-filament-panels::page> <!-- Your content here --> </x-filament-panels::page>
This component provides the standard Filament page layout with proper styling and dark mode support.
Filament CSS Classes
Filament uses specific CSS classes that integrate with its design system:
| Class | Purpose |
|---|---|
fi-section | Container for content sections |
fi-section-header | Header area of a section |
fi-section-content | Content area of a section |
space-y-6 | Vertical spacing between elements |
dark:bg-gray-900 | Dark mode background |
dark:text-white | Dark mode text color |
Step 4: Advanced Configurations
Hide from Navigation
If you want the page accessible via URL but not shown in the sidebar:
protected static bool $shouldRegisterNavigation = false;
Custom URL Slug
Change the URL path for the page:
protected static string $slug = 'our-best-employee'; // Access via: /admin/our-best-employee
Add Navigation Badge
Show a badge next to the navigation item:
public static function getNavigationBadge(): ?string { return 'Featured'; } protected static ?string $navigationBadgeColor = 'success';
Restrict Access with Policies
Control who can view the page:
public static function canAccess(): bool { return auth()->user()->can('view_best_employee'); }
Step 5: Dynamic Employee ID Configuration
Instead of hardcoding the employee ID, use environment configuration:
Method 1: Environment Variable
Add to .env:
BEST_EMPLOYEE_ID=42
In your page class:
public function mount(): void { $employeeId = env('BEST_EMPLOYEE_ID', 1); $this->employee = User::find($employeeId); }
Method 2: Config File
Add to config/app.php:
'best_employee_id' => env('BEST_EMPLOYEE_ID', 1),
In your page class:
public function mount(): void { $this->employee = User::find(config('app.best_employee_id')); }
Method 3: Database Setting
Create a settings table and fetch dynamically:
public function mount(): void { $employeeId = Setting::where('key', 'best_employee_id')->value('value'); $this->employee = User::find($employeeId); }
Common Customizations
Display Related Data
Show related models (e.g., projects, tasks):
public function mount(): void { $this->employee = User::with(['projects', 'tasks', 'achievements']) ->find(42); }
In your view:
@if($employee->projects->isNotEmpty()) <div class="fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5"> <div class="fi-section-header flex items-center gap-x-3 px-6 py-4"> <h3 class="text-base font-semibold">Recent Projects</h3> </div> <div class="fi-section-content p-6 pt-0"> <ul class="space-y-2"> @foreach($employee->projects as $project) <li class="text-sm">{{ $project->name }}</li> @endforeach </ul> </div> </div> @endif
Add Computed Properties
Calculate data on-the-fly:
public function getCompletedProjectsProperty(): int { return $this->employee->projects() ->where('status', 'completed') ->count(); }
Access in view:
{{ $this->completedProjects }}
Add Actions/Buttons
Include interactive buttons:
// In your page class use Filament\Actions\Action; protected function getHeaderActions(): array { return [ Action::make('sendCongratulations') ->label('Send Congratulations') ->icon('heroicon-o-envelope') ->action(function () { // Send email logic Notification::make() ->title('Congratulations sent!') ->success() ->send(); }), ]; }
Troubleshooting
Page Not Appearing in Navigation
Check:
$shouldRegisterNavigationis not set tofalse- User has proper permissions (
canAccess()method) - Cache is cleared:
php artisan filament:cache-components
404 Error When Accessing Page
- Verify the view file path matches the
$viewproperty - Ensure the view file exists in the correct directory
- Check file naming (kebab-case in filesystem, dot notation in class)
Styling Issues
- Always wrap content in
<x-filament-panels::page> - Use Filament's CSS classes (
fi-section, etc.) for consistency - Check dark mode classes are included (
dark:*)
Data Not Loading
- Verify the
mount()method is public - Check database connection and model relationships
- Add error handling for missing records
Complete Working Example
Here's a full working example for a “Company Statistics” page:
Page Class: app/Filament/Pages/CompanyStats.php
<?php namespace App\Filament\Pages; use Filament\Pages\Page; use App\Models\Company; use App\Models\Order; class CompanyStats extends Page { protected static ?string $navigationIcon = 'heroicon-o-chart-bar'; protected static string $view = 'filament.pages.company-stats'; protected static ?string $title = 'Company Statistics'; public ?Company $company = null; public int $totalOrders = 0; public float $totalRevenue = 0; public function mount(): void { // Get the main company record (ID = 1) $this->company = Company::find(1); if (!$this->company) { abort(404, 'Company not found'); } // Calculate statistics $this->totalOrders = Order::count(); $this->totalRevenue = Order::sum('total_amount'); } }
View: resources/views/filament/pages/company-stats.blade.php
<x-filament-panels::page> <div class="grid gap-6 md:grid-cols-3"> <!-- Company Name Card --> <div class="fi-section rounded-xl bg-white p-6 shadow-sm ring-1 ring-gray-950/5"> <h3 class="text-sm font-medium text-gray-500">Company Name</h3> <p class="mt-2 text-2xl font-bold text-gray-950">{{ $company->name }}</p> </div> <!-- Total Orders Card --> <div class="fi-section rounded-xl bg-white p-6 shadow-sm ring-1 ring-gray-950/5"> <h3 class="text-sm font-medium text-gray-500">Total Orders</h3> <p class="mt-2 text-2xl font-bold text-gray-950">{{ number_format($totalOrders) }}</p> </div> <!-- Revenue Card --> <div class="fi-section rounded-xl bg-white p-6 shadow-sm ring-1 ring-gray-950/5"> <h3 class="text-sm font-medium text-gray-500">Total Revenue</h3> <p class="mt-2 text-2xl font-bold text-gray-950">${{ number_format($totalRevenue, 2) }}</p> </div> </div> </x-filament-panels::page>
Additional Resources
Summary
To create a custom page displaying a single record in Filament v4:
- Generate page with
php artisan make:filament-page - Configure the page class with navigation settings
- Fetch your specific record in the
mount()method - Create a Blade view using Filament's component and CSS classes
- Access the page via the navigation menu or direct URL
This pattern works for any scenario where you need to display predetermined, specific data outside of standard CRUD operations.

