Skip to content

Commit 50d7427

Browse files
pilcrowonpaperHinata Masakivicb
authored
Add image optimization with Cloudflare Images (#999)
Co-authored-by: Hinata Masaki <hinata@cloudflare.com> Co-authored-by: Victor Berchet <victor@suumit.com>
1 parent b53a046 commit 50d7427

File tree

10 files changed

+1008
-187
lines changed

10 files changed

+1008
-187
lines changed

examples/playground14/e2e/cloudflare.spec.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { test, expect } from "@playwright/test";
8+
import sharp from "sharp";
89

910
test.describe("playground/cloudflare", () => {
1011
test("NextConfig", async ({ page }) => {
@@ -21,36 +22,64 @@ test.describe("playground/cloudflare", () => {
2122

2223
test.describe("remotePatterns", () => {
2324
test("fetch an image allowed by remotePatterns", async ({ page }) => {
24-
const res = await page.request.get("/_next/image?url=https://avatars.githubusercontent.com/u/248818");
25+
const res = await page.request.get(
26+
"/_next/image?url=https://avatars.githubusercontent.com/u/248818&w=256&q=75"
27+
);
2528
expect(res.status()).toBe(200);
2629
expect(res.headers()).toMatchObject({ "content-type": "image/jpeg" });
2730
});
2831

2932
test("400 when fetching an image disallowed by remotePatterns", async ({ page }) => {
30-
const res = await page.request.get("/_next/image?url=https://avatars.githubusercontent.com/u/248817");
33+
const res = await page.request.get(
34+
"/_next/image?url=https://avatars.githubusercontent.com/u/248817&w=256&q=75"
35+
);
3136
expect(res.status()).toBe(400);
3237
});
3338
});
3439

3540
test.describe("localPatterns", () => {
3641
test("fetch an image allowed by localPatterns", async ({ page }) => {
37-
const res = await page.request.get("/_next/image?url=/snipp/snipp.webp?iscute=yes");
42+
const res = await page.request.get("/_next/image?url=/snipp/snipp.webp?iscute=yes&w=256&q=75");
3843
expect(res.status()).toBe(200);
3944
expect(res.headers()).toMatchObject({ "content-type": "image/webp" });
4045
});
4146

4247
test("400 when fetching an image disallowed by localPatterns with wrong query parameter", async ({
4348
page,
4449
}) => {
45-
const res = await page.request.get("/_next/image?url=/snipp/snipp?iscute=no");
50+
const res = await page.request.get("/_next/image?url=/snipp/snipp?iscute=no&w=256&q=75");
4651
expect(res.status()).toBe(400);
4752
});
4853

4954
test("400 when fetching an image disallowed by localPatterns without query parameter", async ({
5055
page,
5156
}) => {
52-
const res = await page.request.get("/_next/image?url=/snipp/snipp");
57+
const res = await page.request.get("/_next/image?url=/snipp/snipp&w=256&q=75");
5358
expect(res.status()).toBe(400);
5459
});
5560
});
61+
62+
test.describe("imageSizes", () => {
63+
test("400 when fetching an image with unsupported width value", async ({ page }) => {
64+
const res = await page.request.get("/_next/image?url=/snipp/snipp.webp?iscute=yes&w=100&q=75");
65+
expect(res.status()).toBe(400);
66+
});
67+
});
68+
69+
test.describe("qualities", () => {
70+
test("400 when fetching an image with unsupported quality value", async ({ page }) => {
71+
const res = await page.request.get("/_next/image?url=/snipp/snipp.webp?iscute=yes&w=256&q=100");
72+
expect(res.status()).toBe(400);
73+
});
74+
});
75+
76+
test.describe('"w" parameter', () => {
77+
test("Image is shrunk to target width", async ({ page }) => {
78+
const res = await page.request.get("/_next/image?url=/snipp/snipp.webp?iscute=yes&w=256&q=75");
79+
expect(res.status()).toBe(200);
80+
const buffer = await res.body();
81+
const metadata = await sharp(buffer).metadata();
82+
expect(metadata.width).toBe(256);
83+
});
84+
});
5685
});

examples/playground14/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@opennextjs/cloudflare": "workspace:*",
2525
"@playwright/test": "catalog:",
2626
"@types/node": "catalog:",
27+
"sharp": "^0.34.5",
2728
"wrangler": "catalog:"
2829
}
2930
}

examples/playground14/wrangler.jsonc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,8 @@
1818
"hello": "Hello World from the cloudflare context!",
1919
"PROCESS_ENV_VAR": "process.env",
2020
"NEXT_INC_CACHE_KV_PREFIX": "custom_prefix"
21+
},
22+
"images": {
23+
"binding": "IMAGES"
2124
}
2225
}

examples/playground15/next.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ const nextConfig: NextConfig = {
1212
},
1313
deploymentId: getDeploymentId(),
1414
trailingSlash: true,
15+
images: {
16+
formats: ["image/avif", "image/webp"],
17+
},
1518
};
1619

1720
export default nextConfig;

examples/playground15/wrangler.jsonc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,8 @@
4444
"database_id": "db_id",
4545
"database_name": "db_name"
4646
}
47-
]
47+
],
48+
"images": {
49+
"binding": "IMAGES"
50+
}
4851
}

packages/cloudflare/src/api/cloudflare-context.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ declare global {
1313
// Asset binding
1414
ASSETS?: Fetcher;
1515

16+
// Images binding for image optimization
17+
// Optimization is disabled if undefined
18+
IMAGES?: ImagesBinding;
19+
1620
// Environment to use when loading Next `.env` files
1721
// Default to "production"
1822
NEXTJS_ENV?: string;

packages/cloudflare/src/cli/build/open-next/compile-images.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,64 @@ export async function compileImages(options: BuildOptions) {
1919
: {};
2020

2121
const __IMAGES_REMOTE_PATTERNS__ = JSON.stringify(imagesManifest?.images?.remotePatterns ?? []);
22-
const __IMAGES_LOCAL_PATTERNS__ = JSON.stringify(imagesManifest?.images?.localPatterns ?? []);
22+
const __IMAGES_LOCAL_PATTERNS__ = JSON.stringify(
23+
imagesManifest?.images?.localPatterns ?? defaultLocalPatterns
24+
);
25+
const __IMAGES_DEVICE_SIZES__ = JSON.stringify(imagesManifest?.images?.deviceSizes ?? defaultDeviceSizes);
26+
const __IMAGES_IMAGE_SIZES__ = JSON.stringify(imagesManifest?.images?.imageSizes ?? defaultImageSizes);
27+
const __IMAGES_QUALITIES__ = JSON.stringify(imagesManifest?.images?.qualities ?? defaultQualities);
28+
const __IMAGES_FORMATS__ = JSON.stringify(imagesManifest?.images?.formats ?? defaultFormats);
29+
const __IMAGES_MINIMUM_CACHE_TTL_SEC__ = JSON.stringify(
30+
imagesManifest?.images?.minimumCacheTTL ?? defaultMinimumCacheTTLSec
31+
);
2332
const __IMAGES_ALLOW_SVG__ = JSON.stringify(Boolean(imagesManifest?.images?.dangerouslyAllowSVG));
2433
const __IMAGES_CONTENT_SECURITY_POLICY__ = JSON.stringify(
2534
imagesManifest?.images?.contentSecurityPolicy ?? "script-src 'none'; frame-src 'none'; sandbox;"
2635
);
2736
const __IMAGES_CONTENT_DISPOSITION__ = JSON.stringify(
2837
imagesManifest?.images?.contentDispositionType ?? "attachment"
2938
);
39+
const __IMAGES_MAX_REDIRECTS__ = JSON.stringify(
40+
imagesManifest?.images?.maximumRedirects ?? defaultMaxRedirects
41+
);
3042

3143
await build({
3244
entryPoints: [imagesPath],
3345
outdir: path.join(options.outputDir, "cloudflare"),
34-
bundle: false,
46+
bundle: true,
3547
minify: false,
3648
format: "esm",
3749
target: "esnext",
3850
platform: "node",
3951
define: {
4052
__IMAGES_REMOTE_PATTERNS__,
4153
__IMAGES_LOCAL_PATTERNS__,
54+
__IMAGES_DEVICE_SIZES__,
55+
__IMAGES_IMAGE_SIZES__,
56+
__IMAGES_QUALITIES__,
57+
__IMAGES_FORMATS__,
58+
__IMAGES_MINIMUM_CACHE_TTL_SEC__,
4259
__IMAGES_ALLOW_SVG__,
4360
__IMAGES_CONTENT_SECURITY_POLICY__,
4461
__IMAGES_CONTENT_DISPOSITION__,
62+
__IMAGES_MAX_REDIRECTS__,
4563
},
4664
});
4765
}
66+
67+
const defaultDeviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
68+
69+
// 16 was included in Next.js 15
70+
const defaultImageSizes = [32, 48, 64, 96, 128, 256, 384];
71+
72+
// All values between 1-100 were allowed in Next.js 15
73+
const defaultQualities = [75];
74+
75+
// Was unlimited in Next.js 15
76+
const defaultMaxRedirects = 3;
77+
78+
const defaultFormats = ["image/webp"];
79+
80+
const defaultMinimumCacheTTLSec = 14400;
81+
82+
const defaultLocalPatterns = { pathname: "/**" };

0 commit comments

Comments
 (0)