R
Rishtaara
Next.js: Zero to Full-Stack
Lesson 38 of 42Article14 min

Practice: Custom Error Page, Fonts, YouTube Embed

app/not-found.tsx handles missing routes. app/error.tsx is a Client Component error boundary for unexpected failures. app/global-error.tsx covers root layout failures.

Custom error page

app/not-found.tsx handles missing routes. app/error.tsx is a Client Component error boundary for unexpected failures. app/global-error.tsx covers root layout failures.

Real-life example: not-found is a friendly "wrong floor" sign. error.tsx is the building fire warden telling you calmly what to do next.

app/not-found.tsx
import Link from "next/link";

export default function NotFound() {
  return (
    <main>
      <h1>Page not found</h1>
      <p>That lesson may have moved. Try the course home.</p>
      <Link href="/learn">Back to Learn</Link>
    </main>
  );
}
app/error.tsx
"use client";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <main>
      <h1>Something went wrong</h1>
      <p>{error.message}</p>
      <button type="button" onClick={reset}>
        Try again
      </button>
    </main>
  );
}

Custom local fonts recap

Use next/font/local for self-hosted files. Fonts are optimized and avoid layout shift when configured with display: swap.

Real-life example: Self-hosting fonts is stocking your own kitchen spices — you are not waiting for a neighbor to lend garam masala on every cook.

Tip: Prefer WOFF2. Limit weights you actually use — each file costs bytes on first visit.
Local font in root layout
import localFont from "next/font/local";

const brandSans = localFont({
  src: [
    { path: "../fonts/BrandSans-Regular.woff2", weight: "400" },
    { path: "../fonts/BrandSans-Bold.woff2", weight: "700" },
  ],
  variable: "--font-brand",
  display: "swap",
});

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" className={brandSans.variable}>
      <body>{children}</body>
    </html>
  );
}

YouTube embed — safe pattern

Do not paste raw iframes with autoplay spam. Use youtube-nocookie.com, a title for accessibility, and lazy loading. Wrap in an aspect-ratio box.

Real-life example: A careful YouTube embed is inviting a guest speaker with a scheduled slot — not letting them rearrange your whole living room.

Privacy-friendlier YouTube embed
type Props = { videoId: string; title: string };

export function YouTubeEmbed({ videoId, title }: Props) {
  return (
    <div style={{ aspectRatio: "16 / 9", width: "100%" }}>
      <iframe
        src={`https://www.youtube-nocookie.com/embed/${videoId}`}
        title={title}
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowFullScreen
        style={{ width: "100%", height: "100%", border: 0 }}
      />
    </div>
  );
}

// <YouTubeEmbed videoId="dQw4w9WgXcQ" title="Intro to App Router" />