Skip to main content
ASoc
Comparison

Astro vs. Remix: Two Opposite Rendering Defaults

Astro assumes a page is static until a component opts in; Remix assumes a route is a server request until it opts out. This build's own 452-static/8-dynamic split shows why that matters.

The ASoc Team9 min read

Astro and Remix start from opposite assumptions about a route, and that decides more than either framework's feature list. Astro treats a page as static content unless a component opts into hydration (an island); Remix (now React Router v7's Framework Mode) treats a route as a live server request unless it opts into prerendering. This build's own split — 452 static routes, 8 dynamic — is a concrete case for why that starting assumption matters more than the syntax built on top of it.

The same question, asked in two directions

Every route in a web app is either built once (static) or answered per request (dynamic). Astro and Remix disagree about which one is the default:

AstroRemix / React Router v7 Framework Mode
Rendering default per routeStatic HTML, zero JS shipped, until a component opts inServer-rendered per request, until a route opts into prerender config
Interactivity modelIslands — hydrate one component at a time, in React, Vue, Svelte, or nothingWhole-route client hydration — the same component tree runs on the server and again in the browser
Data readsgetStaticPaths at build time, or a server endpoint for dynamic dataloader — runs on the server, colocated with the route
Data writesNo built-in mutation primitive — bring a form handler or an API routeaction — colocated with the same route as its loader
Content authoringContent Collections — typed frontmatter, auto-discovered by glob from src/content/Nothing built in — bring your own MDX bundler or a headless CMS
Fits a CDN with no serverYes, by defaultNo — even its "static" mode still runs through a build adapter, not a framework default

Neither column is wrong. They're optimized for opposite starting points: Astro for a site that's mostly words, Remix for an app that's mostly state.

What porting this storefront would actually cost each one

This build's own route census, from a fresh production build:

Route (app)
┌ ○ /                          (static)
├ ● /templates/[slug]          (static, 111 pages)
├ ● /blog/[slug]                (static, 152 pages)
├ ƒ /dashboard                   (dynamic)
├ ƒ /api/download                (dynamic)
├ ƒ /api/webhooks/lemonsqueezy   (dynamic)
└ ... 452 static, 8 dynamic total

Porting the static 452 to Astro costs almost nothing conceptually — every one of those routes is already "build once, serve everywhere," which is Astro's default with no configuration at all. The 8 dynamic routes (the dashboard, four auth forms, and two API routes) are the part Astro treats as the exception: each would need an island or a server endpoint opted in explicitly, the same 8 exceptions this codebase already isolates by hand.

Porting the same site to Remix inverts the exercise. Remix has no concept of "this route is just static" without the newer prerender array in its config — every one of the 452 static routes would need to be told, individually or by pattern, that it doesn't need a server request. The 8 dynamic routes are the ones that map onto Remix's actual strength: loader/action pairs are built for exactly the read-then-mutate shape a dashboard or a webhook handler has. React vs. Remix covers that colocation model — and this storefront's own Server Actions (checkout.ts, redemption.ts) already do the same job Remix's action does, just without a route-scoped file to hold it — in more depth; it doesn't need repeating here.

The practical read: a site shaped like this one (98% static) fits Astro's default with almost no fighting; the same site on Remix would spend real configuration convincing the framework that most of it doesn't need a server at all.

Content authoring is the other real difference, and it's not about rendering

Astro's Content Collections give typed frontmatter and automatic discovery: drop a file under src/content/posts/, and Astro's build step globs it, validates its frontmatter against a schema, and exposes it typed. Remix ships no equivalent — MDX has to be wired in yourself, the same choice this storefront made:

// next.config.ts
const withMDX = createMDX({
  options: {
    remarkPlugins: ["remark-gfm"], // GFM tables — MDX defaults to
    // CommonMark, which has no table syntax at all
    rehypePlugins: ["rehype-slug"], // id attributes on every heading
  },
});

But this storefront's own registry is the more interesting comparison, because it deliberately does not glob:

// src/lib/blog.ts
const postLoaders: Record<string, () => Promise<{ default: ComponentType }>> = {
  "nextjs-aws": () => import("@/content/blog/nextjs-aws.mdx"),
  "angular-vs-svelte": () => import("@/content/blog/angular-vs-svelte.mdx"),
  // … one explicit entry per published post, 152 of them
};

A wildcard dynamic import built from the slug — the Astro-Content-Collections-style approach — would work, and would auto-discover new files the way Astro's glob does. It was rejected on purpose: a typo in a slug resolves to a silent runtime 404 instead of a build failure, and nothing type-checks against the registry in src/data/blog.ts. npm test catches a missing loader entry specifically because the list is explicit, not globbed. Astro's Collections trade that build-time safety net for less boilerplate — a real tradeoff, not a strictly better default, and one worth naming explicitly if you're choosing a content layer rather than just a rendering model.

Deployment target follows the same split

Astro's default output needs nothing but a CDN — no process, no cold start, because there's no server unless you add one via an adapter. (SvelteKit vs. Remix runs the same request/response-first assumption against a framework that shares it, where the divergence lands on the component layer instead.) Remix assumes a running server (Node, Deno, or an edge runtime) is always part of the picture, even for routes that don't mutate anything; its "static" story is a build-time optimization layered on top of a framework built for request/response, not a CDN-first framework that grew a server story later. GitHub Pages Alternative covers what "needs zero compute" actually buys a static-heavy site, if that's the deciding factor rather than the DX question this post is about.

Troubleshooting

SymptomCauseFix
An Astro island doesn't hydrate on the clientThe component wasn't given a client:* directive, so Astro shipped it as static markup onlyAdd client:load, client:visible, or another hydration directive explicitly — nothing hydrates by default
A Remix route that should be static is still hitting the server on every requestNo prerender entry for that path in the React Router configAdd the route to prerender, or move truly static content to a static host in front of it
Astro Content Collections build fails on a valid-looking frontmatter fieldThe Zod schema in content.config.ts doesn't match what the file actually declaresUpdate the collection schema to match, or the field name in the frontmatter — Astro validates strictly by design
A Remix loader re-runs more often than expectedNo caching headers set on the response, so every navigation re-fetchesReturn Cache-Control headers from the loader, or use useFetcher's revalidation controls deliberately
MDX tables render as plain paragraphsremark-gfm isn't in the plugin list — MDX's default parser is CommonMark, which has no table syntaxAdd remark-gfm (Astro's MDX integration needs the same plugin explicitly; it isn't included by default either)

FAQ

Is Remix still a separate framework from React Router? Practically, no — Remix's app-building conventions (loader, action, nested routes) merged into React Router v7 as "Framework Mode." The name "Remix" now mostly refers to that mode, which is what this post and React vs. Remix both mean by it.

Can Astro do anything Remix does, like form mutations? Not with a built-in primitive. Astro pages can post to a server endpoint you write yourself, but there's no colocated action — you're wiring a fetch handler by hand, which is exactly the kind of work Remix's loader/action pair exists to avoid.

Can Remix render a mostly-static site without paying a server cost per request? Yes, via its prerender config — but it's an opt-in list, not Remix's default the way it is Astro's. A site with hundreds of static routes has to enumerate or pattern-match all of them; Astro assumes it from the start.

Which one fits a template marketplace like this one? Neither, as it happens — this storefront is Next.js, whose App Router already infers the static/dynamic split per route the way Astro does by default, while still offering Remix-style colocated mutations through Server Actions. That's the actual reason the choice here was neither: see React Server Components vs. Client Components for how the same tradeoff plays out inside one framework instead of between two.

Templates in this post

ASoc Pip, ASoc Quest, and ASoc Rally are three of the 66 landing templates in this catalog — every one of them, like this storefront, ships as close to 100% static output as Astro assumes by default.

Browse the full set: Next.js landing page templates and Tailwind landing page templates. For the framework-choice half of this decision, see Astro vs. Next.js for a Marketing Site and React vs. Remix.

Keep reading

Comparison9 min read

The Auth0 Alternative for a Postgres-Backed Next.js Stack

Six RLS policies, all for select, zero write policies anywhere. What authorization looks like when it's a property of the table instead of a service beside it.

Read more
Comparison8 min read

esbuild vs. Vite: What This Repo's Own Lockfile Says

Neither is a dependency here — but the Vite version vitest pulls in has already dropped esbuild for Rolldown, proven straight from the lockfile.

Read more