Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Utiliza las convenciones de Next.js App Router para configurar favicons, generar imágenes Open Graph dinámicas y configurar metadatos de compartir en redes sociales. Next.js admite ambos enfoques basados en archivos y basados en configuración.
Configuración de favicon (basada en archivos):
Coloca archivos de ícono en el directorio app/ con nombres convencionales:
app/
favicon.ico # Ícono de pestaña del navegador (32x32 o 16x16)
icon.png # Navegadores modernos (32x32)
icon.svg # Ícono escalable para navegadores modernos
apple-icon.png # Ícono de toque de Apple (180x180)
Next.js genera automáticamente las etiquetas <link> correctas en <head> para estos archivos.
Imagen OG estática (basada en archivos):
app/
opengraph-image.png # Imagen OG predeterminada (1200x630)
twitter-image.png # Imagen de tarjeta de Twitter (1200x630)
Enfoque de configuración de metadatos:
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: {
default: "Mi Aplicación",
template: "%s | Mi Aplicación",
},
description: "Una descripción de mi aplicación",
icons: {
icon: [
{ url: "/favicon.ico", sizes: "32x32" },
{ url: "/icon.svg", type: "image/svg+xml" },
],
apple: [{ url: "/apple-icon.png", sizes: "180x180" }],
},
openGraph: {
title: "Mi Aplicación",
description: "Una descripción de mi aplicación",
url: "https://myapp.com",
siteName: "Mi Aplicación",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "Vista previa de Mi Aplicación",
},
],
locale: "en_US",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Mi Aplicación",
description: "Una descripción de mi aplicación",
images: ["/twitter-image.png"],
},
};Un generador de imagen OG dinámico para publicaciones de blog usando ImageResponse:
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";
export const alt = "Vista previa de publicación de blog";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
interface Props {
params: Promise<{ slug: string }>;
}
async function getPost(slug: string) {
// Reemplaza con tu lógica de obtención de datos
const posts: Record<string, { title: string; author: string; category: string; date: string }> = {
"getting-started": {
title: "Comenzando con Next.js",
author: "Jane Smith",
category: "Tutorial",
date: "2026-03-15",
},
"server-components": {
title: "Entendiendo React Server Components",
author: "John Doe",
category: "Análisis Profundo",
date: "2026-03-20",
},
};
return posts[slug] || { title: "Publicación de Blog", author: "Autor", 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",
}}
>
{/* Insignia de categoría */}
<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>
{/* Título */}
<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>
{/* Pie de página */}
<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;
// La imagen OG se descubre automáticamente desde opengraph-image.tsx
// pero puedes añadir metadatos adicionales aquí
return {
title: `Blog: ${slug}`,
description: `Lee sobre ${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>Contenido de la publicación del blog aquí...</p>
</article>
);
}Favicon dinámico con 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 y twitter-image.png en segmentos de ruta se recogen automáticamente.ImageResponse desde next/og, que renderiza JSX a una imagen usando Satori (una biblioteca que convierte HTML/CSS a SVG) y luego convierte a PNG.opengraph-image.tsx se pueden colocar en cualquier nivel de segmento de ruta. Un archivo en app/blog/[slug]/opengraph-image.tsx genera imágenes OG únicas por publicación de blog.runtime = "edge" asegura que la imagen se genere en el edge para tiempos de respuesta rápidos. También puedes usar runtime = "nodejs" si necesitas APIs de Node.js.generateMetadata y metadatos basados en archivos pueden coexistir. Las imágenes basadas en archivos tienen prioridad para sus campos de metadatos específicos.Fuentes personalizadas en imágenes OG:
// 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 }}>
Título con Fuente Personalizada
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: "Inter",
data: fontData,
style: "normal",
weight: 700,
},
],
}
);
}Íconos de manifiesto de aplicación web:
// app/manifest.ts
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Mi Aplicación",
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 se importa desde next/og. Acepta JSX como su primer argumento y un objeto de opciones como el segundo.Metadata desde next proporciona escritura completa para todos los campos de metadatos incluyendo openGraph, twitter e 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 no es React completo. Admite un subconjunto de CSS (solo diseño flexbox, sin grid, sin position: absolute, las alternativas son limitadas). Consulta la documentación de Satori para las propiedades CSS admitidas.ArrayBuffer. No puedes usar directamente CSS @font-face o URLs de Google Fonts en ImageResponse.summary_large_image.favicon.ico debe estar en el directorio raíz de app/. Colocarlo en un subdirectorio no funcionará.background-image con url() no está soportado. Usa la etiqueta img en JSX en su lugar.alt en opengraph-image.tsx es requerida para accesibilidad. Se convierte en la etiqueta meta og:image:alt.| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Basado en archivos (opengraph-image.tsx) | Autodescubrimiento, imágenes por ruta, type-safe | Limitaciones de CSS de Satori |
| Ruta de API (/api/og) | Control total, reutilizable entre rutas | Cableado manual de metadatos |
| Imágenes estáticas | Más simple, sin costo de cómputo | Misma imagen para cada página |
| Cloudinary OG | Transformaciones avanzadas, superposiciones de texto | Servicio externo, costo |
| @vercel/og (independiente) | Funciona fuera de Next.js | Requiere configuración separada |
favicon.ico -- ícono de pestaña del navegador.icon.png / icon.svg -- íconos de navegador moderno.apple-icon.png -- Ícono de toque de Apple (180x180).opengraph-image.png -- imagen OG predeterminada (1200x630).twitter-image.png -- imagen de tarjeta de Twitter (1200x630).summary_large_image también utilizan 1200x630.background-image con url() no está soportado; usa una etiqueta img en su lugar.position: absolute son limitadas.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" }}>Título</div>, {
fonts: [{ name: "Inter", data: fontData, weight: 700 }],
});Las fuentes deben cargarse como ArrayBuffer. CSS @font-face y URLs de Google Fonts no funcionan.
app/.<link> correcta para él.// 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 proporciona escritura completa para campos openGraph, twitter e icons.generateMetadata debe retornar Promise<Metadata>.ResolvingMetadata te permite acceder a metadatos padre para fusionar imágenes.og:image:alt para accesibilidad.opengraph-image.tsx) tienen prioridad para sus campos de metadatos específicos.generateMetadata maneja otros campos como título y descripción.// app/manifest.ts
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Mi Aplicación",
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" },
],
};
}Revisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥