On this page

Chapter 1 of 8. Before you touch a config file, decide whether static export actually fits your site. This chapter is that decision, plus the list of things you give up by making it.

Here is the confusion that costs people a weekend. Next.js is a Node framework, so the reasonable-sounding conclusion is to install Node on your host, run next start, and call it done. On shared hosting that path fights you the whole way. You do not fully control the process manager, the host is tuned to serve files rather than supervise a long-running application, and the runtime environment is not the one Next.js assumes. The effort rarely pays off.

There is a second door, and it is the one this site walks through. Next.js can compile your entire site to plain HTML, CSS, and JavaScript at build time, then emit a folder you copy onto the host. No Node runs on the server, and there is no Vercel in the loop. The host serves flat files, which is the workload shared hosting handles well. That mechanism is static export, and captainrandom.co.uk (the site you are reading this on) ships exactly this way.

Next.js with no Vercel and no Node: the static-export model

Static export is one config key. You set output: 'export' in your Next config, run next build, and Next.js writes an out/ folder. Per the Next.js static-export guide, that folder holds "the HTML/CSS/JS assets for your application" and "can be deployed and hosted on any web server that can serve HTML/CSS/JS static assets."

That last clause is the whole deal. A cPanel host serves static assets, so the contract becomes short and unambiguous: drop out/ into public_html/ and the site works. Nothing has to stay running to keep it alive. This repo's CLAUDE.md states the contract in one line: "drop ./out/ into public_html/ and it works."

On this site the build entrypoint is the plain script you would expect:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "typecheck": "tsc --noEmit"
  }
}

next build with output: 'export' set does the export in the same pass. There is no separate next export step any more; Next.js v14.0.0 removed it in favour of the config key. If you are following an older tutorial that tells you to run next export, that tutorial is stale.

What static export forbids: no SSR, ISR, middleware, API routes, or Server Actions

This is the part you must internalise before you write a line of code. Every feature below will compile fine and then fail, either at build or silently in production. If your site needs any of them, static export is the wrong choice and you should stop here.

The static-export guide lists the unsupported features explicitly. These are the ones that catch people:

  • No API routes that read the request. Route Handlers render a static response at build time, and "only the GET HTTP verb is supported." The docs are blunt: "If you need to read dynamic values from the incoming request, you cannot use a static export." No POST endpoint, no reading cookies or headers per request.
  • No Server Actions. The form-submission-to-server model does not exist, because there is no server to submit to.
  • No middleware. No per-request interception and no auth gate that runs on the edge.
  • No Incremental Static Regeneration (ISR) and no revalidate. Pages are built once. To change them you rebuild and redeploy; there is no background regeneration.
  • No rewrites, redirects, or headers from next.config. Those need a server to apply them. On cPanel you move that job to .htaccess, which a later chapter in this course covers in full.
  • Dynamic routes need generateStaticParams. A dynamic segment with no static params, or with dynamicParams: true, has nothing to prerender. You must enumerate every path at build time.

This site's own CLAUDE.md reads like a compressed version of that list, because it was written by hitting every wall: "no API routes, no Server Actions, no middleware, no ISR, no revalidate." That line exists as a contributor guardrail. It stops anyone reintroducing something the export cannot ship.

What you keep, and why a content site fits perfectly

The forbidden list is long, so it is worth being clear about what survives. For a large class of sites, what survives is everything that matters. You keep the full App Router, layouts, React Server Components (running at build time), client components with all their interactivity, static generation of every route, and, for a blog or docs site especially, build-time file reading.

That build-time distinction is what unlocks a content site. Reading MDX files off disk with fs is fine, provided it happens during the build and never per request. The Next.js MDX guide confirms this directly: fs/globby to read content metadata "can only be used server-side," which under static export means at build time. This site's loader leans on that:

// src/lib/mdx.ts, running during `next build`, never at request time
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

const POSTS_DIR = path.join(process.cwd(), 'src/content/posts')

export function getPostBySlug(slug: string) {
  const fullPath = path.join(POSTS_DIR, `${slug}.mdx`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')
  const { data, content } = matter(fileContents)
  // …parse frontmatter, estimate reading time, return a Post
}

fs.readFileSync inside a request-time server render would be a liability. During a static build it is just a script reading files off disk. Every post on this site is an MDX file compiled to HTML this way. MDX itself, per the MDX project, is "a format that combines markdown with JSX," and it "compiles regular markdown to JavaScript." That compile-time step is why MDX slots into a static export cleanly: there is nothing left to interpret at runtime.

So the honest fit test is a content boundary. Marketing pages, a blog, docs, a portfolio, a course catalogue: if your content changes when you publish rather than per user, static export fits, and shared hosting is a fine home for it. This site is a personal blog with a newsletter. The reading surface is 100% static, and the one dynamic piece, newsletter signup, lives in a separate PHP endpoint on the same host rather than inside Next.js. That split is deliberate, and it keeps the export clean.

Deciding: the acceptance threshold

Static export fits when every one of these holds:

  1. Your pages are the same for every visitor. No per-request personalisation, no auth-gated server rendering.
  2. Content changes on a publish cadence you control. You are willing to rebuild and redeploy to update a page, rather than expecting continuous updates.
  3. You do not need a request-reading API route, Server Action, or middleware inside Next.js. A separate backend (PHP, a serverless function elsewhere) is allowed; it just sits outside the export.
  4. You can enumerate every route at build time, giving each dynamic segment a generateStaticParams.

If all four hold, static export to cPanel is a genuine win rather than a compromise. It is faster to serve, since flat files have no cold start, and cheaper to host, since there is no Node process and no Vercel bill. This whole course is the proof: the site you are reading is that build, on that host.

// DECISION

Why not Vercel?

The call

output: 'export'. The site is flat files in public_html/ on a cPanel host, deployed by FTP from GitHub Actions. The host is a dumb file server, on purpose.

Rejected
Vercel / Netlifyplatform coupling and a bill that scales with success, for a site whose needs are static
A VPSfull control but a second system to patch, monitor and secure. More ownership than the problem warrants.
What it costs

Everything the previous section lists: no ISR, no Server Actions, no middleware, no image optimisation service. Every dynamic feature must be rethought as build-time output or a separate PHP endpoint.

Revisit when

If a feature genuinely needs server rendering, not before. Three dynamic features later (newsletter, telemetry, search), the constraint still holds.

If even one of those fails, because you need real-time server logic inside Next.js, per-user rendering, or an endpoint that reads the request, do not force it. Static export will fail quietly in production, which is the hardest kind of failure to diagnose. Pick a host that runs Node, or split the dynamic piece out to its own service.

Assuming the four hold, the next chapter is the config that makes it real: the exact next.config.ts with output: 'export', trailingSlash, and images.unoptimized, and what each of those three lines is load-bearing for.

// EXERCISE

Run the fit audit on a project of your own

Pick a real project you want on shared hosting and put it through the acceptance threshold before writing any config. Inventory every dynamic feature it contains, test each against the four criteria, and write a one-page verdict that maps every forbidden feature to a build-time replacement, an external service, or a hard blocker.

Expected behaviour
  • A written inventory listing every API route, Server Action, middleware file, and revalidate usage the project contains
  • Each inventory item classified as replaceable at build time, movable to an external service, or a hard blocker
  • A pass or fail verdict recorded against each of the four acceptance criteria
  • For any failing criterion, a named alternative such as a separate backend or a Node host, rather than a workaround forced inside the export

PROVE IT Run a grep across the project for revalidate, use server, middleware, and route.ts, and paste the raw output next to your verdict so someone else can confirm nothing was missed.

// CHECKPOINT — FIT DECISION
multiple choice · auto-checked

A Server Action slips into a site you are shipping as a static export. What is the most likely outcome?

exact answer · auto-checked

Which Next.js version removed the separate next export command in favour of the config key?

open · self-checked

A client wants members-only personalised dashboards plus marketing pages that change quarterly. How does the acceptance threshold split this project?

Show answer

The dashboards fail the first criterion, pages being the same for every visitor, so they cannot live inside the export. The marketing pages pass all four criteria, since they change on a publish cadence and need no request-reading code. The honest split is a static export for the content, with the dynamic piece living outside Next.js on a separate backend or a host that runs Node.

↺ re-read: “Deciding: the acceptance threshold

Lived experience

Sources

  • How to create a static export of your Next.js application
    Next.js (Vercel)
    The output:'export' contract, the out/ artefact you drop onto any static host, and the exhaustive list of features static export disables
    nextjs.org
  • How to use markdown and MDX in Next.js
    Next.js (Vercel)
    Confirms fs/globby to read content 'can only be used server-side' — i.e. at build time — which is why the MDX loader is safe under export
    nextjs.org
  • What is MDX?
    MDX (mdxjs.com)
    Definition of MDX and why it compiles to JavaScript at build time, backing the content-authoring model this site uses
    mdxjs.com
Back to guide overview