Privacy choices

Optional Google Analytics and advertising are off until you choose. Read our privacy details.

AI CodingLevel / intermediate10 min field guide

Next.js Static Export: A Release Preflight for Cloudflare Pages

A build-first checklist for dynamic routes, metadata, rendered HTML, and the static artefact Cloudflare Pages actually serves.

ByReviewed
Next.js static export preflight for Cloudflare Pages
FIELD GUIDE · AI CODING · JUN 28, 2026

I do not treat a happy next dev window as release evidence. It proves that the development server can render the page I happened to open. A static host serves a different contract: the build must materialise every route, every image reference, every canonical, and every social card before anything reaches Cloudflare Pages.

This guide is the preflight I use for an App Router site configured with output: "export". It is deliberately build-first. The goal is not a heroic list of framework trivia. The goal is a repeatable answer to one operational question: does the exact out/ directory contain a coherent site that a browser and crawler can consume?

The current Next.js static-export guide is the source of truth for the deployment model. This article adds the practical checks around it: dynamic path enumeration, metadata inheritance, rendered HTML, and a local serving pass.

Start with the deployment contract

For a static export, the configuration is intentionally small:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "export",
  trailingSlash: true,
  images: { unoptimized: true },
};

export default nextConfig;

output: "export" tells Next.js to write static files during next build. trailingSlash: true gives directory-style URLs such as /tutorials/example/, which map cleanly to index.html files on a static host. images.unoptimized is an explicit choice for a static deployment: there is no Next image optimisation server waiting at request time.

That configuration is not a promise that the output is correct. It is a reason to inspect it. If a route is absent, a browser cannot ask a Node server to create it later. If an og:image URL is missing from the rendered head, a social crawler cannot infer it from a React component. Static hosting makes the artefact the product.

Gate one: make the build earn its green status

Run the short checks before the expensive build, then audit the generated files afterwards:

npm run lint
npm run typecheck
npm test
npm run check:content
npm run build
npm run audit:export

Each command answers a different question:

  • lint catches framework and accessibility mistakes that a browser may tolerate.
  • typecheck catches route and metadata shape errors before they become a quiet fallback.
  • test protects shared helpers, especially URL construction and HTML sanitisation.
  • check:content checks the editorial corpus for hard publishing failures.
  • build creates the static candidate in out/.
  • audit:export examines the candidate itself for broken internal links, absent assets, and head-level regressions.

Terminal output from a real Next.js static export build

The build transcript is captured from the release candidate, not copied from a development server.

The useful discipline here is to keep the acceptance criteria separate from the build command. A successful next build proves that Next.js could render its route graph. It does not by itself prove that every generated page has one H1, a canonical, a usable Open Graph image, or a link that resolves in the exported tree. Those are artefact-level checks.

Gate two: enumerate every dynamic route

Dynamic segments need a concrete list at build time. The safe pattern is still simple:

type Props = { params: Promise<{ slug: string }> };

export function generateStaticParams() {
  return getAllTutorials().map((tutorial) => ({ slug: tutorial.slug }));
}

export default async function TutorialPage({ params }: Props) {
  const { slug } = await params;
  const tutorial = getTutorialBySlug(slug);
  if (!tutorial) notFound();
  return <Article tutorial={tutorial} />;
}

Two details deserve an explicit check. First, generateStaticParams must cover the source that actually feeds the route. A filtered list that accidentally drops one published slug produces a missing file, not a runtime recovery. Second, App Router route parameters are asynchronous in current Next.js APIs, so route code and generateMetadata should await params before using the slug. The dynamic-routes reference documents that contract.

Do not settle the question in next dev. Inspect the built path instead. After a build, serve the exported directory and open the real route:

python3 -m http.server 8788 -d out
# http://127.0.0.1:8788/tutorials/your-slug/

Use 127.0.0.1 so the local server target is unambiguous. Check an index, one dynamic article, one collection page, and the not-found path. You are testing the static tree that Cloudflare Pages would receive, not a helpful development fallback.

Gate three: own the full metadata object

Metadata inheritance is a frequent static-export blind spot. Next.js composes metadata from layouts and pages, but nested metadata objects are shallowly replaced. A page that declares its own openGraph object must carry its own image rather than assuming the layout's image survives. The generateMetadata reference calls out that replacement behaviour.

I keep the fallback image in one helper and make each route state its own canonical URL:

export function defaultOgImage(alt = SITE_NAME) {
  return {
    url: `${SITE_URL}/og/home.png`,
    alt,
    width: 1200,
    height: 630,
  };
}

Then a collection route can make the full contract explicit:

export const metadata = {
  alternates: { canonical: "https://autokaam.com/tutorials/" },
  openGraph: {
    title: "AI and developer-tool tutorials",
    images: [defaultOgImage("AutoKaam tutorials")],
  },
};

The exact wording is not the important part. The important part is that the rendered page has one title, one description, one canonical URL, and at least one absolute og:image URL. Article routes should additionally expose publication and modification dates through their JSON-LD. Google's Article structured-data documentation is a useful checklist for that article-specific layer.

Terminal output from the static export audit

The export audit inspects the rendered HTML, so an inherited metadata assumption cannot hide behind a passing typecheck.

Gate four: inspect what a browser receives

The local server pass is where visual quality and crawl quality meet. Open the homepage at desktop width and a narrow mobile width. Verify that the lead image is real rather than an empty visual treatment, that the navigation can be used with a keyboard, and that no copy presents a static build timestamp as a live event.

The exported AutoKaam homepage served locally from the static out directory

The screenshot above is the static output served locally, not a design mock-up.

For a quick head check, use the browser inspector or a small script against a generated file. I look for the canonical link, description, og:image, and JSON-LD block. For a content site, I also sample the newest article, a tutorial, an author page, a topic page, and a paginated hub. Those five paths exercise the places where metadata and internal-link assumptions usually diverge.

The audit should be strict enough to fail when an indexable page has zero or multiple H1s, no Open Graph image, a relative social image URL, a missing local asset, or a broken internal URL. These are cheap deterministic checks. They are much more reliable than discovering an empty social card after a link has already travelled.

The hand-off is the artefact, not the command history

When all gates are green, the object handed to Cloudflare Pages is out/. Keep the build log and the export-audit result with the release hand-off, then let the deployment mechanism consume that directory. Do not make a deploy the first time the static output is exercised.

That distinction also helps when a build changes route count. A new article, a removed tool, or a new taxonomy page can legitimately change the number. The invariant is not a magic number. It is that the new route is present, linked, correctly described, and represented in the generated sitemap where appropriate.

For the broader router decision, read App Router vs Pages Router in 2026. For a concrete release snapshot using this preflight, see 303 Routes, 0 Red Gates. The AutoKaam methodology explains the source and review standards behind these field notes.

Filed under