Home/ Insights/ Architecture

Astro over Next.js — the frontend decision that mostly never gets made

Architecture 11 February 2026 8 min read Michael Seliger

When a frontend is chosen, the decision remarkably often falls only between the "big" ones: Next, Nuxt, maybe SvelteKit. The question that comes before — does this site even need that? — gets skipped.

I worked this through on my own site: migrated from a static Astro site to Astro plus Payload CMS. The reason was not framework frustration but content maintenance — and the wish to check, on a real project, what the technology behind it actually delivers. It could just as well have been a Next-to-Astro move; the lesson is the same.

The decision that mostly never gets made

The real problem is not "Astro or Next" but that the choice is reduced to "which of the big frameworks". Astro rarely even makes the list — although for a website, a landing page or a marketing site it is completely sufficient and, on maintenance, performance and bloat, often the better choice.

Where Astro concretely wins: islands and JS load

This is the core. Most marketing sites show content, a slider, a bit of animation. A SPA-first framework ships a JavaScript runtime for that — whether you use it or not. Astro's islands model ships HTML by default and JavaScript only for the few interactive spots. Less JavaScript shipped is the single biggest lever for page speed.

That is exactly why good page speed scores come more easily with Astro — not because Astro has a magic optimizer, but because the default is "no JavaScript until asked". You reach the scores by not shipping the problem in the first place. SSR works just as it does elsewhere — you are simply lighter on your feet.

Where Next.js stays the right choice

Next has its place, and it is a real one: e-commerce, large applications with lots of dynamic content, deeply interactive product surfaces, a shared stack with the backend. There the weight buys something.

But even there it is now worth checking whether Astro carries it as well. This is not an anti-Next text. The point is not to reach for Next reflexively for a brochure site, just because it is the familiar tool.

The apparent contradiction: this site also runs on Next

The punchline: this very site contains a Next.js app. It is a pnpm monorepo of two parts — the public frontend in Astro 6 with Tailwind, and a Payload CMS that, because Payload v3 is Next.js-native, runs as a thin Next app.

Broken down to the essentials it looks like this:

michaelseliger.com/                  pnpm monorepo
├── apps/
│   ├── web/                          Astro 6 — the public site
│   │   └── src/
│   │       ├── pages/                file routing (/, /de/...)
│   │       └── lib/payload.ts        the ONLY place that calls the CMS
│   └── cms/                          Payload on Next.js — CMS host only
│       └── src/
│           ├── collections/          Posts, Pages, ...
│           └── payload-types.ts      auto-generated types
├── .env                              one source (per-app symlinks)
└── pnpm-workspace.yaml               packages: apps/*

The coupling between the two is deliberately thin — and exactly for that reason easy to rebuild. The web reads the CMS types via a tsconfig alias, without ever copying the file:

// apps/web/tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@cms-types": ["../cms/src/payload-types.ts"]
    }
  }
}

And it fetches the content through exactly one typed place, by API key against the Payload REST API:

// apps/web/src/lib/payload.ts (simplified)
import type { Post } from '@cms-types'

const PAYLOAD_URL = process.env.PAYLOAD_URL ?? 'http://localhost:3000'
const API_KEY = process.env.PAYLOAD_API_KEY ?? ''

async function payloadFetch<T>(path: string): Promise<T> {
  const headers = new Headers({ Accept: 'application/json' })
  if (API_KEY) headers.set('Authorization', `users API-Key ${API_KEY}`)
  const res = await fetch(`${PAYLOAD_URL}/api${path}`, { headers })
  if (!res.ok) throw new Error(`Payload ${res.status}`)
  return (await res.json()) as T
}

export const getPosts = (locale: 'de' | 'en') =>
  payloadFetch<{ docs: Post[] }>(`/posts?locale=${locale}`)

There is no more connection than that: one type alias and one fetch module. Next does what it is genuinely good at — the admin and app surface of the CMS, including live preview. Astro does what it is good at — fast, lean content delivery, bilingual DE/EN, deployed via Dokploy. The split here is the architecture, not a compromise.

Conclusion

The frontend decision is not "which of the big frameworks" but "what does this site actually do?". For marketing and content sites the answer is mostly Astro: less to maintain, less JavaScript, faster out of the box. You keep the heavy frameworks for the places where their weight buys something.

If a frontend decision is on your table right now and you want a sober assessment instead of a framework holy war: that is part of my work in Technical Consulting.