diff --git a/config/services.yaml b/config/services.yaml
index 58a444d..e86596c 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -59,3 +59,7 @@ services:
Enabel\Ux\Component\Widget\Widget:
tags:
- { name: 'twig.component', key: 'Enabel:Ux:Widget', template: '@EnabelUx/widget/widget.html.twig', expose_public_props: true }
+
+ Enabel\Ux\Component\Layout\ErrorPage:
+ tags:
+ - { name: 'twig.component', key: 'Enabel:Ux:ErrorPage', template: '@EnabelUx/layout/error_page.html.twig', expose_public_props: true }
diff --git a/docs/Layout/errorPage.md b/docs/Layout/errorPage.md
new file mode 100644
index 0000000..32253f3
--- /dev/null
+++ b/docs/Layout/errorPage.md
@@ -0,0 +1,83 @@
+# ErrorPage Component
+
+## Description
+
+A full-page error layout for Symfony's TwigBundle exception templates (`error.html.twig`, `error404.html.twig`, `error500.html.twig`, etc.). Replaces the abandoned `@EnabelLayout/error/base.html.twig` from `enabel/layout-bundle`.
+
+The component renders the full `` page (it intentionally does **not** extend `base.html.twig` — error pages must keep working even when the asset pipeline is broken) and emits the markup expected by the theme stylesheet shipped at `@enabel/enabel-bootstrap-theme/dist/css/error.min.css`.
+
+## Pre-requisite
+
+The CSS that styles the error page is shipped by the Enabel Bootstrap Theme. Add it to your importmap:
+
+```bash
+symfony console importmap:require "@enabel/enabel-bootstrap-theme/dist/css/error.min.css"
+```
+
+If you serve the theme through a different path, override the `stylesheet` parameter (see below).
+
+## Parameters
+
+| Parameter | Type | Description | Default |
+|:-------------|:----------|:-------------------------------------------------------------------------|:-------------------------------------------------------------------------|
+| `statusCode` | `int` | HTTP status code displayed as the big number (**required**) | — |
+| `title` | `string` | Heading shown below the status code (**required**) | — |
+| `message` | `string` | Body paragraph explaining the error (**required**) | — |
+| `details` | `?string` | Optional small print (trace IDs, technical details) | `null` |
+| `backUrl` | `?string` | URL of the "back home" button. Button is hidden when `null` | `null` |
+| `backLabel` | `string` | Label of the "back home" button | `'Back to homepage'` |
+| `symbol` | `string` | Path to the watermark image (`.bg img`) | `'/images/enabel-symbol.png'` |
+| `logo` | `string` | Path to the bottom logo (`.logo img`) | `'/images/enabel-logo-email.png'` |
+| `locale` | `string` | Value used for `` | `'en'` |
+| `appName` | `string` | Application name used in the `
` tag and logo `alt` | `'Enabel'` |
+| `stylesheet` | `string` | Path passed to `asset()` for the error CSS | `'vendor/@enabel/enabel-bootstrap-theme/dist/css/error.min.css'` |
+
+## Usage
+
+### Minimal 404 page
+
+```twig
+{# templates/bundles/TwigBundle/Exception/error404.html.twig #}
+{{ component('Enabel:Ux:ErrorPage', {
+ statusCode: 404,
+ title: 'app.error.404.title'|trans,
+ message: 'app.error.404.message'|trans,
+}) }}
+```
+
+### With back button and localized labels
+
+```twig
+{# templates/bundles/TwigBundle/Exception/error404.html.twig #}
+{{ component('Enabel:Ux:ErrorPage', {
+ statusCode: 404,
+ title: 'app.error.404.title'|trans,
+ message: 'app.error.404.message'|trans,
+ backUrl: path('app_home'),
+ backLabel: 'app.error.btn.backhome'|trans,
+ locale: app.request.locale,
+ appName: 'app.name'|trans,
+}) }}
+```
+
+### Generic error template with optional debug details
+
+```twig
+{# templates/bundles/TwigBundle/Exception/error.html.twig #}
+{{ component('Enabel:Ux:ErrorPage', {
+ statusCode: status_code,
+ title: status_text,
+ message: 'app.error.generic.message'|trans,
+ details: app.environment == 'dev' ? exception.message : null,
+ backUrl: path('app_home'),
+}) }}
+```
+
+> [!TIP]
+> On 5xx pages, leave `backUrl` unset — the trace ID then sits on its own line under the message instead of inline next to the button.
+
+## Why a full-page component?
+
+Symfony renders TwigBundle exception templates in a context where the regular `base.html.twig` cannot be trusted (the failing request might be the one that broke the asset pipeline). For this reason the component does not extend `base.html.twig` and only loads a single stylesheet via `asset()`.
+
+If you want to ship your own CSS for the error page, override the `stylesheet` parameter or override the template entirely with the standard bundle override mechanism (see the main [documentation](../index.md#how-to-override-templates)).
diff --git a/docs/index.md b/docs/index.md
index c0fc2ec..b06580d 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -26,6 +26,7 @@ https://github.com/Enabel/Ux
## Layouts
- [Bootstrap](Layout/bootstrap.md) - A layout for rendering Bootstrap components with Enabel UX components & Enabel Bootstrap Theme
+- [ErrorPage](Layout/errorPage.md) - A full-page error layout for Symfony's TwigBundle exception templates
## Install
diff --git a/src/Component/Layout/ErrorPage.php b/src/Component/Layout/ErrorPage.php
new file mode 100644
index 0000000..a1f5a95
--- /dev/null
+++ b/src/Component/Layout/ErrorPage.php
@@ -0,0 +1,70 @@
+
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Enabel\Ux\Component\Layout;
+
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\UX\TwigComponent\Attribute\PreMount;
+
+class ErrorPage
+{
+ public int $statusCode;
+ public string $title;
+ public string $message;
+ public ?string $details;
+ public ?string $backUrl;
+ public string $backLabel;
+ public string $symbol;
+ public string $logo;
+ public string $locale;
+ public string $appName;
+ public string $stylesheet;
+
+ /**
+ * @param array $data
+ *
+ * @return array
+ */
+ #[PreMount]
+ public function preMount(array $data): array
+ {
+ $resolver = new OptionsResolver();
+ $this->configureOptions($resolver);
+
+ return $resolver->resolve($data) + $data;
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setIgnoreUndefined();
+ $resolver->setRequired(['statusCode', 'title', 'message']);
+ $resolver->setDefaults([
+ 'details' => null,
+ 'backUrl' => null,
+ 'backLabel' => 'Back to homepage',
+ 'symbol' => '/images/enabel-symbol.png',
+ 'logo' => '/images/enabel-logo-email.png',
+ 'locale' => 'en',
+ 'appName' => 'Enabel',
+ 'stylesheet' => 'vendor/@enabel/enabel-bootstrap-theme/dist/css/error.min.css',
+ ]);
+
+ $resolver->setAllowedTypes('statusCode', 'int');
+ $resolver->setAllowedTypes('title', 'string');
+ $resolver->setAllowedTypes('message', 'string');
+ $resolver->setAllowedTypes('details', ['string', 'null']);
+ $resolver->setAllowedTypes('backUrl', ['string', 'null']);
+ $resolver->setAllowedTypes('backLabel', 'string');
+ $resolver->setAllowedTypes('symbol', 'string');
+ $resolver->setAllowedTypes('logo', 'string');
+ $resolver->setAllowedTypes('locale', 'string');
+ $resolver->setAllowedTypes('appName', 'string');
+ $resolver->setAllowedTypes('stylesheet', 'string');
+ }
+}
diff --git a/templates/layout/error_page.html.twig b/templates/layout/error_page.html.twig
new file mode 100644
index 0000000..c31de0d
--- /dev/null
+++ b/templates/layout/error_page.html.twig
@@ -0,0 +1,22 @@
+
+
+
+
+
+ {{ statusCode }} — {{ appName }}
+
+
+
+
+
+
+
{{ statusCode }}
+
{{ title }}
+
{{ message }}
+ {% if backUrl %}
{{ backLabel }}{% endif %}
+ {% if details %}
{{ details }}{% endif %}
+
 }})
+
+
+
+
diff --git a/tests/Component/Layout/ErrorPageTest.php b/tests/Component/Layout/ErrorPageTest.php
new file mode 100644
index 0000000..3566b50
--- /dev/null
+++ b/tests/Component/Layout/ErrorPageTest.php
@@ -0,0 +1,170 @@
+
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Enabel\Ux\Tests\Component\Layout;
+
+use Enabel\Ux\Component\Layout\ErrorPage;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
+use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
+
+class ErrorPageTest extends TestCase
+{
+ public function testComponentCanBeInstantiatedWithRequiredParameters(): void
+ {
+ $component = new ErrorPage();
+ $data = $component->preMount([
+ 'statusCode' => 404,
+ 'title' => 'Not Found',
+ 'message' => 'The page does not exist.',
+ ]);
+
+ $component->statusCode = $data['statusCode'];
+ $component->title = $data['title'];
+ $component->message = $data['message'];
+ $component->details = $data['details'];
+ $component->backUrl = $data['backUrl'];
+ $component->backLabel = $data['backLabel'];
+ $component->symbol = $data['symbol'];
+ $component->logo = $data['logo'];
+ $component->locale = $data['locale'];
+ $component->appName = $data['appName'];
+ $component->stylesheet = $data['stylesheet'];
+
+ $this->assertSame(404, $component->statusCode);
+ $this->assertSame('Not Found', $component->title);
+ $this->assertSame('The page does not exist.', $component->message);
+ $this->assertNull($component->details);
+ $this->assertNull($component->backUrl);
+ $this->assertSame('Back to homepage', $component->backLabel);
+ $this->assertSame('/images/enabel-symbol.png', $component->symbol);
+ $this->assertSame('/images/enabel-logo-email.png', $component->logo);
+ $this->assertSame('en', $component->locale);
+ $this->assertSame('Enabel', $component->appName);
+ $this->assertSame('vendor/@enabel/enabel-bootstrap-theme/dist/css/error.min.css', $component->stylesheet);
+ }
+
+ public function testComponentCanBeInstantiatedWithCustomParameters(): void
+ {
+ $component = new ErrorPage();
+ $data = $component->preMount([
+ 'statusCode' => 500,
+ 'title' => 'Server Error',
+ 'message' => 'Something went wrong.',
+ 'details' => 'Trace ID: abc-123',
+ 'backUrl' => '/',
+ 'backLabel' => 'Return home',
+ 'symbol' => '/custom/symbol.svg',
+ 'logo' => '/custom/logo.svg',
+ 'locale' => 'fr',
+ 'appName' => 'Impala',
+ 'stylesheet' => 'css/custom-error.css',
+ ]);
+
+ $this->assertSame(500, $data['statusCode']);
+ $this->assertSame('Server Error', $data['title']);
+ $this->assertSame('Something went wrong.', $data['message']);
+ $this->assertSame('Trace ID: abc-123', $data['details']);
+ $this->assertSame('/', $data['backUrl']);
+ $this->assertSame('Return home', $data['backLabel']);
+ $this->assertSame('/custom/symbol.svg', $data['symbol']);
+ $this->assertSame('/custom/logo.svg', $data['logo']);
+ $this->assertSame('fr', $data['locale']);
+ $this->assertSame('Impala', $data['appName']);
+ $this->assertSame('css/custom-error.css', $data['stylesheet']);
+ }
+
+ public function testMissingStatusCodeThrows(): void
+ {
+ $this->expectException(MissingOptionsException::class);
+
+ $component = new ErrorPage();
+ $component->preMount(['title' => 'Not Found', 'message' => 'Missing']);
+ }
+
+ public function testMissingTitleThrows(): void
+ {
+ $this->expectException(MissingOptionsException::class);
+
+ $component = new ErrorPage();
+ $component->preMount(['statusCode' => 404, 'message' => 'Missing']);
+ }
+
+ public function testMissingMessageThrows(): void
+ {
+ $this->expectException(MissingOptionsException::class);
+
+ $component = new ErrorPage();
+ $component->preMount(['statusCode' => 404, 'title' => 'Not Found']);
+ }
+
+ public function testInvalidStatusCodeTypeThrows(): void
+ {
+ $this->expectException(InvalidOptionsException::class);
+
+ $component = new ErrorPage();
+ $component->preMount([
+ 'statusCode' => '404',
+ 'title' => 'Not Found',
+ 'message' => 'Missing',
+ ]);
+ }
+
+ public function testInvalidTitleTypeThrows(): void
+ {
+ $this->expectException(InvalidOptionsException::class);
+
+ $component = new ErrorPage();
+ $component->preMount([
+ 'statusCode' => 404,
+ 'title' => 404,
+ 'message' => 'Missing',
+ ]);
+ }
+
+ public function testInvalidDetailsTypeThrows(): void
+ {
+ $this->expectException(InvalidOptionsException::class);
+
+ $component = new ErrorPage();
+ $component->preMount([
+ 'statusCode' => 404,
+ 'title' => 'Not Found',
+ 'message' => 'Missing',
+ 'details' => 123,
+ ]);
+ }
+
+ public function testInvalidBackUrlTypeThrows(): void
+ {
+ $this->expectException(InvalidOptionsException::class);
+
+ $component = new ErrorPage();
+ $component->preMount([
+ 'statusCode' => 404,
+ 'title' => 'Not Found',
+ 'message' => 'Missing',
+ 'backUrl' => true,
+ ]);
+ }
+
+ public function testPreMountPreservesAdditionalData(): void
+ {
+ $component = new ErrorPage();
+ $data = $component->preMount([
+ 'statusCode' => 404,
+ 'title' => 'Not Found',
+ 'message' => 'Missing',
+ 'custom_attribute' => 'value',
+ ]);
+
+ $this->assertArrayHasKey('custom_attribute', $data);
+ $this->assertSame('value', $data['custom_attribute']);
+ }
+}