Lesson 34 of 81
The Pages Router split "wrap every page" and "customize the HTML shell" into two separate files with two separate rulebooks. The App Router's root layout quietly merges both jobs into one — which is exactly why porting old _app/_document logic over trips people up.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
In the Pages Router (pages/), two special files sat above every page: _app.tsx wrapped every single page in shared UI and persisted state across navigations — the place for global CSS imports, context providers, and a persistent layout shell. _document.tsx was a level below that, customizing the actual server-rendered HTML document itself: the <html> and <body> tags, <Head> boilerplate, where scripts get injected. It rendered once per request on the server, never re-rendered on navigation, and couldn't hold state or hooks — no interactivity was ever expected from it.
The App Router collapses both jobs into one file: the root app/layout.tsx. It defines <html> and <body> (what _document.tsx used to own) and wraps every route in shared UI (what _app.tsx used to own). There's no longer an artificial two-file split for what's really one continuous concern — "what wraps this page, all the way out to the actual document."
// pages/_app.tsx — Pages Router
import type { AppProps } from "next/app";
import { ThemeProvider } from "@/components/theme-provider";
import "@/styles/globals.css";
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
);
}
// pages/_document.tsx — Pages Router
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
// app/layout.tsx — App Router, same two responsibilities, one file
import { ThemeProvider } from "@/components/theme-provider";
import "@/styles/globals.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
Notice what disappeared, not just what merged: no <Head />, <Main />, or <NextScript /> placeholders — the App Router handles script injection and <head> population itself, and children does the job <Component {...pageProps} /> and <Main /> did together. Global CSS imports move here too, since app/layout.tsx is now the top of the tree, same as _app.tsx was.
_document.tsx specifically will send you looking for something that doesn't exist as a separate concept anymore; it's just part of what app/layout.tsx does.pages/ and app/ can coexist during an incremental migration, each governing their own routes — but never the same route. A request never gets rendered by both _app.tsx's tree and app/layout.tsx's tree at once; Next.js resolves which router owns a given path and renders through that router's chain only._document.tsx's getInitialProps had a real, easy-to-miss cost: using it disabled Automatic Static Optimization for every page in the app, not just the ones that needed it. The App Router has no equivalent footgun — data fetching is scoped per-segment, so one route's needs don't degrade every other route's rendering strategy._app.tsx's pageProps implicitly could. _app.tsx received whatever getInitialProps/getServerSideProps returned for the current page, forwarded through pageProps. A root layout doesn't re-render per navigation and has no equivalent channel — each page fetches what it needs itself, colocated with the page, instead of routed through a shared wrapper._document.tsx customization was optional. Every Pages Router app worked fine without ever creating a _document.tsx (Next.js supplied a default). Every App Router app must define app/layout.tsx with its own <html> and <body> — there's no implicit fallback."You're migrating a Pages Router app to the App Router and it has a fairly involved _app.tsx with five context providers, plus a _document.tsx that injects a third-party analytics script — where does each piece go?" is a realistic, common migration question, and answering it well requires actually understanding that the split those two files represented doesn't exist anymore, rather than hunting for a same-named file to copy logic into.