Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Use Next.js App Router conventions to configure favicons, generate dynamic Open Graph images, and set up social sharing metadata. Next.js supports both file-based and config-based approaches.
Favicon setup (file-based):
Place icon files in the app/ directory with conventional names:
app/
favicon.ico # Browser tab icon (32x32 or 16x16)
icon.png # Modern browsers (32x32)
icon.svg # Scalable icon for modern browsers
apple-icon.png # Apple touch icon (180x180)
Next.js automatically generates the correct <link> tags in <head> for these files.
Static OG image (file-based):
app/
opengraph-image.png # Default OG image (1200x630)
twitter-image.png # Twitter card image (1200x630)
Metadata config approach:
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: {
default: "My App",
template: "%s | My App",
},
description: "A description of my app",
icons: {
icon: [
{ url: "/favicon.ico", sizes: "32x32" },
{ url: "/icon.svg", type: "image/svg+xml" },
],
apple: [{ url: "/apple-icon.png", sizes: "180x180" }],
},
openGraph: {
title: "My App",
description: "A description of my app",
url: "https://myapp.com",
siteName: "My App",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "My App preview",
},
],
locale: "en_US",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "My App",
description: "A description of my app",
images: ["/twitter-image.png"],
},
};A dynamic OG image generator for blog posts using ImageResponse:
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";
export const alt = "Blog post preview";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
interface Props {
params: Promise<{ slug: string }>;
}
async function getPost(slug: string) {
// Replace with your data fetching logic
const posts: Record<string, { title: string; author: string; category: string; date: string }> = {
"getting-started": {
title: "Getting Started with Next.js",
author: "Jane Smith",
category: "Tutorial",
date: "2026-03-15",
},
"server-components": {
title: "Understanding React Server Components",
author: "John Doe",
category: "Deep Dive",
date: "2026-03-20",
},
};
return posts[slug] || { title: "Blog Post", author: "Author", category: "General", date: "2026-01-01" };
}
export default async function OGImage({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
return new ImageResponse(
(
<div
style={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
padding: "60px",
background: "linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%)",
color: "white",
fontFamily: "system-ui, sans-serif",
}}
>
{/* Category badge */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "12px",
}}
>
<div
style={{
backgroundColor: "#3b82f6",
borderRadius: "9999px",
padding: "6px 16px",
fontSize: "16px",
fontWeight: 600,
}}
>
{post.category}
</div>
</div>
{/* Title */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: "16px",
}}
>
<div
style={{
fontSize: "56px",
fontWeight: 800,
lineHeight: 1.1,
letterSpacing: "-0.02em",
maxWidth: "900px",
}}
>
{post.title}
</div>
</div>
{/* Footer */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
fontSize: "20px",
color: "#94a3b8",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<div
style={{
width: "40px",
height: "40px",
borderRadius: "50%",
backgroundColor: "#3b82f6",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "white",
fontSize: "18px",
fontWeight: 700,
}}
>
{post.author[0]}
</div>
<span>{post.author}</span>
</div>
<span>{post.date}</span>
</div>
</div>
),
{
...size,
}
);
}// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
interface PageProps {
params: Promise<{ slug: string }>;
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
// The OG image is auto-discovered from opengraph-image.tsx
// but you can add additional metadata here
return {
title: `Blog: ${slug}`,
description: `Read about ${slug}`,
};
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params;
return (
<article className="prose mx-auto max-w-3xl px-4 py-8">
<h1>{slug}</h1>
<p>Blog post content here...</p>
</article>
);
}Dynamic favicon with icon.tsx:
// app/icon.tsx
import { ImageResponse } from "next/og";
export const size = { width: 32, height: 32 };
export const contentType = "image/png";
export default function Icon() {
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#0f172a",
borderRadius: "6px",
color: "white",
fontSize: "20px",
fontWeight: 800,
}}
>
A
</div>
),
{ ...size }
);
}favicon.ico, icon.png, apple-icon.png, opengraph-image.png, and twitter-image.png in route segments are automatically picked up.ImageResponse from next/og, which renders JSX to an image using Satori (a library that converts HTML/CSS to SVG) and then converts to PNG.opengraph-image.tsx files can be placed at any route segment level. A file at app/blog/[slug]/opengraph-image.tsx generates unique OG images per blog post.runtime = "edge" export ensures the image is generated at the edge for fast response times. You can also use runtime = "nodejs" if you need Node.js APIs.generateMetadata and file-based metadata can coexist. File-based images take precedence for their specific metadata fields.Custom fonts in OG images:
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";
export default async function OGImage() {
const fontData = await fetch(
new URL("../../../public/fonts/Inter-Bold.ttf", import.meta.url)
).then((res) => res.arrayBuffer());
return new ImageResponse(
(
<div style={{ fontFamily: "Inter", fontSize: 48, fontWeight: 700 }}>
Custom Font Title
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: "Inter",
data: fontData,
style: "normal",
weight: 700,
},
],
}
);
}Web app manifest icons:
// app/manifest.ts
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "My App",
short_name: "App",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#0f172a",
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
],
};
}ImageResponse is imported from next/og. It accepts JSX as its first argument and an options object as the second.Metadata type from next provides full typing for all metadata fields including openGraph, twitter, and icons.Promise<Metadata>.import type { Metadata, ResolvingMetadata } from "next";
export async function generateMetadata(
{ params }: { params: Promise<{ slug: string }> },
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
const previousImages = (await parent).openGraph?.images || [];
return {
openGraph: {
images: [`/api/og?title=${slug}`, ...previousImages],
},
};
}ImageResponse JSX is not full React. It supports a subset of CSS (flexbox layout only, no grid, no position: absolute alternatives are limited). Check the Satori documentation for supported CSS properties.ArrayBuffer. You cannot use CSS @font-face or Google Fonts URLs directly in ImageResponse.summary_large_image.favicon.ico must be in the app/ root directory. Placing it in a subdirectory will not work.background-image with url() is not supported. Use the img tag in the JSX instead.alt export in opengraph-image.tsx is required for accessibility. It becomes the og:image:alt meta tag.| Approach | Pros | Cons |
|---|---|---|
| File-based (opengraph-image.tsx) | Auto-discovery, per-route images, type-safe | Satori CSS limitations |
| API route (/api/og) | Full control, reusable across routes | Manual metadata wiring |
| Static images | Simplest, no compute cost | Same image for every page |
| Cloudinary OG | Advanced transformations, text overlays | External service, cost |
| @vercel/og (standalone) | Works outside Next.js | Requires separate setup |
favicon.ico -- browser tab icon.icon.png / icon.svg -- modern browser icons.apple-icon.png -- Apple touch icon (180x180).opengraph-image.png -- default OG image (1200x630).twitter-image.png -- Twitter card image (1200x630).summary_large_image cards also use 1200x630.background-image with url() is not supported; use an img tag instead.position: absolute alternatives are limited.const fontData = await fetch(
new URL("../../../public/fonts/Inter-Bold.ttf", import.meta.url)
).then((res) => res.arrayBuffer());
return new ImageResponse(<div style={{ fontFamily: "Inter" }}>Title</div>, {
fonts: [{ name: "Inter", data: fontData, weight: 700 }],
});Fonts must be loaded as ArrayBuffer. CSS @font-face and Google Fonts URLs do not work.
app/ root directory.<link> tag for it.// app/icon.tsx
import { ImageResponse } from "next/og";
export const size = { width: 32, height: 32 };
export const contentType = "image/png";
export default function Icon() {
return new ImageResponse(
<div style={{ background: "#0f172a", color: "white", fontSize: "20px" }}>A</div>,
{ ...size }
);
}import type { Metadata, ResolvingMetadata } from "next";Metadata provides full typing for openGraph, twitter, and icons fields.generateMetadata must return Promise<Metadata>.ResolvingMetadata lets you access parent metadata for merging images.og:image:alt meta tag for accessibility.opengraph-image.tsx) take precedence for their specific metadata fields.generateMetadata handles other fields like title and description.// app/manifest.ts
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "My App",
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" },
],
};
}Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥