Adding Google Fonts to a Next.js app — the _document way and what I use now

Why _document.tsx?
Next.js lets you customise the outer HTML shell — the <html> and <body> tags that wrap every page. Font <link> tags belong in that shell, not repeated inside individual components.
The file lives at pages/_document.tsx. Official docs: Custom Document.
Note:
_document.tsxonly runs on the server. It doesn't re-render on client-side navigation. That's fine for fonts — you load them once and they apply everywhere.
Step 1 — Get your link tags from Google Fonts
- Go to Google Fonts
- Select your font (I used Inter for body copy)
- Pick the weights you need — I went with 300 through 800
- Click Get font → Get embed code
- Copy the
<link>tags Google gives you
You'll get something like this:
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet"
/>
Paste those into _document.tsx — inside <Head>, not <body>.
Step 2 — Create pages/_document.tsx
import Document, { Html, Head, Main, NextScript } from "next/document";
export default function MyDocument() {
return (
<Html lang="en">
<Head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
A few things worth noting:
Put <link> tags in <Head>, not <body>
Font links are document metadata! They belong in <Head>. I saw some tutorials that dropped them in the body. It might work in some browsers, but it's not correct and can cause inconsistent loading.
crossOrigin — not crossOrigin="true"
Google's embed snippet uses crossorigin on the preconnect to fonts.gstatic.com. In React/JSX, write it as:
crossOrigin="anonymous"
Not !crossOrigin="true"
The HTML spec expects "anonymous" or "use-credentials". This matters for CORS when the browser fetches font files from Google's CDN.
preconnect — please don't skip it
The two preconnect links tell the browser to open connections to Google's font servers early. Small detail, real difference in how fast your fonts show up. Keep both.
display=swap in the URL
Notice &display=swap at the end of the Google Fonts URL. That tells the browser to show fallback text immediately and swap in the font when it loads. Stops invisible text while fonts download.
Step 3 — Use the font in your CSS
Loading the font isn't enough. You have to reference it.
In styles/globals.css:
body {
font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
}
Or for a heading font like Merriweather, add a second <link> in <Head> and use:
h1, h2, h3 {
font-family: "Merriweather", serif;
}
Always include a fallback stack after your custom font. If Google Fonts fails to load, your site still looks fine.
Adding a second font
Same process. Go to Google Fonts, select Merriweather (or whatever you want), copy the new <link>, add it to <Head>:
<link
href="https://fonts.googleapis.com/css2?family=Merriweather:wght@300;400;700;900&display=swap"
rel="stylesheet"
/>
You can also combine families in one URL if you prefer fewer requests:
?family=Inter:wght@300;400;500;600;700;800&family=Merriweather:wght@300;400;700;900
What I use on winpeed.com now
When I rebuilt my portfolio on Next.js 15, I switched to next/font/google. Next.js self-hosts the font files at build time — no external requests to Google at runtime, no layout shift from late-loading stylesheets.
utils/fonts.ts:
import { Inter, Merriweather } from "next/font/google";
export const inter = Inter({
subsets: ["latin"],
weight: ["300", "400", "500", "600", "700", "800"],
variable: "--font-inter",
display: "swap",
});
export const merriweather = Merriweather({
subsets: ["latin"],
weight: ["300", "400", "700", "900"],
variable: "--font-merriweather",
display: "swap",
});
pages/_app.tsx:
import { inter, merriweather } from "../utils/fonts";
function MyApp({ Component, pageProps }: AppProps) {
return (
<div className={`${inter.variable} ${merriweather.variable} ${inter.className}`}>
<Component {...pageProps} />
</div>
);
}
styles/globals.css:
body {
font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
}
Headings in styled-components:
font-family: var(--font-merriweather), serif;
No <link> tags needed. No _document.tsx font setup. The CSS variables (--font-inter, --font-merriweather) work anywhere in your styles.
My _document.tsx today handles other things — favicons, theme flash prevention, styled-components SSR — not fonts.
Which approach should you use?
_document + <link> | next/font/google | |
|---|---|---|
| Setup | Copy tags from Google Fonts | Import from next/font/google |
| Works on | Pages Router | App Router and Pages Router |
| Font hosting | Loaded from Google CDN | Self-hosted by Next.js |
| Privacy / GDPR | External request to Google | No runtime Google request |
| Best for | Quick setup, older Next projects | New Next.js projects (12.2+) |
If you're starting fresh on Next.js 12.2 or later, I'd go next/font. If you already have a _document.tsx with fonts working, no pressure to migrate.
Common mistakes I ran into
Fonts not applying — I added the <link> but forgot font-family in CSS. The font loads but nothing uses it.
Wrong crossOrigin — crossOrigin="true" doesn't do what you think. Use "anonymous".
Links in <body> — Move them to <Head>.
Loading too many weights — Every weight is more bytes. I only load what I actually use in the design. This is very important
Typography is one of those details people feel before they can name it. My portfolio looked more intentional the day I stopped accepting whatever font the browser defaulted to.
Referenced Docs: Next.js Custom Document · Next.js Font Optimization
Share this post