Skip to content

feat(web): allow the static export's assets to be served from a prefix - #1014

Draft
jiashuoz wants to merge 1 commit into
mainfrom
feat/marketing-asset-prefix
Draft

feat(web): allow the static export's assets to be served from a prefix#1014
jiashuoz wants to merge 1 commit into
mainfrom
feat/marketing-asset-prefix

Conversation

@jiashuoz

@jiashuoz jiashuoz commented Sep 9, 2026

Copy link
Copy Markdown
Member

One optional config line: assetPrefix: process.env.NEXT_ASSET_PREFIX || undefined.

Why

A deployment that builds this static export more than once, at different cadences, hits a collision: both builds emit /_next/* and only one of them can own that path. Concretely — serving the marketing pages (which change with copy) separately from the dashboard (which must match the deployed server version). assetPrefix gives one of the builds its own asset namespace.

It's also the standard way to serve a static export's assets from a CDN subpath, so it's useful to self-hosters independently of that.

Why it's safe

Unset everywhere by default. Verified against real builds (Next 16, output: "export"):

  • With NEXT_ASSET_PREFIX=/_marketing: every script, stylesheet and font reference becomes /_marketing/_next/static/..., with no bare /_next left in the emitted HTML.
  • With it unset: the export is byte-identical to a build from the unmodified config — all 380 files, hash-compared — modulo two pre-existing sources of build nondeterminism: Next's random per-build ID directory (_next/static/<id>/_buildManifest.js) and the <lastmod> timestamps sitemap.ts stamps at build time.

|| undefined rather than || "" is deliberate: Next maps an empty assetPrefix onto basePath, which is a different behaviour from "no prefix configured".

No test here, on purpose

next.config.ts uses import.meta, so jest (CJS) can't load it, and reshaping the config to make it unit-testable would be a worse trade than the guard it buys. The consumer asserts it end-to-end instead: the hosted build's bundle test checks the emitted HTML actually references /_marketing/_next/, which fails loudly if this option is ever removed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GB8rYQbBBqow6ZmZ7JdyqD

Adds an optional NEXT_ASSET_PREFIX passthrough to next.config.ts.
Unset everywhere by default, so the published image, the dev server and
self-hosted builds are unchanged.

It exists for deployments that build the static export more than once at
different cadences -- e.g. serving the marketing pages from one build and
the dashboard from another. Both would otherwise claim /_next/*, and only
one of them can own that path. A self-hoster serving assets from a CDN
subpath can use it the same way.

Verified against a real build (Next 16, output: "export"):
- with NEXT_ASSET_PREFIX=/_marketing, every script/style/font reference
  becomes /_marketing/_next/static/..., with no bare /_next left;
- with it unset, the export is byte-identical to a build from the
  unmodified config across all 380 files, modulo Next's random per-build
  ID directory and the <lastmod> timestamps sitemap.ts stamps at build
  time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GB8rYQbBBqow6ZmZ7JdyqD
@jiashuoz

Copy link
Copy Markdown
Member Author

Parked with the hosted-side work (tokencanopy/e2a-ops #375, #376 are drafts)

This PR is the safest of the set and is independently verified: with NEXT_ASSET_PREFIX unset, the export is byte-identical to a build from the unmodified config across all 380 files — the only difference being sitemap.xml's wall-clock <lastmod>, which varies between two consecutive builds of main anyway. No dependency, lockfile, workflow or lifecycle-script changes.

Two things to fold in before merging:

  1. Validate the prefix. NEXT_ASSET_PREFIX=_marketing (no leading slash) builds with rc=0 and zero warnings, then emits a relative asset reference:

    https://e2a.dev/                        -> /_marketing/_next/...           works
    https://e2a.dev/use-cases/support-agent -> /use-cases/_marketing/_next/...  404
    

    The homepage is the only page that works — and it's the one anyone smoke-tests. Throw unless the value matches /^(\/[^/]|https?:\/\/)/.

  2. The self-host framing oversells it. The comment pitches this at self-hosters serving assets from a CDN subpath, but the export still writes assets to out/_next/ regardless — relocating them to match the prefix is the consumer's job. The hosted builder does that; a self-hoster's Caddy (which hardcodes /_next/static/*) would not. Either implement that path or narrow the comment to the hosted use case.

Also worth adding NEXT_ASSET_PREFIX to web/.env.example — nothing in this repo currently references it, so a routine "remove unused env var" sweep would delete the line with nothing failing.

@jiashuoz
jiashuoz marked this pull request as draft September 10, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant