Si acabas de inicializar un nuevo proyecto en Remix, probablemente notaste una gran omisión: no hay un archivo index.html en la carpeta public esperando tus etiquetas <link>. Configurar correctamente el favicon en Remix requiere cambiar tu modelo mental sobre cómo manejar el <head> del documento. En las aplicaciones tradicionales de React, simplemente tiras un archivo .ico en la raíz y listo. Remix maneja las cosas de manera diferente, apoyándose fuertemente en las exportaciones basadas en rutas para gestionar los metadatos.

Mira las pestañas del navegador de GitHub. No solo sirven una imagen estática; su favicon cambia según el tema del sistema e incluso se actualiza dinámicamente para mostrar un punto azul cuando tienes notificaciones sin leer. Para lograr ese nivel de detalle en Remix, no puedes depender de la resolución mágica de archivos. Necesitas definir explícitamente tus recursos.

La filosofía de Remix: Explícito sobre implícito

A diferencia del App Router de Next.js, que escanea automáticamente tu directorio en busca de archivos llamados favicon.ico o icon.svg, Remix prefiere las declaraciones explícitas. Personalmente, prefiero este enfoque. El enrutamiento mágico de archivos es genial hasta que se rompe, y entonces te quedas escarbando en el código fuente del framework para averiguar por qué tu icono se está almacenando en caché incorrectamente.

En Remix, usas la exportación LinksFunction. Esta función devuelve un array de objetos que Remix mapea directamente a etiquetas <link> de HTML. Como tu favicon necesita aparecer en cada página de tu aplicación, el único lugar lógico para poner esto es en tu archivo app/root.tsx.

Paso 1: Prepara tu stack de recursos

Antes de escribir código, necesitas los archivos de imagen reales. En 2026, no necesitas 15 tamaños de iconos diferentes. Solo necesitas tres archivos específicos ubicados en tu directorio public:

Si solo tienes un logo en alta resolución, pásalo por Mzu favicondl. Generará exactamente este stack moderno y eliminará los metadatos innecesarios que inflan los archivos SVG.

Paso 2: La exportación links en root.tsx

Abre tu archivo app/root.tsx. Si usaste la plantilla por defecto de Remix, probablemente ya tengas una función links exportando tu hoja de estilos. Vamos a expandir ese array para incluir nuestro stack de favicon.

import type { LinksFunction } from '@remix-run/node';

export const links: LinksFunction = () => [
  // Respaldo heredado
  { rel: 'icon', href: '/favicon.ico', sizes: '48x48' },
  // Icono escalable moderno
  { rel: 'icon', href: '/icon.svg', type: 'image/svg+xml' },
  // Soporte para dispositivos Apple
  { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' }
];

Remix toma estos objetos y los inyecta en el componente <Links /> renderizado dentro del <head> de tu documento. El orden importa aquí. Los navegadores leen de arriba a abajo, y los navegadores modernos priorizarán correctamente el SVG sobre el archivo ICO si lo soportan.

Paso 3: Manejo del modo oscuro

Si tu logo es negro, desaparecerá por completo cuando un usuario cambie su navegador al modo oscuro. Como el array links de Remix acepta cualquier atributo de enlace HTML válido, podemos usar la propiedad media para intercambiar iconos basándonos en las preferencias del sistema del usuario.

export const links: LinksFunction = () => [
  { rel: 'icon', href: '/favicon.ico', sizes: '48x48' },
  {
    rel: 'icon',
    href: '/icon-light.svg',
    type: 'image/svg+xml',
    media: '(prefers-color-scheme: light)'
  },
  {
    rel: 'icon',
    href: '/icon-dark.svg',
    type: 'image/svg+xml',
    media: '(prefers-color-scheme: dark)'
  }
];

Esto es mucho más limpio que escribir CSS en línea dentro de un solo archivo SVG, y asegura que el navegador solo solicite el recurso que realmente necesita.

Avanzado: Favicons dinámicos con Loader Data

Aquí hay un error común: los desarrolladores intentan usar la exportación links para mostrar un indicador de notificaciones. ¿El problema? LinksFunction no tiene acceso a tu useLoaderData(). Se evalúa antes de que tu componente se renderice.

Si necesitas un favicon dinámico basado en el estado del usuario (como mensajes no leídos), debes omitir la exportación links para esa etiqueta específica y renderizar un elemento HTML estándar manualmente dentro del <head> de tu componente raíz.

export default function App() {
  const data = useLoaderData<typeof loader>();
  const faviconUrl = data.hasUnread ? '/icon-unread.svg' : '/icon.svg';

  return (
    <html lang='en'>
      <head>
        <Meta />
        <Links />
        {/* Favicon dinámico renderizado manualmente */}
        <link rel='icon' href={faviconUrl} type='image/svg+xml' />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

Este enfoque te da lo mejor de ambos mundos. Mantienes tus recursos estáticos (como el Apple Touch Icon) en la limpia exportación links, mientras manejas el estado dinámico directamente en tu árbol de React. Si vienes de una configuración estándar de favicon en React, este patrón te resultará muy familiar.

Pruebas finales

Una vez desplegado, los navegadores pueden ser notoriamente tercos a la hora de actualizar los iconos almacenados en caché. Si recargas y sigues viendo el viejo logo de Remix, probablemente estés lidiando con un problema de caché local. No pierdas horas depurando tu código; lee primero sobre cómo forzar una limpieza de caché del favicon.

Remix te obliga a ser explícito sobre el head de tu documento, lo que en última instancia conduce a menos errores y mejor rendimiento. Apégate al patrón de respaldo SVG + ICO, utiliza la exportación links para los recursos estáticos, y tendrás una presencia profesional en las pestañas del navegador que funcionará a la perfección en todos los dispositivos.