firebase / firebase/apphosting-adapters
adapter-nextjs: all public/ assets 404 in production with webpack standalone builds (monorepo) — no bundler-independent public copy
- Dominant language
- TypeScript
- Stars
- 478
- Forks
- 1.5k
- Avg merge
- 3d 22h
- Merged PRs (30d)
- 2
Description
## Summary
With the **webpack** builder (Next 16, `output: 'standalone'`, pnpm monorepo, app at `apps/web`), App Hosting deploys serve **every `public/` asset as a Next 404** in production — images, fonts, `manifest.json`, all of it — while `/_next/static/*` serves fine. The same app built with **turbopack** (same adapter version) serves `public/` correctly.
We shipped this to production unknowingly during a builder cutover (turbopack → webpack) and every public asset on the site 404'd until we diagnosed it.
## Mechanism (adapter-nextjs 14.0.21, `src/utils.ts`)
The adapter has **no bundler-independent `public/` copy**:
- `.next/static` is copied **explicitly and unconditionally** (utils.ts ~148-150: `copy(staticDirectory, opts.outputStaticDirectoryPath, ...)`) — which is why `/_next/static` always works.
- `public/` is only handled by the generic `copyResources()` (utils.ts ~163-181), copying top-level app-dir entries into `standaloneAppPath` **gated by `existsInOutputBundle`** — i.e. it relies on Next's standalone output already containing (or correctly receiving) `public/`.
- **Turbopack** standalone output includes `public/`; **webpack** standalone documentedly does not ("This minimal server does not copy the `public` or `.next/static` folders by default" — Next docs). In a monorepo layout (`.next/standalone/apps/web/...`, driven by `outputFileTracingRoot`), the fallback copy does not land where the standalone `server.js` resolves `public/` from, so the runtime ends up with an empty/near-empty `public/`.
**The diagnostic tell:** the only public file that served in our production deploys was the single file NFT-tracing happened to pull into standalone (a route reads it at runtime) — proving the standalone serving path works and the directory simply wasn't populated.
## Repro context
- `@apphosting/adapter-nextjs` 14.0.21 (current `latest`; buildpack default via `npm view`), Next.js 16.1.6, `next build --webpack`, `output: 'standalone'`, pnpm workspace + `turbo.json` at repo root (monorepo mode, `GOOGLE_BUILDABLE=apps/web`).
- Result: `.next/standalone/apps/web/public/` contains only NFT-traced files; all other public assets 404 from the deployed Cloud Run service.
## Workaround we ship
`postbuild` npm hook that performs the documented Next standalone copy:
copy `public/` → `.next/standalone//public` and `.next/static` → `.next/standalone//.next/static`. With `public/` pre-placed, the adapter packages it and assets serve.
## Suggested fix
Copy `public/` explicitly and unconditionally into `standaloneAppPath/public` (mirroring the existing explicit `.next/static` copy), independent of bundler. The e2e suite currently tests neither public-asset serving nor a monorepo layout — both would have caught this.
Contributor guide
Assessment
This issue has not been assessed yet.