Icons follow the same rule as share images: name the file correctly and the <link> tags write themselves.
| File | Extensions | Where | Emits |
|---|---|---|---|
| favicon | .ico | root app/ only | <link rel="icon" sizes="any"> |
| icon | .ico .jpg .jpeg .png .svg | any segment | <link rel="icon"> |
| apple-icon | .jpg .jpeg .png | any segment | <link rel="apple-touch-icon"> |
| icon, apple-icon | .js .ts .tsx | any segment | the same, rendered by code |
The rel, type and sizes attributes are read off the file: a 32x32 PNG becomes type="image/png" sizes="32x32", an SVG gets sizes="any". Number the files (icon1.png) to ship several; a favicon cannot be generated from code. One app/icon.svg produced <link rel="icon" href="/icon.svg?icon.2g5h9x05m6bzi.svg" sizes="any" type="image/svg+xml"/>. A manifest.ts returning MetadataRoute.Manifest is served at /manifest.webmanifest and adds <link rel="manifest">.
Viewport and theme color are deliberately not part of metadata. They live in a viewport export, or an async generateViewport, because Next.js 10,514 must emit them before the streamed metadata of generateMetadata resolves: a late viewport tag reflows the page.
export const viewport: Viewport = { width: "device-width", initialScale: 1,
themeColor: [{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
{ media: "(prefers-color-scheme: dark)", color: "#0b1020" }] };All three land in the first bytes of the response, ahead of the title: <meta name="viewport" content="width=device-width, initial-scale=1"/> and one theme-color tag per entry.