firebase / firebase/apphosting-adapters

adapter-nextjs: all public/ assets 404 in production with webpack standalone builds (monorepo) — no bundler-independent public copy

Open
#630 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.