Skip to content

Commit 4e9c8aa

Browse files
aynaashclaude
andcommitted
fix(nextcompile): serve root-level public/ files (install.sh, robots.txt) from R2
The dispatcher only consulted R2 for /_next/static/* and /public/* prefixed paths, but Next.js serves files in public/ at the bare root and the packager uploads them keyed by basename. As a result every root-level public asset 404'd on Cloudflare Workers deploys — including /install.sh, which made the curl|bash one-liner advertised in the README dead on arrival. Add serveRootPublicFromR2 in serve.mjs (gated on paths whose final segment contains a '.' so we don't spend an R2 GET on every typo'd route) and call it from dispatcher.mjs after route dispatch fails but before notFound. /install.sh, /install.bat, /daemon.sh, /robots.txt, /favicon.ico, etc. now resolve via the public Cloudflare deploy. Verified end-to-end: redeployed nextdeploy.org and confirmed `curl -fsSL https://nextdeploy.org/install.sh | bash` now succeeds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 685de5a commit 4e9c8aa

2 files changed

Lines changed: 32 additions & 2 deletions

File tree

shared/nextcompile/runtime_src/dispatcher.mjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
// 5. SSG routes — pre-rendered HTML from R2
1313
// 6. ISR routes — HTML from R2 (revalidation arrives with cache.mjs)
1414
// 7. Dynamic table — regex-matched routes, ordered by specificity
15-
// 8. 404
15+
// 8. Root-served public files (/install.sh, /robots.txt, …) from R2
16+
// 9. 404
1617
//
1718
// Every handler invocation is wrapped in an AsyncLocalStorage context so
1819
// Next's async cookies() / headers() / draftMode() resolve to per-request
@@ -23,7 +24,7 @@
2324
// without RSC fall through to the legacy default-export path (Pages
2425
// Router + simple App Router pages).
2526

26-
import { serveStaticFromR2, serveSSGFromR2 } from "./serve.mjs";
27+
import { serveStaticFromR2, serveSSGFromR2, serveRootPublicFromR2 } from "./serve.mjs";
2728
import { matchDynamic, buildRouteContext } from "./route_match.mjs";
2829
import { notFound, serverError } from "./errors.mjs";
2930
import { runWithContext, createRequestContext } from "./context.mjs";
@@ -69,6 +70,9 @@ export async function dispatch(request, env, ctx, tables) {
6970
const routed = await tryRouteDispatch(request, env, ctx, url, pathname, tables);
7071
if (routed) return routed;
7172

73+
const publicRoot = await serveRootPublicFromR2(env, pathname);
74+
if (publicRoot) return publicRoot;
75+
7276
return notFound();
7377
} catch (err) {
7478
return serverError(err);

shared/nextcompile/runtime_src/serve.mjs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,32 @@ export async function serveStaticFromR2(env, pathname) {
3838
return new Response(obj.body, { headers });
3939
}
4040

41+
/**
42+
* Serve a public/* file requested at the bare root path (e.g.
43+
* /install.sh, /robots.txt, /favicon.ico). Next serves public/ at the
44+
* root, and the packager uploads those files to R2 keyed by their
45+
* basename — so the request path minus the leading slash is the R2
46+
* key. Restricted to paths whose final segment contains a "." to keep
47+
* real 404s from spending an R2 GET each.
48+
*/
49+
export async function serveRootPublicFromR2(env, pathname) {
50+
if (!env.ASSETS) return null;
51+
if (pathname.length < 2 || pathname.endsWith("/")) return null;
52+
const last = pathname.slice(pathname.lastIndexOf("/") + 1);
53+
if (!last.includes(".")) return null;
54+
55+
const obj = await env.ASSETS.get(pathname.slice(1));
56+
if (!obj) return null;
57+
58+
const headers = new Headers();
59+
obj.writeHttpMetadata?.(headers);
60+
if (obj.httpEtag) headers.set("etag", obj.httpEtag);
61+
if (!headers.has("cache-control")) {
62+
headers.set("cache-control", "public, max-age=300");
63+
}
64+
return new Response(obj.body, { headers });
65+
}
66+
4167
/**
4268
* Serve a pre-rendered HTML file (SSG or ISR) from R2. The dispatcher
4369
* resolves the R2 key from manifest.routes.ssg/isr and passes it here.

0 commit comments

Comments
 (0)