Next.js
Good enough to run our site on. Learn the caching rules first.
hashlogics.com itself runs Next.js App Router with Payload CMS embedded, static by default. This is what building it, and Shift Link on top of it, taught us.
The verdict
Next.js App Router is the right default for a content-heavy product that needs to render fast and rank well. It is the wrong tool for a team that has not budgeted time to learn its caching model, because that model is where most production surprises live.
The App Router earns its keep on static generation. This site prerenders every page to HTML at build time, so a page view never touches the database. We proved that by pointing a running build at a database that did not exist. Every page still served 200.
What costs teams time is that segment config and dynamic APIs opt a whole route tree into per-request rendering. One cookies() call in a shared layout puts every page under it back on the database. That is a design decision, and it needs to be made on purpose.
In short
5 things that decide this
- 01Static generation by default means a page view can skip the database entirely, which is the framework's real advantage over a plain client-rendered app.
- 02Segment config (dynamic, revalidate) is parsed from source before the module runs, so it must be a literal value. A computed expression fails the build with an unrecognised-config error.
- 03A component imported into a shared layout ships to every route under it, whether that route needs it or not. Client-only code in the wrong layout bloats every page's bundle.
- 04cookies() and headers(), called anywhere in a route's tree, opt that whole tree into dynamic rendering. One call in a layout takes every page beneath it with it.
- 05It is the right front end for a product that needs both a fast marketing site and an authenticated app, and a poor fit for a team unwilling to learn what triggers dynamic rendering.
For someone who has not shipped on it
Next.js is a React framework that decides, route by route, whether a page is built once ahead of time or rendered on every request. The App Router is its current routing system, built around React Server Components.
The pitch is one codebase for two halves of a product. A marketing site that should be static and fast, and an authenticated app that needs live data. Making that pitch real means knowing exactly which part of a page tree pulls each half in.
What we run on it
hashlogics.com is Next.js App Router with Payload CMS embedded in the same app: one repository, one deploy. Every page prerenders to HTML at build time. Publishing a change calls a targeted revalidation instead of a redeploy, so an edit goes live in seconds while readers keep hitting static pages.
Shift Link is a UK workforce compliance platform for healthcare and logistics staffing. It runs Next.js on the web alongside a React Native mobile app, both backed by Supabase. Shift managers and compliance staff use the web app, and it needs to load fast on a warehouse floor with patchy signal.
The pattern across both is the same. Content that does not change per request gets built once. Anything tied to a session or a live record earns dynamic rendering deliberately, rather than by accident.
- RouteStarts static by default.
- cookies() / headers()Called anywhere in the tree.
- Whole subtreeOpts into per-request rendering.
- Build outputCheck the prerender manifest, not the build log symbol.
The build log's static/dynamic marker can be cosmetic on a route using generateStaticParams. The prerender manifest is the source of truth.
Where it stands
Good at
- Static generation by default, which means a normal page view never has to reach the database.
- One framework for a fast marketing site and an authenticated app, instead of maintaining two.
- Server Components keep data-fetching code off the client bundle when written correctly.
- Preview and revalidation APIs let a CMS-backed page go live in seconds without a redeploy.
Weak at
- Segment config must be a literal value. A computed expression such as a ternary on an environment variable fails the build with an unrecognised-identifier error, because Next parses it from source before the module runs.
- One dynamic API called in a shared layout puts every page beneath it back on the database, and the effect is easy to miss because the route still returns 200.
- A client component imported into a shared layout ships its code to every route that layout wraps, even routes that never render it.
- The build log's static/dynamic symbol is not reliable proof for a route using generateStaticParams. We check the prerender manifest directly rather than trust the log.
A production build on this stack
Common questions
01Is Next.js good for a SaaS product?
For most SaaS products, yes, and the App Router's static-by-default behaviour is why. Marketing pages, docs and pricing render once and serve fast, while the authenticated app opts into dynamic rendering only where it needs live data. The risk is treating the whole app as dynamic by default and losing that advantage.
02What breaks when a team moves from Pages Router to App Router?
The mental model of what triggers server rendering. Pages Router made data fetching explicit per page with getServerSideProps. App Router can trigger it from one API call anywhere in a shared layout. That is easy to miss during a migration, and it shows up as slower pages with no obvious cause.
03Does Next.js lock you into Vercel?
The framework itself does not; Next.js is open source and can be self-hosted. Some features, like certain caching and image optimisation behaviours, are tuned for Vercel's infrastructure and need more configuration elsewhere. We cover the hosting side in our Vercel review.
04How much does App Router change the actual code?
Server Components change where data-fetching logic lives, and route groups and parallel routes give you layout control the Pages Router did not have. For a team already comfortable with React, the syntax is familiar. The learning curve is in the rendering and caching model, not the JSX.

