Skip to content

Commit 41226e8

Browse files
arbrandesclaude
andcommitted
feat: add NotFoundPage and route error handling for 404s
Replace React Router's default developer-facing error screen with a user-friendly NotFoundPage for 404 errors, falling back to the existing ErrorPage for other route errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7be4b26 commit 41226e8

5 files changed

Lines changed: 88 additions & 17 deletions

File tree

runtime/react/NotFoundPage.jsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React, { useState } from 'react';
2+
import {
3+
Container, Row, Col, Hyperlink,
4+
} from '@openedx/paragon';
5+
6+
import { useSiteEvent } from './hooks';
7+
import {
8+
FormattedMessage,
9+
IntlProvider,
10+
getMessages,
11+
getLocale,
12+
LOCALE_CHANGED,
13+
} from '../i18n';
14+
15+
16+
function NotFoundPage() {
17+
const [locale, setLocale] = useState(getLocale());
18+
19+
useSiteEvent(LOCALE_CHANGED, () => {
20+
setLocale(getLocale());
21+
});
22+
23+
return (
24+
<IntlProvider locale={locale} messages={getMessages()}>
25+
<Container fluid className="py-5 justify-content-center align-items-start text-center">
26+
<Row>
27+
<Col>
28+
<p className="h2 mb-3">
29+
<FormattedMessage
30+
id="not.found.page.heading"
31+
defaultMessage="404"
32+
description="heading for page not found"
33+
/>
34+
</p>
35+
<p className="text-muted">
36+
<FormattedMessage
37+
id="not.found.page.message"
38+
defaultMessage="The page you're looking for could not be found."
39+
description="message displayed when a page is not found"
40+
/>
41+
</p>
42+
<Hyperlink destination="/">
43+
<FormattedMessage
44+
id="not.found.page.link.text"
45+
defaultMessage="Go to the homepage"
46+
description="link text to navigate to the homepage from a 404 page"
47+
/>
48+
</Hyperlink>
49+
</Col>
50+
</Row>
51+
</Container>
52+
</IntlProvider>
53+
);
54+
}
55+
56+
export default NotFoundPage;

runtime/react/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export { default as AuthenticatedPageRoute } from './AuthenticatedPageRoute';
1414
export { default as Divider } from './Divider';
1515
export { default as ErrorBoundary } from './ErrorBoundary';
1616
export { default as ErrorPage } from './ErrorPage';
17+
export { default as NotFoundPage } from './NotFoundPage';
1718
export { useSiteEvent, useAuthenticatedUser, useSiteConfig, useAppConfig } from './hooks';
1819
export { default as LoginRedirect } from './LoginRedirect';
1920
export { default as PageWrap } from './PageWrap';

shell/router/createRouter.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ describe('createRouter', () => {
3636
[
3737
{
3838
Component: Shell,
39+
errorElement: expect.anything(),
3940
children: mockRoutes,
4041
},
4142
],

shell/router/createRouter.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

shell/router/createRouter.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { createBrowserRouter, isRouteErrorResponse, useRouteError } from 'react-router-dom';
2+
3+
import { getBasename } from '../../runtime/initialize';
4+
import ErrorPage from '../../runtime/react/ErrorPage';
5+
import NotFoundPage from '../../runtime/react/NotFoundPage';
6+
import Shell from '../Shell';
7+
8+
import getAppRoutes from './getAppRoutes';
9+
10+
function RouteError() {
11+
const error = useRouteError();
12+
13+
if (isRouteErrorResponse(error) && error.status === 404) {
14+
return <NotFoundPage />;
15+
}
16+
17+
return <ErrorPage />;
18+
}
19+
20+
export default function createRouter() {
21+
return createBrowserRouter([
22+
{
23+
Component: Shell,
24+
errorElement: <RouteError />,
25+
children: getAppRoutes(),
26+
}
27+
], {
28+
basename: getBasename(),
29+
});
30+
}

0 commit comments

Comments
 (0)