Recipe: custom 404 page

Developer preview

This feature is in developer preview. Use with caution as it may change unexpectedly. For more information, contact us.

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.

  • 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 slug page-not-found.

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 resultWhat UniformComposition doesResponse
A compositionRenders the composition.200
A redirect with the status 301 or 308Calls permanentRedirect().308
A redirect with a different statusCalls redirect().307
No route, or an API errorCalls notFound().404

notFound() from next/navigation renders the nearest not-found.tsx file. For app/uniform/[code], this is usually app/not-found.tsx.

Make app/not-found.tsx:

app/not-found.tsx

import Link from "next/link"; export default function NotFound() { return ( <main> <h1>Page not found</h1> <p>We cannot find the page that you asked for.</p> <Link href="/">Go to the home page</Link> </main> ); }
  • 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.tsx also handles URLs that match no route of your app.

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.

  1. 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.

  2. Replace app/not-found.tsx with 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> ); }
  3. Set defaultConsent to the same value as defaultConsent in uniform.server.config.ts. Without a server configuration file, the SDK uses true. With a file that does not set defaultConsent, the SDK uses false.

  4. 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 is 404.

  • not-found.tsx can be an async server component. Thus it can get data before it renders.

  • PageState has these fields:

    FieldValue 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.
  • resolveCompositionBySlug uses the Composition API, not the Route API. Thus it applies no redirects.

  • UniformContext starts 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.

  • Draft content: the 404 page always shows the published composition, also in draft mode.
  • Locale: not-found.tsx gets no route parameters. It does not know the locale of the URL. Use a fixed locale, or put a not-found.tsx file 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 404 only when it calls notFound() before the response streams. After that, the status stays 200, and Next.js adds the noindex tag.

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.
  • UniformComposition calls requireComposition. For a redirect with the status 301 or 308, it calls permanentRedirect(), and Next.js sends 308. For other status codes, it calls redirect(), and Next.js sends 307.
  • 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.
  • resolveCompositionById and resolveCompositionBySlug apply 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:

import { requireComposition, type ResolvedRouteResult } from "@uniformdev/next-app-router"; export function applyUniformRedirect(result: ResolvedRouteResult) { if (result.route?.type === "redirect") { requireComposition(result); // calls redirect() or permanentRedirect() } return result; }

For more information, refer to Resolve compositions on a page.