diff --git a/.changeset/evil-rabbits-relax.md b/.changeset/evil-rabbits-relax.md new file mode 100644 index 00000000..7eeaa752 --- /dev/null +++ b/.changeset/evil-rabbits-relax.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/cloudflare": patch +--- + +fix: Respect trailing slash config for \_next/image route in worker diff --git a/examples/playground15/app/api/signal/revalidate/route.ts b/examples/playground15/app/api/signal/revalidate/route.ts index 18128538..fc4de326 100644 --- a/examples/playground15/app/api/signal/revalidate/route.ts +++ b/examples/playground15/app/api/signal/revalidate/route.ts @@ -3,7 +3,7 @@ import { revalidatePath } from "next/cache"; export const dynamic = "force-dynamic"; export async function GET() { - revalidatePath("/signal"); + revalidatePath("/signal/"); return new Response("ok"); } diff --git a/examples/playground15/app/image/page.tsx b/examples/playground15/app/image/page.tsx new file mode 100644 index 00000000..7613f59c --- /dev/null +++ b/examples/playground15/app/image/page.tsx @@ -0,0 +1,11 @@ +import Image from "next/image"; + +import tomineImg from "../../public/tomine.webp"; + +export default function Page() { + return ( +
+ Picture of Tomine +
+ ); +} diff --git a/examples/playground15/e2e/base.spec.ts b/examples/playground15/e2e/base.spec.ts index f67b1504..be2435c6 100644 --- a/examples/playground15/e2e/base.spec.ts +++ b/examples/playground15/e2e/base.spec.ts @@ -46,9 +46,9 @@ test.describe("playground/base", () => { }); test("returns correct information about the request from a route handler", async ({ page, baseURL }) => { - const res = await page.request.get("/api/request"); + const res = await page.request.get("/api/request/"); // Next.js can fall back to `localhost:3000` or `n` if it doesn't get the host - neither of these are expected. - const expectedURL = `${baseURL}/api/request`; + const expectedURL = `${baseURL}/api/request/`; await expect(res.json()).resolves.toEqual({ nextUrl: expectedURL, url: expectedURL }); }); diff --git a/examples/playground15/e2e/image.test.ts b/examples/playground15/e2e/image.test.ts new file mode 100644 index 00000000..46ed08c2 --- /dev/null +++ b/examples/playground15/e2e/image.test.ts @@ -0,0 +1,10 @@ +import { test, expect } from "@playwright/test"; + +test.describe("next/image with trailing slash", () => { + test("next/image with trailing slash", async ({ page }) => { + await page.goto("/image"); + await expect(page.getByAltText("Picture of Tomine")).toBeVisible(); + // The trailing slash should only be there if trailingSlash is enabled in next.config.ts + expect(await page.getAttribute("img", "src")).toMatch(/^\/_next\/image\//); + }); +}); diff --git a/examples/playground15/next.config.mjs b/examples/playground15/next.config.ts similarity index 81% rename from examples/playground15/next.config.mjs rename to examples/playground15/next.config.ts index 0d2f9dfd..5a9fdb88 100644 --- a/examples/playground15/next.config.mjs +++ b/examples/playground15/next.config.ts @@ -1,9 +1,9 @@ import { initOpenNextCloudflareForDev, getDeploymentId } from "@opennextjs/cloudflare"; +import { NextConfig } from "next"; initOpenNextCloudflareForDev(); -/** @type {import('next').NextConfig} */ -const nextConfig = { +const nextConfig: NextConfig = { typescript: { ignoreBuildErrors: true }, eslint: { ignoreDuringBuilds: true }, experimental: { @@ -11,6 +11,7 @@ const nextConfig = { serverSourceMaps: true, }, deploymentId: getDeploymentId(), + trailingSlash: true, }; export default nextConfig; diff --git a/examples/playground15/public/tomine.webp b/examples/playground15/public/tomine.webp new file mode 100644 index 00000000..7b994ae3 Binary files /dev/null and b/examples/playground15/public/tomine.webp differ diff --git a/packages/cloudflare/src/cli/build/open-next/compile-init.ts b/packages/cloudflare/src/cli/build/open-next/compile-init.ts index 5dc2f9fa..6ccf4ffd 100644 --- a/packages/cloudflare/src/cli/build/open-next/compile-init.ts +++ b/packages/cloudflare/src/cli/build/open-next/compile-init.ts @@ -17,6 +17,7 @@ export async function compileInit(options: BuildOptions, wranglerConfig: Unstabl const nextConfig = loadConfig(path.join(options.appBuildOutputPath, ".next")); const basePath = nextConfig.basePath ?? ""; const deploymentId = nextConfig.deploymentId ?? ""; + const trailingSlash = nextConfig.trailingSlash ?? false; await build({ entryPoints: [initPath], @@ -31,6 +32,7 @@ export async function compileInit(options: BuildOptions, wranglerConfig: Unstabl __NEXT_BASE_PATH__: JSON.stringify(basePath), __ASSETS_RUN_WORKER_FIRST__: JSON.stringify(wranglerConfig.assets?.run_worker_first ?? false), __DEPLOYMENT_ID__: JSON.stringify(deploymentId), + __TRAILING_SLASH__: JSON.stringify(trailingSlash), }, }); } diff --git a/packages/cloudflare/src/cli/templates/init.ts b/packages/cloudflare/src/cli/templates/init.ts index 49ac9fff..fc0a1d11 100644 --- a/packages/cloudflare/src/cli/templates/init.ts +++ b/packages/cloudflare/src/cli/templates/init.ts @@ -97,6 +97,7 @@ function initRuntime() { __BUILD_TIMESTAMP_MS__, __NEXT_BASE_PATH__, __ASSETS_RUN_WORKER_FIRST__, + __TRAILING_SLASH__, // The external middleware will use the convertTo function of the `edge` converter // by default it will try to fetch the request, but since we are running everything in the same worker // we need to use the request as is. @@ -155,4 +156,6 @@ declare global { var __ASSETS_RUN_WORKER_FIRST__: boolean | string[] | undefined; // Deployment ID var __DEPLOYMENT_ID__: string; + // Next trailingSlash config + var __TRAILING_SLASH__: boolean; } diff --git a/packages/cloudflare/src/cli/templates/worker.ts b/packages/cloudflare/src/cli/templates/worker.ts index 892b957f..6d2c3fdf 100644 --- a/packages/cloudflare/src/cli/templates/worker.ts +++ b/packages/cloudflare/src/cli/templates/worker.ts @@ -39,7 +39,10 @@ export default { } // Fallback for the Next default image loader. - if (url.pathname === `${globalThis.__NEXT_BASE_PATH__}/_next/image`) { + if ( + url.pathname === + `${globalThis.__NEXT_BASE_PATH__}/_next/image${globalThis.__TRAILING_SLASH__ ? "/" : ""}` + ) { const imageUrl = url.searchParams.get("url") ?? ""; return await fetchImage(env.ASSETS, imageUrl, ctx); }