All resources
Note #012

Four production-only bugs `next dev` will never show you

Canonicals pointing at localhost, a URL frozen at build time, uploads that 404, a security policy that silently breaks the page. Four Next.js bugs invisible in development, and how to catch them.

By Tenovates4 min read

Written 2026-09 · Next.js 16

The problem

The development server is built for speed and forgiveness: it rebuilds on every change, reads environment files directly, serves new files the moment they appear, and relaxes several security settings. A production build does none of that. Some bugs only exist in the gap between the two — and because everyone tests in development, they reach production untouched.

Here are four of the most common in Next.js projects.

1. Canonicals that point at localhost

Symptom : in production, the page's <link rel="canonical"> and its social-sharing image point at your local development address, on port 3000.

Why : relative URLs in metadata (a canonical of /about, an image of /og.png) are resolved against metadataBase. When it isn't set, Next.js falls back to a local default. In development that looks right; in production it tells search engines the real page lives somewhere else.

Fix : set metadataBase once, in the root layout, from the site's public URL — and fail loudly if it's missing, rather than falling back to a local address, which is exactly the bug:

const siteUrl = process.env.NEXT_PUBLIC_SITE_URL;
if (!siteUrl) throw new Error("NEXT_PUBLIC_SITE_URL is not set");

export const metadata = {
  metadataBase: new URL(siteUrl),
};

Check : on the production site, read the canonical of a real page (curl -sL <url> | grep canonical) and make sure it shows your domain.

2. A URL frozen at build time

Symptom : you changed the site URL in the server's environment and restarted — the site still uses the old value.

Why : variables prefixed NEXT_PUBLIC_ are inlined into the bundle when the site is built. Changing them afterwards has no effect until the next build. In container builds, there's a second trap: if the environment file isn't part of the build context (and it shouldn't be — it holds secrets), the variable is simply empty at build time.

Fix : pass public values to the build explicitly (a build argument, or your platform's build-time variables), and rebuild when they change. Keep secrets out of the build context entirely.

Check : after deploying, look at a value that depends on the variable (a canonical, an absolute link) on the live site.

3. Uploads that 404 after the deploy

Symptom : an image uploaded from the admin appears in the admin, but returns 404 on the public site — while images that existed at build time work fine.

Why : the production server can index the public/ folder when it starts; a file written there later may not be served by it at all. In development, new files are picked up immediately, so the bug never shows.

Fix : don't rely on public/ for files that appear at run time. Serve them through a route handler that reads the upload directory (validating the path strictly), or from a web server or storage service configured to serve that directory directly.

Check : on the live site, upload a file after deploying and open its URL.

4. A security policy that silently breaks the page

Symptom : after adding a Content Security Policy, pages look normal but nothing is interactive — buttons do nothing, forms don't submit.

Why : Next.js pages include inline scripts that hydrate the page. A strict policy that forbids inline scripts blocks them. The HTML still renders, so the page looks fine; the browser console is the only place that says otherwise. Development mode differs again (it needs eval for fast refresh), so a policy tested only in development can fail both ways.

Fix : build the policy for production explicitly — allow what the framework needs (or use nonces), add each third-party origin you actually load — and test it on a production build.

Check : in a real browser, open a page with a form, click something interactive, and read the console for policy violations. Then trigger one violation on purpose to confirm your check would catch it.

The take-away

Test the production build, at the production address, before trusting it. All four bugs are invisible in development and obvious within minutes on the real thing.

Checklist

Production-only checks for a Next.js site
1. Canonical of a real page → your domain, not localhost (metadataBase)
2. NEXT_PUBLIC_* values → provided at build time ; rebuild after changing
3. File uploaded after the deploy → actually served
4. CSP → pages hydrate, no console violation ; a deliberate violation is caught