Images, Fonts, Metadata
next/image, next/font, generateMetadata, OG images.
Three problems Next.js solves so quietly you might miss them: images that are correctly sized and modern-format encoded, fonts that don't flash, and metadata (title, OG image, twitter card) that's rendered per route by a typed API. Each of these is a fight in raw HTML. In Next they take a single import.
next/image: do less, get more
The standard <img>tag has been doing the wrong thing for fifteen years. It doesn't pick the right size for the viewport, doesn't serve AVIF or WebP, and ships layout-shifting placeholders. next/image fixes all of that with one component.
import Image from "next/image";
import hero from "./hero.jpg";
export default function Home() {
return (
<Image
src={hero}
alt="A misty mountain at dawn"
placeholder="blur" // automatic blur-up from the static import
priority // skip lazy-load for the LCP image
/>
);
}What you actually get from that one tag:
- A responsive
srcsetwith the correct sizes for the viewport. - AVIF or WebP if the browser supports it, with JPEG/PNG fallback.
- Width/height baked in so the layout reserves space (no shift, no jank).
- Lazy loading off-screen images by default.
import img from "./photo.jpg") give Next the file at build time, so it knows the dimensions and can pre-generate the blur placeholder. Remote URLs work too, but you must declare allowed domains in next.config.ts and pass width/height manually.<Image
src="https://cdn.example.com/cover.jpg"
alt="Cover art"
width={1200}
height={630}
sizes="(max-width: 768px) 100vw, 50vw"
/>export default {
images: {
remotePatterns: [
{ protocol: "https", hostname: "cdn.example.com" },
],
},
};The sizes prop matters more than people realize
Without sizes, the browser assumes the image will be 100vw and downloads the largest variant. Telling Next how the image is actually laid out shrinks the request dramatically on big screens.
next/font: zero layout shift, no network round trip
Web fonts traditionally fetch from a CDN, swap in late, and cause layout shift. next/font downloads the font at build time, inlines a fallback metric to prevent shift, and serves the file from your own domain (better privacy, better caching).
import { Geist, Geist_Mono } from "next/font/google";
const sans = Geist({
subsets: ["latin"],
variable: "--font-sans",
});
const mono = Geist_Mono({
subsets: ["latin"],
variable: "--font-mono",
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${sans.variable} ${mono.variable}`}>
<body>{children}</body>
</html>
);
}Now --font-sans and --font-mono are CSS variables you can use anywhere:
body { font-family: var(--font-sans); }
code, pre { font-family: var(--font-mono); }Geist is the default new-Next font; you can swap in any Google Font by name. For local fonts, use next/font/local and point to a file in your project.
subsetsarray. Without it, Next can't compute the fallback metrics, and Latin-only sites end up shipping Cyrillic and Greek glyphs too. Specifying subsets makes the file smaller and the rendering correct.Metadata API: SEO without head spaghetti
Every page.tsx and layout.tsx can export a metadata object (static) or a generateMetadata function (dynamic). Next merges them from root to leaf and renders the right <head> tags.
import type { Metadata } from "next";
export const metadata: Metadata = {
title: {
template: "%s | Acme",
default: "Acme",
},
description: "We make widgets.",
openGraph: { siteName: "Acme" },
};import type { Metadata } from "next";
type Props = { params: Promise<{ slug: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) return { title: "Not found" };
return {
title: post.title,
description: post.summary,
openGraph: {
title: post.title,
description: post.summary,
images: [{ url: post.coverUrl, width: 1200, height: 630 }],
},
twitter: { card: "summary_large_image" },
};
}
export default async function PostPage({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}Templates from the root layout (%s | Acme) wrap whatever title the leaf returns, so title: "Hello" becomes Hello | Acme automatically.
OG images: render React to PNG at the edge
For Open Graph and Twitter card previews, you usually want a per-page image with the post title overlaid. Next does this with a file convention plus a JSX-to-image API.
import { ImageResponse } from "next/og";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
type Props = { params: Promise<{ slug: string }> };
export default async function Image({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
padding: 80,
background: "linear-gradient(135deg, #0a0a0a, #1f1f1f)",
color: "white",
fontSize: 72,
fontFamily: "sans-serif",
}}
>
<div style={{ fontSize: 32, opacity: 0.7 }}>Acme Blog</div>
<div style={{ marginTop: 24 }}>{post.title}</div>
</div>
),
{ ...size },
);
}The file lives next to the route, returns an ImageResponse with arbitrary JSX, and Next renders it to PNG on demand. Drop a twitter-image.tsx next to it for the Twitter variant. The metadata API automatically links to both.
Other file conventions you get for free
app/icon.pngorapp/icon.tsx→ favicons and apple-touch-icons.app/robots.txtorapp/robots.ts→ robots file (static or dynamic).app/sitemap.ts→ typed sitemap. Export a function returning{ url, lastModified }entries.app/manifest.ts→ PWA manifest.
Quiz
Why does next/image require width and height (or a static import) for remote URLs?
Recap
next/imagegives you responsive, modern-format, lazy-loaded images with no layout shift. Use static imports for build-time dimensions and blur placeholders.next/fontself-hosts fonts, inlines fallback metrics, and exposes CSS variables. Geist is the default.- Export a
metadataobject (static) orgenerateMetadatafunction (dynamic) from any layout or page. Root templates wrap leaf titles. app/.../opengraph-image.tsxwithImageResponserenders per-route OG PNGs from JSX.- File conventions also cover icons, robots, sitemap, and manifest. All typed, all colocated.