Skip to main content
ariefrse.my
HomeSkillsProjectsAI WorkWritingContact
Resume
All writing

Note · Next.js

The settings that did nothing and the icons that didn't exist

Configuration is a list of claims, and nothing checks claims by default. Two failures with the same shape: declared but never delivered.

July 16, 2026 · 5 min read

An audit of my portfolio turned up two failures with the same shape: declared but not delivered. Neither threw an error. Both had been live for months.

The security headers that were never sent

My next.config.ts had a proper headers() block. CSP, frame protection, the lot.

But this site is a static export, output: 'export'. Next.js builds plain HTML files and my own nginx serves them. In that mode headers() and redirects() do nothing at all, because there is no Next server at runtime to apply them. Next does not warn you. The config parses fine and is silently ignored.

nginx has its own trap waiting behind that one: it drops every inherited add_header directive the moment a location block adds any header of its own. So even after moving the headers to nginx, the locations that set their own caching headers quietly shed the entire security set.

The manifest that promised phantom files

The PWA manifest referenced eight icon sizes, screenshots and shortcut icons. None of those files existed. The layout also linked /icon.svg and /apple-touch-icon.png, also missing.

What was declaredWhat was actually delivered
CSP and frame protection in next.config.tsNothing. A static export never runs a Next server to apply it
Full header set on every nginx locationOnly the top-level headers. Overridden blocks dropped the rest
Eight manifest icon sizes plus screenshots404 on every request. The files never existed
/icon.svg, /apple-touch-icon.png404. Linked in the layout, never generated

The fix

Make the promises true, and make them smaller.

Repeat the full header set in every nginx location that sets any header, verified with nginx -t and then by curling the live responses. Draw a real icon.svg, generate every referenced PNG from it, and rewrite the manifest to list only assets that exist.

The second half of that matters as much as the first. A shorter manifest that is entirely true is worth more than a comprehensive one that 404s.

What I learned

Configuration is a list of claims, and nothing checks claims by default. The audit that catches this is dumb and effective: for every promise in config, a header, a file, a redirect, go and ask the live site for it, then look at what actually comes back.

Same lesson as making CI check the live site . Trust what the running system serves, not what the files say.

nextjsnginxsecurity-headersaudit

Share

XLinkedInWhatsAppThreads

Related

  • Everything fails silentlyGreen is not evidence. It is the absence of evidence. A running gallery of production failures that passed every check that existed.
  • A green build is not a deploy: making CI check the live siteEvery stage of my pipeline vouched for itself and nobody vouched for the result. The first real run of the fix caught ten days of deploys that had gone nowhere.

© 2026 Muhammad Arief Roseli

GitHubLinkedInSnapInvois