diff --git a/src/App.jsx b/src/App.jsx
index 839cd7c..72384f3 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -23,6 +23,7 @@ import ServicesPage from './pages/ServicesPage'
import ServerpulsPage from './pages/ServerpulsPage'
import BankCallbackPage from './pages/BankCallbackPage'
import AdminRoute from './components/AdminRoute'
+import RouteErrorBoundary from './components/RouteErrorBoundary'
import { safeReturnPath } from './lib/returnPath'
// Hochhaus der Hustler: erst beim Öffnen laden, damit Engine und Schrift nicht im Haupt-Bundle liegen
@@ -124,7 +125,8 @@ function AppContent() {
WEBklar
-
+ {/* Fängt u. a. einen fehlenden lazy-Chunk ab (alter Tab nach einem Deploy): Menü bleibt, Knopf lädt neu */}
+
)
diff --git a/src/components/RouteErrorBoundary.jsx b/src/components/RouteErrorBoundary.jsx
new file mode 100644
index 0000000..5a1f5a0
--- /dev/null
+++ b/src/components/RouteErrorBoundary.jsx
@@ -0,0 +1,42 @@
+import { Component } from 'react'
+import EmptyState from './ui/EmptyState'
+import Button from './ui/Button'
+
+// Fängt Fehler beim Aufbau einer Seite ab, damit nicht die ganze Oberfläche verschwindet.
+// Wichtigster Fall: Nach einem Deploy fehlt einem alten, noch offenen Tab der Chunk einer lazy geladenen Seite
+// (nginx antwortet statt mit der .js-Datei mit der index.html, der Import schlägt fehl). Ohne Boundary räumt React
+// dann den gesamten Baum ab (leere Seite); so bleiben Menü und Rahmen stehen und ein Knopf lädt neu.
+// resetKey ist der Pfad: ein Seitenwechsel im Menü räumt den Fehler weg. Ein fehlgeschlagener lazy-Import bleibt
+// dagegen fehlgeschlagen, bis die Seite neu geladen wird (React merkt sich die abgelehnte Zusage).
+export default class RouteErrorBoundary extends Component {
+ constructor(props) {
+ super(props)
+ this.state = { error: null, resetKey: props.resetKey }
+ }
+
+ static getDerivedStateFromError(error) {
+ return { error }
+ }
+
+ static getDerivedStateFromProps(props, state) {
+ if (props.resetKey !== state.resetKey) return { error: null, resetKey: props.resetKey }
+ return null
+ }
+
+ componentDidCatch(error) {
+ console.error('Seitenfehler:', error)
+ }
+
+ render() {
+ if (!this.state.error) return this.props.children
+ return (
+
+ window.location.reload()}>Seite neu laden}
+ />
+
+ )
+ }
+}
diff --git a/tests/unit/routeErrorBoundary.test.mjs b/tests/unit/routeErrorBoundary.test.mjs
new file mode 100644
index 0000000..b2cf418
--- /dev/null
+++ b/tests/unit/routeErrorBoundary.test.mjs
@@ -0,0 +1,65 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import esbuild from 'esbuild'
+
+// Die Boundary ist JSX mit React: gebündelt wird mit eigenem automatischem JSX-Laufzeitpaket (load.mjs kennt kein JSX).
+// Gezeichnet wird mit renderToStaticMarkup; der Fehlerzustand kommt über eine Unterklasse, die ihn vorab setzt
+// (der Server-Renderer selbst fängt keine Fehler in Boundaries ab).
+const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
+
+async function load() {
+ const result = await esbuild.build({
+ stdin: {
+ contents: [
+ "import { createElement } from 'react'",
+ "import { renderToStaticMarkup } from 'react-dom/server'",
+ "import Boundary from './src/components/RouteErrorBoundary.jsx'",
+ 'export { createElement, renderToStaticMarkup, Boundary }',
+ ].join('\n'),
+ resolveDir: repoRoot,
+ loader: 'js',
+ },
+ bundle: true,
+ format: 'esm',
+ platform: 'browser',
+ jsx: 'automatic',
+ define: { 'process.env.NODE_ENV': '"production"' },
+ write: false,
+ })
+ const code = result.outputFiles[0].text
+ return import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`)
+}
+
+test('RouteErrorBoundary: ohne Fehler bleiben die Kinder unverändert', async () => {
+ const { createElement, renderToStaticMarkup, Boundary } = await load()
+ const html = renderToStaticMarkup(createElement(Boundary, { resetKey: '/hustler' }, createElement('p', null, 'Inhalt')))
+ assert.equal(html, '
Inhalt
')
+})
+
+test('RouteErrorBoundary: im Fehlerfall Hinweis mit Knopf „Seite neu laden“ statt der Kinder, als Alarm ausgezeichnet', async () => {
+ const { createElement, renderToStaticMarkup, Boundary } = await load()
+ class Probe extends Boundary {
+ constructor(props) {
+ super(props)
+ this.state = { error: new TypeError('Failed to fetch dynamically imported module'), resetKey: props.resetKey }
+ }
+ }
+ const html = renderToStaticMarkup(createElement(Probe, { resetKey: '/hustler' }, createElement('p', null, 'Inhalt')))
+ assert.doesNotMatch(html, /Inhalt/)
+ assert.match(html, /role="alert"/)
+ assert.match(html, /Diese Seite konnte nicht angezeigt werden\./)
+ assert.match(html, /