Recipe: custom 404 page
Developer preview
Goal: a URL with no Uniform route shows your own 404 page, with the status code 404. Optionally, Uniform authors manage the content of the 404 page.
Prerequisites#
- An app with the composition route
app/uniform/[code]/page.tsx. Refer to Add the composition route. - For the Uniform-managed 404 page: the package
@uniformdev/canvas, and a published composition with the slugpage-not-found.
How the SDK finds a missing page#
The middleware makes no API calls, so it does not know if a route exists. It rewrites all requests to app/uniform/[code]/page.tsx. Then UniformComposition resolves the route and does one of these actions:
| Route API result | What UniformComposition does | Response |
|---|---|---|
| A composition | Renders the composition. | 200 |
| A redirect with the status 301 or 308 | Calls permanentRedirect(). | 308 |
| A redirect with a different status | Calls redirect(). | 307 |
| No route, or an API error | Calls notFound(). | 404 |
notFound() from next/navigation renders the nearest not-found.tsx file. For app/uniform/[code], this is usually app/not-found.tsx.
Step 1: Add the 404 page#
Make app/not-found.tsx:
app/not-found.tsx
- Next.js renders this file in the root layout.
- Next.js adds
<meta name="robots" content="noindex">to the 404 page. - The root
app/not-found.tsxalso handles URLs that match no route of your app.
Step 2 (optional): Show 404 content from Uniform#
Authors can manage the 404 content in a composition. not-found.tsx gets no props, so it gets no code from the middleware. Thus you make the page state on the page, and resolve the composition by its slug.
In Uniform, make a composition with the slug
page-not-found. Do not attach it to a project map node, so that it has no URL of its own. Publish the composition.Replace
app/not-found.tsxwith this file:app/not-found.tsx
import { CANVAS_PUBLISHED_STATE } from "@uniformdev/canvas"; import { resolveCompositionBySlug, UniformContext, UniformResolvedComposition, } from "@uniformdev/next-app-router"; import type { PageState } from "@uniformdev/next-app-router-shared"; import Link from "next/link"; import { resolveComponent } from "@/components/resolveComponent"; // The page state of a published request. Set all fields. const pageState: PageState = { compositionState: CANVAS_PUBLISHED_STATE, routePath: "/page-not-found", keys: undefined, releaseId: undefined, defaultConsent: true, // the same value as defaultConsent in uniform.server.config.ts previewMode: undefined, locale: undefined, }; export default async function NotFound() { const result = await resolveCompositionBySlug({ slug: "page-not-found", pageState }); // No composition, or an API error: show a fallback. if (!result.route) { return ( <main> <h1>Page not found</h1> <Link href="/">Go to the home page</Link> </main> ); } return ( <UniformContext result={result}> <UniformResolvedComposition result={result} resolveComponent={resolveComponent} /> </UniformContext> ); }Set
defaultConsentto the same value asdefaultConsentinuniform.server.config.ts. Without a server configuration file, the SDK usestrue. With a file that does not setdefaultConsent, the SDK usesfalse.Open a URL that does not exist, for example
/no-page-here. Make sure that the page shows the Uniform content and that the status is404.
How it works#
not-found.tsxcan be an async server component. Thus it can get data before it renders.PageStatehas these fields:Field Value for the 404 page compositionStateCANVAS_PUBLISHED_STATE(64): get the published composition.routePathA path for analytics. The resolver also uses it as the matched route. keysundefined.releaseIdundefined: get the content outside of a release.defaultConsentThe default consent of new visitors. The browser Context starts with this value. previewModeundefined.localeundefined, or a fixed locale.resolveCompositionBySluguses the Composition API, not the Route API. Thus it applies no redirects.UniformContextstarts the Uniform Context in the browser. Render it one time on the page.A published composition uses the Next.js data cache. When you publish the composition again, the webhook revalidates its cache tags. Refer to Caching.
Limits#
- Draft content: the 404 page always shows the published composition, also in draft mode.
- Locale:
not-found.tsxgets no route parameters. It does not know the locale of the URL. Use a fixed locale, or put anot-found.tsxfile in each locale segment. - Personalization: the page state has no
edgeMode. Thus the browser chooses the variants of personalizations and A/B tests after hydration. - Status after the response starts: Next.js sends the status
404only when it callsnotFound()before the response streams. After that, the status stays200, and Next.js adds thenoindextag.
Uniform redirects#
Authors make redirects in Uniform. The SDK applies them on the page, not in the middleware:
- The Route API applies redirects only to published content. In draft and editor states, it ignores them.
UniformCompositioncallsrequireComposition. For a redirect with the status 301 or 308, it callspermanentRedirect(), and Next.js sends308. For other status codes, it callsredirect(), and Next.js sends307.- Next.js does not keep the exact status code that the author set.
- If the response streams already, for example below
loading.tsx, Next.js redirects in the browser with a meta tag. resolveCompositionByIdandresolveCompositionBySlugapply no redirects.
If your page resolves the route itself, call requireComposition(result) to apply the redirect or the 404. To apply only the redirect, and render the page when there is no composition, use this code:
For more information, refer to Resolve compositions on a page.