Deployment and routing
How it works, in a paragraph. Two separate Next.js applications serve one domain: the existing Densy app owns everything, and this app owns one path segment beneath it. That arrangement is called multi-zone, and it works because this app is built with a path prefix baked in. Almost every problem on this page is a variation of one theme — some URLs get that prefix automatically and some do not, and the ones that do not fail silently.
Why the prefix is a build-time value
Both applications serve their assets from the same conventional path. Without a prefix, a request for one app’s code can be answered by the other, and the page dies with a chunk-loading error that looks like a caching problem and is not.
The prefix is read from the environment because the routing arrangement belongs to the deploy rather than to the source — but it is compiled in, so the build has to know it. A build made without it cannot be served under it later.
An empty prefix means “own the root”, which is what local development and a standalone preview want. It is set as an absent value rather than an empty string, because an empty string is not the same as no prefix to the framework’s own validation.
Trap one: the image optimiser
The framework applies the prefix to routes and to its own asset paths. It does not apply it to the URL the image optimiser passes to itself. So an image referenced by a plain string path resolves to an unprefixed URL, the optimiser cannot fetch it, and the request fails with a status that suggests a bad request rather than a missing prefix.
The fix is to import images as modules rather than reference them by path. An imported image is served from the prefixed asset path and carries its own dimensions.
Trap two: metadata URLs, which fail the opposite way
Link-preview tags need absolute URLs — scrapers do not resolve relative ones — and they are built by joining a configured base URL to the route. That base is a second, independent resolution path, and the prefix is not added to it.
Which means the base URL must itself include the prefix. Measured both ways against the built HTML rather than reasoned about:
| Configured base | Emitted image URL | |
|---|---|---|
| origin plus the segment | …/norwood-stage/opengraph-image-… | correct |
| origin only | …/opengraph-image-… | 404 |
There is no doubling to fear, and the failure runs in the direction nobody checks. That second URL is served by the other application on this domain, so it answers a 404 page rather than nothing — every scraper drops the image, and the preview degrades to a bare text row while the tags still look plausible in the page source. The failure mode is an absent preview, not a broken one.
So the two traps have opposite remedies. For images you rely on the framework’s prefixing; for metadata you supply it yourself.
Links across the zone boundary
Inside this app, a framework Link gets the prefix and a plain anchor does not. For
a path that exists in both applications — /privacy is the live example — the
same href therefore reaches two different pages depending on which element you used,
and both are pages Densy owns. The wrong one does not 404; it shows a real but
inapplicable document. Choose deliberately at every link site.
The values that must be right before launch
| Value | Wrong low | Wrong high |
|---|---|---|
| Trusted proxy hops | The daily allowance is bypassed by sending the header yourself | Every visitor counts against one key, so the second person of the day is told they are out of checks |
| Site base URL | Link previews silently disappear | — |
| Path prefix | Asset collisions with the other app | Routes 404 |
The proxy-hop count is the one that is not a preference. The forwarded-for header grows left to right as each hop appends the address it received the request from, so the client is the Nth entry from the right when there are N trusted hops. Reading the leftmost entry instead means trusting a value the client supplied, and the limit is then bypassed by anyone who sets the header.
Language and previews
The interface language lives in a cookie rather than in the URL. A scraper sends no cookies, so every link preview renders in English, whatever the reader’s language. That is a routing consequence, not a metadata one: localised previews need per-language URLs, and there are none. It is recorded rather than worked around, because the workaround is a routing change with real cost.