Start for free

Next.js static export or SSR: choose before deploying

A Next.js project can produce a folder of public files or require a server at request time. Those outputs need different hosting paths. This guide builds a small static export, tests its route files and shows why a request-dependent page cannot simply be uploaded as HTML.

Updated

Choose by what the page needs when a visitor arrives

Start with the feature that changes per request. A public page generated from a known list of content can be built ahead of time. A page that reads an incoming private session cookie to render personalized HTML needs a runtime. A static frontend may call an existing external API from the browser, but that API must handle authorization and private operations itself.

Architecture and Shipvela deployment paths
NeedSuitable outputShipvela boundary
Public pages generated from known contentStatic export, normally out/Build locally; upload only generated files with the CLI.
Browser interactions and calls to an existing APIStatic frontend plus a separately hosted APIBrowser configuration is public; Shipvela does not provision the backend or database.
Request-time Next.js server renderingCompatible Next.js SSR buildSupported Next 12–15 on a paid plan through the separate GitHub path; npm repository root only.
Next 16 server runtime or arbitrary server frameworkRuntime-specific hostingNot supported as Shipvela SSR. A successful local Next 16 static export is a different case.
Streaming, on-demand ISR or Edge API routesAdditional runtime capabilitiesOutside the current Shipvela Next SSR adapter limits. A paid plan does not imply every Next feature works.

Do not downgrade a working application just to match a hosting label without reviewing its dependencies, security updates and behavior. Either produce a legitimate static export or use a host that supports the runtime your app needs. For supported SSR, validate the exact adapter limits in the product docs before changing the project.

A build-time page produces public files for static hosting; a request-dependent page needs a supported runtime. Browser API calls remain separate backend requests.
Original architecture diagram. “Server Component” does not itself mean “a server is running for each visitor.” Open full-size diagram.

Check the features that can prevent export

Questions to answer before setting output: export
Feature in your appCan the export contain it?Required change or limit
Server Components using build-time dataOften yes; their output can be generated during the build.Do not include private data in rendered HTML or serialized output.
Dynamic segment such as /notes/[slug]Yes for an enumerated finite set.Return all intended paths from generateStaticParams; new content needs a new build.
Cookies, request-dependent route handlers or Server ActionsNot as request-time server functionality in a static folder.Keep a supported server or move the operation to an existing secure backend.
Client Components using window or localStorageOnly with browser-only access after hydration.Top-level browser API access can break prerendering during build.
Default next/image optimizationRequires a runtime image service.Use deliberately preprocessed images, unoptimized images or a suitable custom loader.
Next rewrites, redirects, headers, ISR or ProxyNot provided by uploading exported files.Verify host support separately; a config entry cannot create a server in out/.

This is a decision checklist, not a complete compatibility matrix for all Next versions. The current official static-export guide documents unsupported features. The example below pins the version it actually ran rather than silently tracking latest.

Build a complete small export

Download the Next.js static-export fixture, extract it and open shipvela-next-export-fixture. It contains a home page and one known note route. The recorded local test used Next.js 16.3.7, React 19.3.0, Node 22.20.0 and npm 10.9.3. These versions describe the local compiler, not Shipvela SSR compatibility. Use a currently supported Node release that satisfies your own project’s engines.

Reproducible local build procedure

cd shipvela-next-export-fixture
npm ci --ignore-scripts
npm run build

The build script is next build --webpack. The fixture disables persistent webpack caching to keep the local lab small; that is not a hosting requirement. It uses no remote fonts, external data fetching, database or secret values. Its important configuration is:

Static-export settings in next.config.mjs; the downloaded lab also disables its webpack cache

export default {
  output: 'export',
  trailingSlash: true,
  images: { unoptimized: true },
};

output: export requests a static artifact. trailingSlash emits nested index.html files that a conventional static server can address. images.unoptimized avoids assuming a running image-optimization endpoint; you still need sensible image dimensions and file sizes. Do not add these settings to a large application without auditing its server-dependent features.

Route-generation portion of app/notes/[slug]/page.jsx

export function generateStaticParams() {
  return [{ slug: 'welcome' }];
}
export const dynamicParams = false;

export default async function Note({ params }) {
  const { slug } = await params;
  return <main><h1>Note: {slug}</h1></main>;
}

Only welcome is included in the route list. That is intentional: a route pattern in source does not create HTML for every possible slug. For a real content collection, derive the list from approved build-time data and confirm that every intended page appears in the artifact.

Inspect the artifact before publishing

Check the generated route files

node -e "const fs=require('node:fs'); for(const p of ['out/index.html','out/notes/welcome/index.html','out/404.html']) console.log(p, fs.existsSync(p));"

The recorded build produced all three files. The nested page already contained its Note: welcome heading before client JavaScript ran. The out directory also contained _next assets and navigation payloads. Keep those generated files together. Uploading .next, the source app folder or only the HTML pages omits part of the static output.

What to expect in the output
PathRoleVerification
out/index.htmlHome documentOpen through an HTTP server; confirm the heading and link.
out/notes/welcome/index.htmlEnumerated nested documentLoad /notes/welcome/ directly, then refresh.
out/_next/…JavaScript, styles and other generated client filesCheck that the document’s asset URLs return the correct types.
out/404.htmlGenerated not-found documentThe host must choose it with an appropriate status; its presence alone does not configure routing.
out/notes/unlisted/No generated route in this exampleAn unknown path should not silently display the home page.

Do not use --spa for this example: replacing every route request with the home document discards the distinction between separately generated pages. The lab’s server resolves a directory to its index.html and returns 404 for a missing route. A local result does not prove the production host’s slash redirects or custom 404 presentation.

Test direct requests as well as links

Serve the exported files with the included local-only test server

node scripts/serve.mjs out strict 4176

Open http://127.0.0.1:4176, follow Read the welcome note and refresh the note URL. Then paste /notes/welcome/ into a fresh tab. All should show the same page. The fixture includes the same small static test server used by the runtime lab; it starts no Next server and executes no server-side application code.

Expected local statuses: 200 for the exported note, 404 for the unlisted note

curl -i http://127.0.0.1:4176/notes/welcome/
curl -i http://127.0.0.1:4176/notes/unlisted/

A useful acceptance test also disables JavaScript and inspects the exported page’s meaningful content. Client-side interactivity will naturally stop, but the heading and text should still exist for this example. If a real page renders only an empty shell, investigate its rendering design rather than assuming every static export contains all useful content.

The automated local check verifies route files, HTTP responses and the request-dependent failure below. It does not certify Shipvela CDN behavior, external API authorization, all browsers or a real cloud deployment.

Observe a request-only feature fail the static build

A small negative test helps distinguish “can compile React” from “can export this application.” The test temporarily adds a page that reads an incoming cookie and then runs the same export build. That requires information which does not exist for a future visitor at build time.

Deliberately incompatible example for app/account/page.jsx; do not publish this as authentication code

import { cookies } from 'next/headers';
export default async function Account() {
  const store = await cookies();
  return <p>{store.get('demo_session')?.value || 'No session'}</p>;
}

The test expects a non-zero build result. A successful export must not pretend to personalize this HTML for an incoming user. The test removes the temporary account page and rebuilds the original fixture. It does not create a real session or inspect anyone’s cookies. Consult Next.js cookies when designing a runtime page.

For an app that already has a secure external API, a browser may request authenticated data after load. That is a separate architecture with its own loading, error, authorization and SEO implications. Moving code into a Client Component does not make private keys safe or replace server authorization.

Publish the generated folder through the static path

Once the local checks pass, use the existing CLI setup and login instructions. Check whether shipvela.json already links the directory to a project. For a new static-upload project, the publishing procedure is:

Deployment procedure only; no live upload was performed for this guide

npm run build
shipvela publish out --name field-notes --json

For later updates to that linked project, use shipvela publish out --json. The CLI uploads the prebuilt archive; it does not run Next.js for visitors. Wait for the specific job’s success, then test the returned URL, the nested route and an unknown URL before connecting a domain.

Do not substitute a GitHub import of this Next 16 fixture and expect the same path. Current Shipvela GitHub Next SSR admission is bounded to Next 12–15 on paid plans. This example uses local compilation followed by static file upload. It neither changes pricing nor expands runtime support.

Recover from common export failures

Symptom and concrete next action
SymptomActionProof of recovery
Build says a dynamic route lacks generateStaticParamsEnumerate intended slugs or keep a runtime for unbounded routes.Each intended route exists in out and an unlisted route is handled intentionally.
window is not defined during prerenderMove browser-only access into an effect or another browser-only path.The production build completes, then the interaction still works in a browser.
Image optimization errorSelect an export-compatible image strategy.The built page requests actual images successfully without a missing runtime endpoint.
Nested page fails only on refreshInspect generated path shape and the host’s file resolution.Direct HTTP request returns the intended document, not the homepage fallback.
A newly added content slug is missingRegenerate the route list and rebuild the entire export.The new route file and navigation assets are present in the new artifact.
Private data appears in the HTML or bundleRemove it from build output and revoke any exposed credential.A fresh artifact contains only intended public data; access rules are enforced elsewhere.

Keep the complete source and lockfile so the export can be rebuilt. A successful static build is the start of release acceptance; use the launch checklist for content, domain, accessibility and operational checks before sharing the final URL.