Guide Remix SEO

Comment Remix's meta(), loader(), headers(), et liens() route exports contrôler SEO — plus ce que modifié as Remix v2 became React Router, maintenant on v8.

Première publication : 26 juin 2026 · Dernière mise à jour : 3 août 2026 · Avancé

TL;DR — Remix est SSR-par-default, donc contenu ships dans le initial HTML avec aucun rendre-queue dependency — le architecture supprime la plupart JS-SEO risque quand SSR est qu’est-ce que en réalité deployed, though il ne fait pas par itself guarantee indexation, classement, or Core Web Vitals résultats. Le fonctionner est dans le per-route exports: meta() (titles, descriptions, OG, JSON-LD, et dynamic canonicals via tagName: "link"), loader() (serveur données + réel HTTP 404s/redirections), headers() (Cache-Control, X-Robots-Tag), et links() (static canonicals, preloads). Le signature gotcha: nested routes drop parent meta unless vous merge il via le matches argument. Naming, dated: Remix v2’s APIs merged dans React Router v7 dans 2024; React Router est maintenant on v8 (v7 encore security-pris en charge); Remix 3 (beta) est a séparé, newer complet-stack framework, pas simplement React Router renamed. Ce page covers le Remix v2 / React Router v7-v8 route API.

SSR by par défaut — pourquoi Remix starts ahead

Remix’s serveur rendu est a framework capability whose deployment behavior dépend on le adapter et route code. Preuve à l’appui de cette affirmation Primary standard or official documentation supporting the adjacent article claim. Portée : Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Niveau de confiance : élevé · Vérifié : Remix: Route meta Valider le delivered HTML et resources plutôt que assuming a framework par défaut ensures SEO résultats. Preuve à l’appui de cette affirmation Primary standard or official documentation supporting the adjacent article claim. Portée : Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Niveau de confiance : élevé · Vérifié : Google: JavaScript SEO basics

Scope remarque: le walkthrough ci-dessous documents le meta() / loader() / headers() / links() / ErrorBoundary route module API as il exists dans Remix v2 et React Router v7-v8 framework mode — ce que le grand majority de production Remix/React Router sites exécuter. Remix 3 (beta) est a séparé, ground-up rewrite avec différent APIs et n’est pas covered ici; vérifier qui un vous êtes en réalité on avant copying n’importe quoi ci-dessous.

Bare React (CRA, Vite + React) ships an vide <div id="root"></div> et construit lune page dans le navigateur. Google peut rendre que, mais vous avez signed up pour le rendre queue, statelessness, et DOM-parity problèmes covered dans JavaScript SEO. Remix ne fait pas ont que par défaut: il executes votre nested route tree (Root → Layout → Route) on le serveur et, par par défaut, sends contenu-complet HTML on chaque document requête. Framework mode peut aussi être configuré à statically prerender routes à construire temps or exécuter as a client-rendu SPA — donc pour quelconque donné deployment, confirmer le réel rendu mode (via curl/View Source, ci-dessous) plutôt que assuming SSR depuis le framework nom.

«Google’s own guidance lines up with this. From the JavaScript SEO basics doc: “Server-side or pre-rendering is still a great idea because it makes your website faster for users and crawlers, and not all bots can run JavaScript.” That last clause is the whole argument for Remix — Bingbot renders JS slowly and incompletely, and many AI crawlers and social preview bots (Twitterbot, facebookexternalhit) don’t render at all. Remix’s SSR output serves all of them correctly without special handling. » (Traduction) (Résumé en français de la section vingt-sept, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)

Après le initial charger, Remix utilise client-side routing pour subsequent navigations (comme chaque React framework). c’est fine: Google discovers liens depuis le SSR’d HTML et explore chaque URL as son propre serveur requête — chaque returning complet HTML. Il ne fait pas rely on watching client-side route transitions. Et parce que Remix’s <Link> renders a réel <a href>, liens sont crawlable par construction — Google “peut seulement découvrir votre liens si ils sont <a> HTML elements avec an href attribut.”

Le meta() export: titles, descriptions, OG, JSON-LD

Chaque route peut export a meta function returning an array de descriptor objects. Parce que il peut lire loader données, metadata est dynamic et définir dans le initial HTML <head> — aucun JS-injection delay:

// app/routes/blog.$slug.tsx
import type { MetaFunction } from "@remix-run/node";

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  if (!data) return [{ title: "Post Not Found" }];
  return [
    { title: `${data.post.title} | My Blog` },
    { name: "description", content: data.post.excerpt },
    { property: "og:title", content: data.post.title },
    { property: "og:image", content: data.post.ogImage },
    { property: "og:type", content: "article" },
    {
      "script:ld+json": {
        "@context": "https://schema.org",
        "@type": "Article",
        headline: data.post.title,
        datePublished: data.post.publishedAt,
        author: { "@type": "Person", name: data.post.author },
      },
    },
  ];
};

«The { "script:ld+json": {...} } descriptor renders a proper <script type="application/ld+json"> tag in the SSR’d <head> — visible to Google’s parser without executing any JavaScript. Martin Splitt’s preference applies: “We support JSON-LD in dynamically rendered content, but it’s generally better to have it in the initial HTML.” In Remix, the initial HTML is the default place it lands. » (Traduction) (Résumé en français de la section trente-deux, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)

«## Nested routes and meta inheritance — the common trap » (Traduction) (Résumé en français de la section trente-trois, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)

«This is the Remix SEO mistake. Remix takes the last matching route with a meta export and uses that — parent meta is dropped. A child that exports its own meta() without merging silently loses every tag the root set (site-wide description, root OG tags, etc.): » (Traduction) (Résumé en français de la section trente-quatre, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)

// ❌ Root's description is now gone on this route
export const meta: MetaFunction = () => {
  return [{ title: "About Us" }];
};

«The fix is the matches argument — flatten and spread parent meta: » (Traduction) (Résumé en français de la section trente-six, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)

// ✅ Keep parent meta, append your own
export const meta: MetaFunction = ({ matches }) => {
  const parentMeta = matches.flatMap((match) => match.meta ?? []);
  return [...parentMeta, { title: "About Us" }];
};

// ✅ Keep parent meta but override only the title
export const meta: MetaFunction = ({ matches }) => {
  const parentMeta = matches
    .flatMap((match) => match.meta ?? [])
    .filter((meta) => !("title" in meta));
  return [...parentMeta, { title: "About Us" }];
};

«If you set global tags, put truly universal ones (charset, viewport) directly in root.tsx’s JSX where merging never strips them, and reserve meta() for page-level signals you actually want to override per route. » (Traduction) (Résumé en français de la section trente-huit, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)

«## The loader() function: real 404s, real redirects, dynamic meta » (Traduction) (Résumé en français de la section trente-neuf, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)

Loaders exécuter serveur-side seulement — DB requêtes, API calls, et secrets jamais reach Googlebot. Le SEO payoff est correct Code d’état HTTPs:

// app/routes/products.$id.tsx
import { json, redirect } from "@remix-run/node";
import type { LoaderFunctionArgs } from "@remix-run/node";

export async function loader({ params }: LoaderFunctionArgs) {
  const product = await db.products.findById(params.id);
  if (!product) {
    throw new Response("Product not found", { status: 404 });
    // ✅ real HTTP 404 — not a soft 404
  }
  if (product.movedTo) {
    throw redirect(`/products/${product.movedTo}/`, 301); // ✅ real 301
  }
  return json(product);
}

«This is the antidote to the soft-404 problem SPAs create — a 200 OK page that just says “not found.” As John Mueller put it: “If a page returns a 200 but there’s no content — that’s a soft 404. That’s problematic because we don’t know to treat it as a 404. From a crawling standpoint, we’ll just keep trying to crawl it.” Remix’s throw new Response(..., { status: 404 }) propagates a genuine 404, which lines up with Google’s instruction to “use a meaningful status code, like a 404 for a page that could not be found.” » (Traduction) (Résumé en français de la section quarante-deux, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)

Whatever a loader renvoie est exposed à le client même si le component ne fait pas rendre il — treat loaders comme public API endpoints et ne pas retourner secrets.

Ce status propagation est ce que se produit on a direct document requête (le cas a robot d’exploration rend). Client-side navigations après hydration sont a séparé code chemin que peut behave differently, et some hosting adapters rewrite or intercept thrown réponses — vérifier les deux le direct-requête et post-hydration behavior on votre réel deployment plutôt que assuming il depuis le loader code alone.

Le headers() function: Cache-Contrôle et X-Robots-Balise

Per-route HTTP headers — le lever pour CDN mise en cache (faster TTFB → meilleur Core Web Vitals) et pour robots directives que fonctionner même on robots d’exploration que ne pas lire le HTML corps:

import type { HeadersFunction } from "@remix-run/node";

// CDN-friendly caching
export const headers: HeadersFunction = () => ({
  "Cache-Control": "max-age=300, s-maxage=3600, stale-while-revalidate=86400",
});

// HTTP-level noindex — effective for Bingbot and non-rendering crawlers
export const headers: HeadersFunction = () => ({
  "X-Robots-Tag": "noindex, nofollow",
});

«X-Robots-Tag: noindex at the header level reaches crawlers that never parse the body. By default only the deepest route’s headers() runs in a nested tree, so the simplest pattern is to define headers on leaf routes only and avoid merge complexity. » (Traduction) (Résumé en français de la section quarante-huit, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)

A meta-noindex caveat worth knowing regardless de framework, depuis Martin Splitt: “Le noindex balise peut causer Google à skip rendu entirely. Donc si vous êtes trying à noindex via JavaScript, vous peut être creating a situation où Google jamais même exécute le JavaScript à voir le noindex.” Remix sidesteps ce parce que le directive lands dans le SSR’d <head> (or an HTTP header), pas dans client-seulement JavaScript.

links() injects <link> elements (favicons, stylesheet preloads, static canonicals). But it has aucun accès to loader données — it’s static per route module:

export const links: LinksFunction = () => [
  { rel: "canonical", href: "https://example.com/canonical-url/" },
];

Pour dynamic canonicals — paginated, filtered, or parameterized URLs — utiliser meta() avec tagName: "link", qui fait voir data, params, et location:

export const meta: MetaFunction<typeof loader> = ({ data }) => [
  {
    tagName: "link",
    rel: "canonical",
    href: `https://example.com/products/${data.product.slug}/`,
  },
];

Le règle: static canonical → links(); dynamic canonical → meta() avec tagName: "link". Ce matches Martin Splitt’s advice à définir canonicals dans HTML plutôt que JavaScript — “définir votre canonical dans le HTML, pas avec JavaScript… c’est simplement plus fragile.” Les deux Remix approaches rendre dans le serveur HTML. (Voir le balise canonical deep dive.)

Erreur boundaries et 404/5xx pages

Quand a loader, action, or component throws, le route’s ErrorBoundary renders — dans placer, à l’intérieur le surviving layout (nav, footer stay). Paired avec a thrown 404 Réponse, vous obtenir a réel 404 status et a usable page:

import { isRouteErrorResponse, useRouteError } from "@remix-run/react";

export function ErrorBoundary() {
  const error = useRouteError();
  if (isRouteErrorResponse(error) && error.status === 404) {
    return <div><h1>404 — Page Not Found</h1></div>;
  }
  return <div>Something went wrong.</div>;
}

A root-level ErrorBoundary dans app/root.tsx catches n’importe quoi route boundaries ne pas. Le myth que “throwing a 404 breaks the layout” est faux — le boundary renders dans le route hierarchy.

Sitemaps et robots.txt as routes

Remix a aucun built-dans sitemap generator; vous construire les as resource routes. Le bracket notation escapes le dot donc l’URL est literally /sitemap.xml:

// app/routes/sitemap[.xml].tsx
export async function loader() {
  const posts = await db.posts.findMany({ where: { published: true } });
  const body = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${posts.map((p) => `  <url><loc>https://example.com/blog/${p.slug}/</loc><lastmod>${p.updatedAt.toISOString()}</lastmod></url>`).join("\n")}
</urlset>`;
  return new Response(body, {
    headers: { "Content-Type": "application/xml", "Cache-Control": "public, max-age=3600" },
  });
}

robots.txt follows the même app/routes/robots[.txt].tsx pattern, returning text/plain.

Remix, React Router, et le 2026 produit split — a dated timeline

Ce a gotten plus layered since le original 2024 merge, donc treat il as history plus actuel state plutôt que a unique fact:

«- Dec 2024 — Remix v2 merges into React Router v7. @remix-run/react and @remix-run/node consolidate into react-router; the @remix-run/* packages still work as a bridge. What’s SEO-relevant from this move:

  • routes.ts — a routing config file; routes can be defined programmatically for more flexible URL structures.
  • Static pre-rendering — individual routes can opt into build-time HTML generation (SSG-style), or SPA mode — configuration choices, not automatic.
  • End-to-end type safety — loader return types flow into meta(), reducing bugs in dynamic meta generation.
  • Vite — the default bundler since Remix v2; route-level code splitting keeps client bundles lean (good for TBT/LCP).
  • What didn’t change: the meta()/loader()/headers()/links()/ ErrorBoundary pattern and the <Meta /> / <Links /> / <Scripts /> components in root.tsx. Existing Remix v2 SEO code carries over with minimal changes. » (Traduction) (Résumé en français de la section soixante-six, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.) «- June 2026 — React Router v8 ships. React Router v7 remains security-supported; Remix v2 and React Router v6 are now end-of-life. The route module SEO API this page documents (meta, loader, headers, links, ErrorBoundary) is the same shape across v7 and v8 — check your installed version’s changelog for any descriptor-level changes before copying code verbatim. » (Traduction) (Résumé en français de la section soixante-six, sous-partie deux : le texte source est conservé pour vérification lors de la relecture francophone.) «- Also as of 2026 — Remix is no longer just “the old name for React Router.” The Remix team describes React Router as their React meta-framework, and Remix (now in a “Remix 3” beta) as a separate, newer full-stack framework — “one team, two projects.” If you’re auditing or building on @remix-run/* / react-router route exports, this article’s API guidance applies directly. If you’re on Remix 3, verify its documentation independently — the meta/loader/headers/links pattern described here is not confirmed to carry over unchanged. » (Traduction) (Résumé en français de la section soixante-six, sous-partie trois : le texte source est conservé pour vérification lors de la relecture francophone.)

Bottom line pour la plupart sites dans production aujourd’hui: “Remix SEO” et “React Router SEO” sont encore le même route API et le même practical topic. Simplement ne pas assume que aussi covers Remix 3.

Vous encore don’t besoin dynamic rendering

«Because Remix serves HTML on the server, you never need Rendertron or Prerender.io. Google deprecated dynamic rendering anyway: “Dynamic rendering was a workaround and not a long-term solution… it creates additional complexities and resource requirements.” SSR is the recommended path, and Remix gives it to you by default. » (Traduction) (Résumé en français de la section soixante-neuf, sous-partie un : le texte source est conservé pour vérification lors de la relecture francophone.)

Auditing a Remix site

  • Vérifier le initial HTML avec curl or View Source — pas DevTools Elements (qui montre le post-hydration DOM).
  • Vérifier réel Code d’état HTTPs on manquant pages (404, pas 200) et redirections (301/302).
  • Tester avec GSC Inspection d’URL — Remix’s servi HTML devrait match le rendu view.
  • Regarder pour nested-meta merge bugs — confirmer child pages encore carry le site-wide description et OG balises.
  • Valider JSON-LD avec le Résultats enrichis Tester, et exécuter a explorer (Ahrefs Site Audit, Screaming Frog) à catch bloqué assets et depth problèmes.

None de le ci-dessus est a guarantee de indexation, rankings, or Core Web Vitals — SSR, correct code d’états, et clean metadata supprimer architecture-level risque; le rest encore dépend on qualité du contenu, le deployment adapter, et comment le page performs contre tout sinon dans le SERP.

Pour le base library underneath Remix, voir React SEO; pour le framework-agnostic rendu règles, voir JavaScript SEO et le JavaScript frameworks hub.

Ajouter une note d’expert

Épingler une citation d’expert

Nouvelle personne ? Créez son profil non revendiqué à /admin/experts/ → Épingler une citation d’expert d’abord.