If you've pasted favicon <link> tags directly into app/root.tsx and watched Remix dedupe or silently drop them — the right pattern is the links export. This is where Remix v2 expects HTML link descriptors, and the framework renders them into the document head for you. (See the official Remix route links documentation for the full API surface.)

Prerequisites

You need a working Remix v2 project and a stack of favicon files. For the complete modern set, prepare:

Anything you drop into public/ is served from the site root, so /public/favicon.svg resolves to https://yoursite.com/favicon.svg with no extra routing.

Step 1: Build the links export in root.tsx

The links export is a function returning an array of <link> descriptors. Remix maps each descriptor onto a real HTML tag in the document head. Use LinksFunction from @remix-run/node (or your runtime-specific package) for proper typing.

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

export const links: LinksFunction = () =&gt; [
  { rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' },
  { rel: 'alternate icon', href: '/favicon.ico', type: 'image/x-icon' },
];

The rel values map directly onto the HTML spec — see whatwg's rel=icon definition for the full vocabulary. Browsers pick the first icon that matches their current context, so list your crisp SVG before the ICO fallback.

Step 2: Add Apple Touch and manifest entries

Remix treats every descriptor the same way regardless of resource type. Stack them in the same array:

export const links: LinksFunction = () =&gt; [
  { rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' },
  { rel: 'alternate icon', href: '/favicon.ico', type: 'image/x-icon' },
  { rel: 'apple-touch-icon', href: '/apple-touch-icon.png', sizes: '180x180' },
  { rel: 'manifest', href: '/site.webmanifest' },
];

The sizes attribute is informational — browsers consult it when picking the best fit for a given rendering context. For broader favicon link tag patterns beyond the Remix export, see our favicon HTML code examples.

Step 3: Ship dark-mode SVGs cleanly

Browsers do honor media on icon link tags. You can ship two SVGs and let the browser pick the right one based on the user's color scheme. Save a second file as /public/favicon-dark.svg, then add both descriptors:

{
  rel: 'icon',
  href: '/favicon.svg',
  type: 'image/svg+xml',
  media: '(prefers-color-scheme: light)',
},
{
  rel: 'icon',
  href: '/favicon-dark.svg',
  type: 'image/svg+xml',
  media: '(prefers-color-scheme: dark)',
},

The HTML spec explicitly supports media descriptors on icon links, so this is a first-class pattern — not a hack. For the SVG-side setup that makes these files swap correctly, our favicon dark mode guide walks through the media query syntax in detail.

Step 4: Cache-bust when you update icons

Browsers cache favicon.ico aggressively, and Remix does not add a content hash to public/ assets by default. Append a version query string when you ship a new design:

{ rel: 'icon', href: '/favicon.svg?v=2', type: 'image/svg+xml' }

Bump the v value on every icon release. Caches tied to URLs will refresh, and your users see the new tab graphic immediately.

Common pitfalls

Watch for these when wiring icons through the Remix links export:

Wrapping up

The Remix links export is the cleanest place to wire favicon HTML tags. You get type safety, predictable SSR output, and a single file to edit when you update your icon stack. Pair it with a sharp vector source from Mzu favicondl, ship a cache-bust query, and your Remix app will look professional in every browser tab.