diff --git a/resources/views/errors/page.blade.php b/resources/views/errors/page.blade.php
new file mode 100644
index 0000000..12de456
--- /dev/null
+++ b/resources/views/errors/page.blade.php
@@ -0,0 +1,70 @@
+@php
+ $pages = [
+ 401 => ['title' => 'Authentication required', 'text' => 'Sign in to continue to the admin area.', 'icon' => 'lock'],
+ 403 => ['title' => 'Access denied', 'text' => 'You do not have permission to view this page.', 'icon' => 'shield'],
+ 404 => ['title' => 'Page not found', 'text' => 'This admin page may have moved, or the address may be incorrect.', 'icon' => 'search'],
+ 419 => ['title' => 'Session expired', 'text' => 'Your session has expired. Refresh the page and try again.', 'icon' => 'clock'],
+ 429 => ['title' => 'Too many requests', 'text' => 'You have made too many requests. Please try again shortly.', 'icon' => 'clock'],
+ 500 => ['title' => 'Something went wrong', 'text' => 'An unexpected error occurred. Please try again later.', 'icon' => 'alert'],
+ 503 => ['title' => 'Temporarily unavailable', 'text' => 'The admin area is temporarily unavailable. Please try again soon.', 'icon' => 'alert'],
+ ];
+ $page = $pages[$status] ?? $pages[500];
+@endphp
+
+
+
+
+
+
+ {{ $status }} · {{ $page['title'] }} · {{ config('app.name', 'Lazy Admin') }}
+
+
+
+{{ config('app.name', 'Lazy Admin') }}/ Admin
+
+
+ @if($page['icon'] === 'search')
+
+ @elseif($page['icon'] === 'lock')
+
+ @elseif($page['icon'] === 'shield')
+
+ @elseif($page['icon'] === 'clock')
+
+ @else
+
+ @endif
+
+ {{ $status }}
+ {{ $page['title'] }}
+ {{ $page['text'] }}
+
+
+
+
+
+
diff --git a/src/Exceptions/AdminErrorPages.php b/src/Exceptions/AdminErrorPages.php
new file mode 100644
index 0000000..0c8d9dd
--- /dev/null
+++ b/src/Exceptions/AdminErrorPages.php
@@ -0,0 +1,68 @@
+expectsJson() || $request->is('livewire/*') || ! $this->isAdminRequest($request)) {
+ return null;
+ }
+
+ if (! $exception instanceof HttpExceptionInterface) {
+ return null;
+ }
+
+ $status = $exception->getStatusCode();
+
+ if (! in_array($status, [401, 403, 404, 419, 429, 500, 503], true)) {
+ return null;
+ }
+
+ return response()->view('lazy::errors.page', [
+ 'status' => $status,
+ 'homeUrl' => url('/'.trim((string) config('lazy.admin.route.prefix', 'admin'), '/')),
+ ], $status, $exception->getHeaders());
+ }
+
+ private function isAdminRequest(Request $request): bool
+ {
+ $domain = config('lazy.admin.route.domain');
+
+ if (is_string($domain) && $domain !== '' && $request->getHost() !== $domain) {
+ return false;
+ }
+
+ $routeName = $request->route()?->getName();
+ $namePrefix = trim((string) config('lazy.admin.route.name', 'admin.'), '.');
+
+ if (is_string($routeName) && $namePrefix !== '' &&
+ ($routeName === $namePrefix || str_starts_with($routeName, $namePrefix.'.'))) {
+ return true;
+ }
+
+ $prefix = trim((string) config('lazy.admin.route.prefix', 'admin'), '/');
+
+ if ($prefix === '') {
+ return false;
+ }
+
+ $segments = explode('/', trim($request->decodedPath(), '/'));
+ $prefixSegments = explode('/', $prefix);
+
+ if (array_slice($segments, 0, count($prefixSegments)) === $prefixSegments) {
+ return true;
+ }
+
+ // Permit a locale before the admin prefix on a missing route.
+ return count($segments) > count($prefixSegments)
+ && preg_match('/^[a-z]{2}(?:-[A-Za-z]{2})?$/', $segments[0]) === 1
+ && array_slice($segments, 1, count($prefixSegments)) === $prefixSegments;
+ }
+}
diff --git a/src/LazyAdminServiceProvider.php b/src/LazyAdminServiceProvider.php
index 034f50f..7aa9fa2 100644
--- a/src/LazyAdminServiceProvider.php
+++ b/src/LazyAdminServiceProvider.php
@@ -2,6 +2,7 @@
namespace Step2dev\LazyAdmin;
+use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Auth;
@@ -38,6 +39,7 @@
use Step2dev\LazyAdmin\Controllers\UserController;
use Step2dev\LazyAdmin\Dashboard\DashboardRegistry;
use Step2dev\LazyAdmin\Database\Seeders\DatabaseSeeder;
+use Step2dev\LazyAdmin\Exceptions\AdminErrorPages;
use Step2dev\LazyAdmin\Facades\Route as RouteFacade;
use Step2dev\LazyAdmin\Http\Livewire\Activity\Page as ActivityPage;
use Step2dev\LazyAdmin\Http\Livewire\Dashboard\Page as DashboardPage;
@@ -203,6 +205,12 @@ public function bootingPackage(): void
public function packageBooted(): void
{
+ $handler = $this->app->make(ExceptionHandler::class);
+
+ if (method_exists($handler, 'renderable')) {
+ $handler->renderable(new AdminErrorPages);
+ }
+
View::composer('lazy::header', static function ($view): void {
$guard = (string) config('lazy.auth.guard', 'web');
$user = Auth::guard($guard)->user();
diff --git a/tests/Feature/AdminErrorPagesTest.php b/tests/Feature/AdminErrorPagesTest.php
new file mode 100644
index 0000000..f25bd97
--- /dev/null
+++ b/tests/Feature/AdminErrorPagesTest.php
@@ -0,0 +1,40 @@
+set('lazy.admin.route.prefix', 'admin');
+
+ $render = new AdminErrorPages;
+
+ $admin = $render(new HttpException(404), Request::create('/admin/missing'));
+ expect($admin)->not->toBeNull()
+ ->and($admin->getStatusCode())->toBe(404)
+ ->and($admin->getContent())->toContain('Page not found');
+
+ expect($render(new HttpException(404), Request::create('/public/missing')))->toBeNull();
+ expect($render(new HttpException(404), Request::create('/administrator/missing')))->toBeNull();
+ expect($render(new HttpException(404), Request::create('/admin/missing', 'GET', [], [], [], ['HTTP_ACCEPT' => 'application/json'])))->toBeNull();
+ expect($render(new HttpException(404), Request::create('/livewire/update')))->toBeNull();
+});
+
+it('supports localized admin paths and custom prefixes', function (): void {
+ config()->set('lazy.admin.route.prefix', 'control');
+
+ $render = new AdminErrorPages;
+
+ expect($render(new HttpException(403), Request::create('/uk/control/private'))?->getStatusCode())->toBe(403)
+ ->and($render(new HttpException(503), Request::create('/control/offline'))?->getStatusCode())->toBe(503)
+ ->and($render(new HttpException(404), Request::create('/uk/admin/missing')))->toBeNull();
+});
+
+it('honors a configured admin domain', function (): void {
+ config()->set('lazy.admin.route.domain', 'admin.example.test');
+
+ $render = new AdminErrorPages;
+
+ expect($render(new HttpException(404), Request::create('https://example.test/admin/missing')))->toBeNull()
+ ->and($render(new HttpException(404), Request::create('https://admin.example.test/admin/missing'))?->getStatusCode())->toBe(404);
+});