Favicons e Imágenes OG
Receta
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"],
},
};Ejemplo Funcional
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 }
);
}Análisis Profundo
Cómo Funciona
- Next.js App Router utiliza convenciones de archivos para generar metadatos. Los archivos nombrados
favicon.ico,icon.png,apple-icon.png,opengraph-image.pngytwitter-image.pngen segmentos de ruta se recogen automáticamente. - La generación de imágenes dinámicas utiliza
ImageResponsedesdenext/og, que renderiza JSX a una imagen usando Satori (una biblioteca que convierte HTML/CSS a SVG) y luego convierte a PNG. - Los archivos
opengraph-image.tsxse pueden colocar en cualquier nivel de segmento de ruta. Un archivo enapp/blog/[slug]/opengraph-image.tsxgenera imágenes OG únicas por publicación de blog. - La exportación
runtime = "edge"asegura que la imagen se genere en el edge para tiempos de respuesta rápidos. También puedes usarruntime = "nodejs"si necesitas APIs de Node.js. generateMetadatay metadatos basados en archivos pueden coexistir. Las imágenes basadas en archivos tienen prioridad para sus campos de metadatos específicos.- Next.js añade encabezados de caché apropiados a las imágenes generadas, por lo que se almacenan en caché por CDNs y navegadores.
Variaciones
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" },
],
};
}Notas de TypeScript
ImageResponsese importa desdenext/og. Acepta JSX como su primer argumento y un objeto de opciones como el segundo.- El tipo
Metadatadesdenextproporciona escritura completa para todos los campos de metadatos incluyendoopenGraph,twittereicons. - Las funciones de metadatos dinámicos deben retornar
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],
},
};
}Trampas
- JSX de
ImageResponseno es React completo. Admite un subconjunto de CSS (solo diseño flexbox, sin grid, sinposition: absolute, las alternativas son limitadas). Consulta la documentación de Satori para las propiedades CSS admitidas. - Los archivos de fuente deben cargarse como
ArrayBuffer. No puedes usar directamente CSS@font-faceo URLs de Google Fonts enImageResponse. - Las imágenes OG deben tener exactamente 1200x630 píxeles para una visualización óptima en todas las plataformas de redes sociales. Las tarjetas de Twitter también utilizan 1200x630 para
summary_large_image. favicon.icodebe estar en el directorio raíz deapp/. Colocarlo en un subdirectorio no funcionará.- Las rutas de imágenes OG dinámicas se añaden a tus invocaciones de Edge Function. Si tienes miles de páginas, considera estrategias de almacenamiento en caché o generación estática.
- Satori no admite todas las propiedades de CSS. Notablemente,
background-imageconurl()no está soportado. Usa la etiquetaimgen JSX en su lugar. - La exportación
altenopengraph-image.tsxes requerida para accesibilidad. Se convierte en la etiqueta metaog:image:alt. - Los íconos de toque de Apple deben tener 180x180 píxeles. iOS genera automáticamente otros tamaños.
Alternativas
| 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 |
Preguntas Frecuentes
¿Qué nombres de archivo detecta automáticamente Next.js App Router para favicons e imágenes OG?
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).
¿Cómo genera imágenes ImageResponse desde next/og?
- Renderiza JSX usando Satori (que convierte HTML/CSS a SVG).
- El SVG se convierte luego a PNG.
- Admite solo un subconjunto de CSS (solo flexbox, sin grid).
¿Cuál es el tamaño recomendado para imágenes Open Graph?
- 1200x630 píxeles para visualización óptima en todas las plataformas de redes sociales.
- Las tarjetas de Twitter
summary_large_imagetambién utilizan 1200x630.
Trampa: ¿Qué limitaciones de CSS existen dentro de JSX de ImageResponse?
- Solo se admite diseño flexbox -- sin CSS Grid.
background-imageconurl()no está soportado; usa una etiquetaimgen su lugar.- Las alternativas a
position: absoluteson limitadas. - Consulta la documentación de Satori para la lista completa de propiedades CSS admitidas.
¿Cómo cargas fuentes personalizadas en una imagen OG dinámica?
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.
¿Dónde debe colocarse favicon.ico en el proyecto?
- En el directorio raíz de
app/. - Colocarlo en un subdirectorio no funcionará.
- Next.js genera automáticamente la etiqueta
<link>correcta para él.
¿Cómo generas un favicon dinámico usando 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={{ background: "#0f172a", color: "white", fontSize: "20px" }}>A</div>,
{ ...size }
);
}¿Qué tipos de TypeScript se utilizan para generación de metadatos e imágenes OG?
import type { Metadata, ResolvingMetadata } from "next";Metadataproporciona escritura completa para camposopenGraph,twittereicons.generateMetadatadebe retornarPromise<Metadata>.ResolvingMetadatate permite acceder a metadatos padre para fusionar imágenes.
Trampa: ¿Por qué se requiere la exportación alt en opengraph-image.tsx?
- Se convierte en la etiqueta meta
og:image:altpara accesibilidad. - Sin ella, los lectores de pantalla y las plataformas de redes sociales no tienen descripción de la imagen.
¿Pueden coexistir metadatos basados en archivos y generateMetadata?
- Sí, pueden coexistir en el mismo segmento de ruta.
- Las imágenes basadas en archivos (por ejemplo,
opengraph-image.tsx) tienen prioridad para sus campos de metadatos específicos. generateMetadatamaneja otros campos como título y descripción.
¿Qué tamaño de ícono de toque de Apple debes usar?
- 180x180 píxeles.
- iOS genera automáticamente otros tamaños.
¿Cómo configuras un manifiesto de aplicación web con íconos en Next.js?
// 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" },
],
};
}