I went through next.config.mjs looking for something else and found three entries that had no effect on anything. Then I found a fourth that looked equally decorative and turned out to be the reason a route works in production at all.
The pattern that connects them is worth more than any of them individually: configuration is not code you can read to find out what it does. It is a request to a build system that may or may not be listening.
1. A PWA plugin that never emitted a service worker
The config used to start with this:
import withPWA from '@ducanh2912/next-pwa';
export default withPWA({
cacheOnFrontEndNav: true,
aggressiveFrontEndNavCaching: true,
})(nextConfig);
Both options read as an attempt to make client side navigation faster, and both were doing nothing at all. That plugin works by injecting a webpack plugin into the build. This project is on Next 16, where next build compiles with Turbopack. No webpack, no injection, no output.
The tell is that there is no artifact. There is no public/sw.js in the working tree, none in the build output, and none has ever been committed. The plugin's entire job is to write one file, and that file has never existed.
What makes this worse than having no PWA setup is that it stops the question being asked. Somebody profiling navigation performance reads the config, sees aggressive caching enabled, and goes to look somewhere else. Dead configuration that looks like a performance feature is an active obstacle, not neutral clutter.
What is there now is a comment where the plugin used to be, saying exactly this, plus what it would take to do it properly: a Turbopack compatible service worker, which today means Serwist rather than next-pwa.
2. optimizePackageImports for a package we do not install
experimental: {
optimizePackageImports: ['lucide-react', '@hugeicons/core-free-icons', ...],
},
lucide-react is not in package.json. It is not imported anywhere. It was presumably copied from a blog post, because lucide-react is the canonical example in every article about this option.
optimizePackageImports rewrites barrel imports like import { Icon } from 'pkg' into direct module paths so the bundler can drop everything else in the barrel. Listing a package you do not depend on is harmless, which is exactly the problem: it is indistinguishable from the entries that matter, so the list stops being readable as a statement about your app.
The entry that matters here is @hugeicons/core-free-icons. It is a large icon barrel, and 108 files in this repo import from it. That is the one doing work. Removing the fake neighbour is not a performance change, it is so the remaining names are all true.
The general test: for each entry, name the package in your dependency tree it applies to. If you cannot, delete it.
3. A robots.txt rule for a filename that is not a URL
Same class of thing, different file. Our robots.txt contains:
Disallow: /manifest.ts
app/manifest.ts is a source file. In the App Router it is a metadata route convention: Next evaluates the exported function and serves the result at /manifest.webmanifest. /manifest.ts is never requested by anything, so the rule blocks nothing, and the URL that actually exists is not covered by it.
Nobody has been harmed by this, because there is no reason to hide a web app manifest. It is in the post because it is the same mistake as the lucide-react entry: a rule written against the shape of the repository rather than the shape of the deployed application. Route conventions make that mistake easy, because the filename and the URL are deliberately not the same string.
4. The one that was load bearing
outputFileTracingIncludes: {
'/api/.../[id]': ['./lib/.../data/**/*.json'],
},
This one looks like the others. It is the reason a route works.
Next traces the module graph to decide which files go into a serverless function bundle. Tracing follows imports. That directory of JSON is not imported: a route handler reads it off disk at request time, chosen by a path parameter. From the bundler's point of view nothing references those files, so they are not part of the function, so they are not deployed.
The symptom is the nastiest shape a deploy bug comes in. Local next dev reads from your working tree, where every file is present, so it works perfectly. Production has a function bundle with the code and none of the data, so every request 404s. Nothing in the build output complains, because nothing was wrong with the build. The files were simply never mentioned.
Two details in how it is written:
It is a glob, **/*.json, rather than a list of filenames. A list would mean a new data file ships broken until somebody remembers this config exists, which is the drift failure again with a longer feedback loop, because you would only find out in production.
And the key is a route pattern, not a directory. The include is scoped to the one function that needs the data, so the other functions in the deployment do not carry it. Tracing exists to keep bundles small; opting out globally would undo that everywhere to fix one route.
The actual lesson
Three of these four entries were unfalsifiable by reading. cacheOnFrontEndNav: true looks exactly as real as outputFileTracingIncludes. The only difference is that one produces an artifact you can go and look for and the other does not.
So the check that actually works is: name the file this entry creates, changes, or includes, then go and look for it. public/sw.js does not exist, so the PWA config is dead. The function bundle either contains the JSON or it does not. That question has an answer. "Is this option being applied?" does not.
There is one loose end this leaves, which I am keeping honest about rather than quietly fixing before writing it up. app/manifest.ts still declares display: 'standalone', and the homepage still links it. Without a service worker, Chrome will not offer an install prompt regardless of what the manifest says, so the manifest is currently describing an app that cannot be installed. Its description field is also stale in a way that dates it precisely: it says "all 14 Arctic Shores psychometric games" from back when one provider was the entire product.
See it
All of these are public, and the point is that they are checkable rather than takeable on trust:
- cogniprep.app/sw.js returns a 404 page. That is the missing artifact from the PWA config, still missing.
-
cogniprep.app/manifest.webmanifest returns the manifest, including the stale description. The source file is
app/manifest.ts, which is a different string from that URL. -
cogniprep.app/robots.txt has
Disallow: /manifest.tsnear the bottom, blocking the filename and not the route. - Open cogniprep.app, then DevTools, Application, Service Workers. Empty.
Run the same check on your own site. Open /sw.js and see whether the thing your config says you have actually shipped.
Top comments (0)