Is your Remix app server-rendered? (By default, yes)
loader for every route by default and renders the page to complete HTML with that data before sending it, so most Remix routes return full content to crawlers. Content goes missing when a route uses a clientLoader to fetch its primary data in the browser, when key content is deferred and streamed, or when the app is built in SPA Mode.Remix is server-first by design — the loader model is the whole point of the framework — so it belongs with Astro and Gatsby on the reassuring side of this set, not with the client-first SPAs. The Remix diagnosis is about a few specific opt-outs from that default, each tied to Remix's loader and nested-routing model.
Why loaders make Remix safe by default
Every Remix route can export a loader, a function that runs on the server before the route renders. Remix calls the loaders for the matched route and its parents, resolves their data, renders the route tree to HTML with that data in place, and sends the finished page. Your component reads it through useLoaderData. By the time the HTML reaches the browser — or a crawler — the content is already in it.
This is why a well-built Remix route returns a near-100% content ratio with metadata intact before any JavaScript runs. The failures below are all cases where data deliberately does not come from a server loader.
The clientLoader trap
Remix added clientLoader for data that should be fetched in the browser — client-side caches, values that depend on browser-only state, or navigations you want to speed up after the first load. A clientLoader runs in the browser, not on the server, so anything it returns is not in the initial HTML.
The trap has two grades. If a route has both a server loader and a clientLoader, the server data is still in the HTML and only the client-fetched additions are missing — a HYBRID result. But if a route is defined with only a clientLoader and no server loader, that route has no server-resolved data at all: its content is assembled entirely in the browser, and a crawler gets a shell. That is a full CSR verdict on an otherwise server-first framework. Keep primary, indexable content in the server loader.
Deferred streaming and <Await>
Remix can stream. Wrapping a value in defer() lets the route respond immediately with the rest of the page while that value resolves, rendering a <Suspense> fallback in its place and streaming the real value in through <Await>once it's ready.
Nested routes render in parts
Remix's routing is nested: a URL is composed of a parent route and its children, each with its own loader. This is a strength, but it means a single URL can be partly server-rendered and partly not. A parent layout route with a proper server loader can render perfectly while a child route fetches its section client-side — so the page "works" and still hands a crawler a page with a hole in it where the child's content should be. When a Remix route reads HYBRID, check each route in the nesting chain, not just the leaf.
SPA Mode ships an empty shell
Remix can be built in SPA Mode, which turns off server rendering entirely and produces a single-page app: an empty HTML shell hydrated and rendered in the browser, exactly like a plain React SPA. It exists for cases where a server isn't available or wanted. If your Remix app is in SPA Mode, this check will return CSR, and the fix is not a Remix tweak — it is deciding whether that content needs to be indexable at all, and if so, moving off SPA Mode. See the React page for the shape of that problem.
Your configuration and the verdict to expect
Find your setup. On Remix, an unexpected amber or red almost always points to a client loader, a deferred value, or SPA Mode rather than to missing server rendering.
When Remix actually breaks
Remix rendering FAQ
Yes. Remix runs a server loader for every matched route, resolves the data, and renders the route tree to complete HTML before sending it. Server rendering is the framework's default behavior, not an opt-in, which is why most Remix routes deliver full content to crawlers without executing JavaScript.
