Translations of this page:
  • en

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 class
  • resources/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

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:

  • $shouldRegisterNavigation is not set to false
  • 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 $view property
  • 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:

  1. Generate page with php artisan make:filament-page
  2. Configure the page class with navigation settings
  3. Fetch your specific record in the mount() method
  4. Create a Blade view using Filament's component and CSS classes
  5. 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.

Edit this page
Back to top