Panduan Remix SEO

Bagaimana Remix's meta(), loader(), headers(), dan tautan() route exports control SEO — plus apa changed sebagai Remix v2 menjadi React Router, now pada v8.

Pertama kali diterbitkan: 26 Jun 2026 · Terakhir diperbarui: 3 Agu 2026 · Lanjutan

TL;DR — Remix adalah SSR-oleh-default, so konten ships di awal HTML dengan Tidak render-queue dependency — architecture menghapus sebagian besar JS-SEO risk ketika SSR adalah apa sebenarnya deployed, though ini tidak oleh itself jaminan pengindeksan, peringkat, atau Core Web Vitals outcomes. berfungsi adalah di per-route exports: meta() (judul, deskripsi, OG, JSON-LD, dan dynamic canonicals melalui tagName: "link"), loader() (server data + nyata HTTP 404s/redirects), headers() (Cache-Control, X-Robots-Tag), dan links() (static canonicals, preloads). signature gotcha: nested routes drop parent meta unless Anda gabungkan ini melalui matches argument. Naming, dated: Remix v2’s APIs digabungkan ke React Router v7 di 2024; React Router adalah now pada v8 (v7 masih security-didukung); Remix 3 (beta) adalah sebuah terpisah, newer penuh-stack kerangka kerja, tidak sekadar React Router renamed. ini halaman covers Remix v2 / React Router v7-v8 route API.

SSR oleh default — mengapa Remix dimulai ahead

Remix’s server rendering adalah sebuah kerangka kerja kemampuan milik siapa deployment perilaku bergantung pada adapter dan route code. Bukti untuk klaim ini Primary standard or official documentation supporting the adjacent article claim. Cakupan: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Tingkat keyakinan: tinggi · Diverifikasi: Remix: Route meta Validate delivered HTML dan resources alih-alih assuming sebuah kerangka kerja default ensures SEO outcomes. Bukti untuk klaim ini Primary standard or official documentation supporting the adjacent article claim. Cakupan: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Tingkat keyakinan: tinggi · Diverifikasi: Google: JavaScript SEO basics

Cakupan note: walkthrough di bawah documents meta() / loader() / headers() / links() / ErrorBoundary route module API sebagai ini ada di Remix v2 dan React Router v7-v8 kerangka kerja mode — apa besar majority dari production Remix/React Router situs jalankan. Remix 3 (beta) adalah sebuah terpisah, ground-up rewrite dengan berbeda APIs dan tidak covered di sini; periksa yang satu Anda’re sebenarnya pada sebelum copying apa pun di bawah.

Bare React (CRA, Vite + React) ships sebuah empty <div id="root"></div> dan membangun halaman di browser. Google dapat render itu, tetapi Anda’ve signed up untuk render queue, statelessness, dan DOM-parity masalah covered di JavaScript SEO. Remix tidak memiliki itu default: ini executes Anda nested route tree (Root → Layout → Route) pada server dan, oleh default, mengirim konten-menyelesaikan HTML pada setiap document permintaan. kerangka kerja mode dapat juga menjadi configured untuk statically prerender routes di bangun time atau jalankan sebagai sebuah client-dirender SPA — so untuk apa pun diberikan deployment, konfirmasi sebenarnya rendering mode (melalui curl/View Sumber, di bawah) alih-alih assuming SSR dari kerangka kerja name.

«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. » (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian dua puluh tujuh, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)

setelah awal muat, Remix menggunakan client-side routing untuk subsequent navigations (like setiap React kerangka kerja). itu’s fine: Google discovers tautan dari SSR’d HTML dan melakukan crawl setiap URL sebagai -nya own server permintaan — setiap returning penuh HTML. ini tidak rely pada watching client-side route transitions. dan karena Remix’s <Link> renders sebuah nyata <a href>, tautan adalah dapat di-crawl oleh construction — Google “dapat hanya discover Anda links jika they adalah <a> HTML elements dengan an href attribute.” (terjemahan) “dapat hanya menemukan Anda tautan jika mereka adalah tidak terdefinisi HTML elements dengan sebuah tidak terdefinisi attribute.”

meta() export: judul, deskripsi, OG, JSON-LD

setiap route dapat export sebuah meta function returning sebuah array dari descriptor objects. Karena ini dapat baca loader data, metadata adalah dynamic dan set di awal HTML <head> — Tidak 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 },
      },
    },
  ];
};

{ "script:ld+json": {...} } descriptor renders sebuah proper <script type="application/ld+json"> tag di SSR’d <head> — terlihat untuk Google’s parser tanpa executing apa pun JavaScript. Martin Splitt’s preference applies: “We dukung JSON-LD di dynamically dirender konten, tetapi ini generally better untuk memiliki it di itu awal HTML.” (terjemahan) “kami mendukung JSON-LD di dynamically dirender konten, tetapi ini adalah umumnya better untuk memiliki ini di awal HTML.” di Remix, awal HTML adalah default place ini lands.

Nested routes dan meta inheritance — umum trap

ini adalah Remix SEO mistake. Remix takes terakhir matching route dengan sebuah meta export dan menggunakan itu — parent meta adalah dropped. A child itu exports its own meta() tanpa merging silently loses setiap tag itu root set (site-wide description, root OG tags, dan seterusnya.):

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

perbaiki adalah matches argument — flatten dan spread parent meta:

// ✅ 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" }];
};

jika Anda set global tags, put truly universal ones (charset, viewport) secara langsung di root.tsx’s JSX di mana merging tidak pernah strips them, dan reserve meta() untuk halaman-tingkat sinyal Anda sebenarnya ingin untuk override per route.

loader() function: nyata 404s, nyata redirects, dynamic meta

Loaders jalankan server-side hanya — DB kueri, API panggilan, dan secrets tidak pernah reach Googlebot. SEO payoff adalah correct HTTP kode status:

// 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.” » (Terjemahan) (Ringkasan bahasa Indonesia untuk bagian empat puluh dua, bagian kecil satu: teks sumber dipertahankan agar dapat diperiksa dalam tinjauan penutur asli.)

Whatever sebuah loader mengembalikan adalah exposed untuk client bahkan jika component tidak render ini — treat loaders like publik API endpoints dan jangan kembalikan secrets.

ini status propagation adalah apa happens pada sebuah direct document permintaan ( case sebuah crawler membuat). Client-side navigations setelah hydration adalah sebuah terpisah code path itu dapat behave differently, dan beberapa hosting adapters rewrite atau intercept thrown respons — verify keduanya direct-permintaan dan post-hydration perilaku pada Anda sebenarnya deployment alih-alih assuming ini dari loader code alone.

headers() function: Cache-Control dan X-Robots-Tag

Per-route header HTTP — lever untuk CDN caching (lebih cepat TTFB → better Core Web Vitals) dan untuk robots directives itu berfungsi bahkan pada crawler itu jangan baca HTML body:

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 di header tingkat reaches crawler itu tidak pernah parse body. oleh default hanya deepest route’s headers() berjalan di sebuah nested tree, so simplest pattern adalah untuk define headers pada leaf routes hanya dan hindari gabungkan complexity.

sebuah meta-noindex caveat worth knowing regardless dari kerangka kerja, dari Martin Splitt: “Itu noindex tag dapat penyebab Google untuk skip rendering entirely. So jika Anda trying untuk noindex melalui JavaScript, Anda mungkin menjadi creating a situation di mana Google never even berjalan itu JavaScript untuk see itu noindex.” (terjemahan) “ tidak terdefinisi tag dapat penyebab Google untuk skip rendering entirely. So jika Anda’re trying untuk tidak terdefinisi melalui JavaScript, Anda dapat menjadi membuat sebuah situation di mana Google tidak pernah bahkan berjalan JavaScript untuk see noindex.” Remix sidesteps ini karena directive lands di SSR’d <head> (atau sebuah header HTTP), tidak di client-hanya JavaScript.

links() injects <link> elements (favicons, stylesheet preloads, static canonicals). tetapi ini memiliki Tidak access untuk loader data — ini adalah static per route module:

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

untuk dynamic canonicals — paginated, filtered, atau parameterized URLs — gunakan meta() dengan tagName: "link", yang melakukan see data, params, dan location:

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

aturan: static canonical → links(); dynamic canonical → meta() dengan tagName: "link". ini matches Martin Splitt’s advice untuk set canonicals di HTML alih-alih JavaScript — “set Anda canonical di itu HTML, tidak dengan JavaScript… ini sekadar lebih fragile.” (terjemahan) “set Anda canonical di HTML, tidak dengan JavaScript… ini adalah hanya lebih fragile.” Keduanya Remix approaches render ke server HTML. (See tag canonical deep dive.)

Error boundaries dan 404/5xx halaman

Ketika sebuah loader, tindakan, atau component throws, route’s ErrorBoundary renders — di place, di dalam surviving layout (nav, footer stay). Paired dengan sebuah thrown 404 respons, Anda mendapatkan sebuah nyata 404 status dan sebuah usable halaman:

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>;
}

sebuah root-tingkat ErrorBoundary di app/root.tsx catches apa pun route boundaries jangan. myth itu “throwing a 404 breaks itu layout” (terjemahan) “throwing sebuah 404 breaks layout” adalah salah — boundary renders di dalam route hierarchy.

Sitemaps dan robots.txt sebagai routes

Remix memiliki Tidak dibangun-di sitemap generator; Anda bangun them sebagai resource routes. bracket notation escapes dot so URL adalah secara harfiah /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 mengikuti yang sama app/routes/robots[.txt].tsx pattern, returning text/plain.

Remix, React Router, dan 2026 product split — sebuah dated timeline

ini memiliki gotten lebih layered since original 2024 gabungkan, so treat ini sebagai history plus saat ini state alih-alih sebuah single fact:

  • Dec 2024 — Remix v2 merges ke React Router v7. @remix-run/react dan @remix-run/node consolidate ke react-router; @remix-run/* packages masih berfungsi sebagai sebuah bridge. apa SEO-relevant dari ini move:
  • routes.ts — sebuah routing config file; routes dapat menjadi didefinisikan programmatically untuk lebih flexible struktur URL.
  • Static pre-rendering — individual routes dapat opt ke bangun-time HTML generation (SSG-style), atau SPA mode — configuration choices, tidak automatic.
  • End-untuk-end jenis safetyloader kembalikan jenis flow ke meta(), reducing bugs di dynamic meta generation.
  • Vite — default bundler since Remix v2; route-tingkat code splitting mempertahankan client bundles lean (baik untuk TBT/LCP).
  • Apa tidak perubahan: meta()/loader()/headers()/links()/ ErrorBoundary pattern dan <Meta /> / <Links /> / <Scripts /> components di root.tsx. Existing Remix v2 SEO code carries di atas dengan minimal perubahan.
  • June 2026 — React Router v8 ships. React Router v7 tetap security-didukung; Remix v2 dan React Router v6 adalah now end-dari-life. route module SEO API ini halaman documents (meta, loader, headers, links, ErrorBoundary) adalah yang sama shape di seluruh v7 dan v8 — periksa Anda installed versi’s changelog untuk apa pun descriptor-tingkat perubahan sebelum copying code verbatim.
  • juga sebagai dari 2026 — Remix adalah Tidak lebih lama hanya “the old name for React Router.” (terjemahan) “ old name untuk React Router.” Remix team mendeskripsikan React Router sebagai mereka React meta-kerangka kerja, dan Remix (now di sebuah “Remix 3” (terjemahan) “Remix 3” beta) sebagai sebuah terpisah, newer penuh-stack kerangka kerja — “satu team, dua projects.” (terjemahan) “satu team, dua projects.” jika Anda’re auditing atau membangun pada @remix-run/* / react-router route exports, ini artikel’s API guidance applies secara langsung. jika Anda’re pada Remix 3, verify -nya documentation independently — meta/loader/headers/tautan pattern described di sini adalah tidak confirmed untuk carry di atas unchanged.

Bottom line untuk sebagian besar situs di production hari ini: “Remix SEO” (terjemahan) “Remix SEO” dan “React Router SEO” (terjemahan) “React Router SEO” adalah masih yang sama route API dan yang sama practical topic. hanya jangan assume itu juga covers Remix 3.

Anda masih jangan perlu dynamic rendering

Karena Remix menyajikan HTML pada server, Anda tidak pernah perlu Rendertron atau 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.” (terjemahan) “Dynamic rendering adalah sebuah workaround dan tidak sebuah panjang-istilah solusi… ini membuat additional complexities dan resource requirements.” SSR adalah recommended path, dan Remix memberikan ini untuk Anda oleh default.

Auditing sebuah Remix situs

  • periksa awal HTML dengan curl atau View Sumber — tidak DevTools Elements (yang menampilkan post-hydration DOM).
  • Verify nyata HTTP kode status pada missing halaman (404, tidak 200) dan redirects (301/302).
  • Test dengan GSC pemeriksaan URL — Remix’s disajikan HTML seharusnya match dirender view.
  • cari nested-meta gabungkan bugs — konfirmasi child halaman masih carry situs-wide deskripsi dan OG tags.
  • Validate JSON-LD dengan Rich hasil Test, dan jalankan sebuah crawl (Ahrefs situs Audit, Screaming Frog) untuk catch blocked assets dan depth issues.

None dari di atas adalah sebuah jaminan dari pengindeksan, rankings, atau Core Web Vitals — SSR, correct kode status, dan bersih metadata hapus architecture-tingkat risk; rest masih bergantung pada konten quality, deployment adapter, dan bagaimana halaman performs terhadap semuanya else di SERP.

untuk base library underneath Remix, see React SEO; untuk kerangka kerja-agnostic rendering aturan, see JavaScript SEO dan JavaScript kerangka kerja hub.

Tambahkan catatan pakar

Sematkan kutipan pakar

Orang baru? Buat profilnya yang belum diklaim di /admin/experts/ → Sematkan kutipan pakar terlebih dahulu.