opennextjs-azure 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![NPM Downloads](https://img.shields.io/npm/dt/opennextjs-azure)](https://www.npmjs.com/package/opennextjs-azure)
7
7
  [![License: MIT](https://img.shields.io/npm/l/opennextjs-azure)](https://opensource.org/licenses/MIT)
8
8
 
9
- Built on the [OpenNext](https://opennext.js.org) framework, this adapter brings native Next.js support to Azure Functions.
9
+ Built on the [OpenNext](https://opennext.js.org) framework, this adapter brings native Next.js support to Azure Functions with a Vercel-grade developer experience.
10
10
 
11
11
  ![Deploy Screenshot](./docs/deploy-screenshot.png)
12
12
 
@@ -23,6 +23,7 @@ Azure Functions is Microsoft's serverless compute platform—comparable to AWS L
23
23
  | Next.js Feature | Azure Implementation |
24
24
  | -------------------------------------- | --------------------------------------------------------------- |
25
25
  | Incremental Static Regeneration | Azure Blob Storage |
26
+ | Image Optimization | Azure Blob Storage with automatic caching |
26
27
  | Streaming SSR | Azure Functions with Node.js streams |
27
28
  | `revalidateTag()` / `revalidatePath()` | Azure Table Storage + Queue Storage |
28
29
  | Fetch caching | Azure Blob Storage with build ID namespacing |
@@ -139,6 +140,12 @@ Converts between Azure Functions HTTP triggers and Next.js InternalEvent/Interna
139
140
  - **Tag Cache:** Azure Table Storage maps tags → paths for `revalidateTag()`
140
141
  - **Revalidation Queue:** Azure Queue Storage triggers on-demand regeneration
141
142
 
143
+ **Image Optimization:**
144
+
145
+ - **Source Images:** Loaded from `assets` blob container
146
+ - **Optimized Cache:** Processed images cached in `optimized-images` container to avoid re-processing
147
+ - **Processing:** Processing is done with the `sharp` library, which gets added for you during deployment.
148
+
142
149
  **Build Process:**
143
150
  Uses OpenNext's AWS build with Azure-specific overrides, then adds Azure Functions metadata (`host.json`, `function.json`) for v3 programming model.
144
151
 
@@ -0,0 +1,20 @@
1
+ import { InternalEvent, InternalResult } from '@opennextjs/aws/types/open-next.js';
2
+ import { OpenNextHandler, OpenNextHandlerOptions } from '@opennextjs/aws/types/overrides.js';
3
+
4
+ /**
5
+ * Azure Image Optimization Handler
6
+ *
7
+ * Wraps OpenNext's default image optimization handler with Azure Blob caching.
8
+ *
9
+ * When AZURE_IMAGE_OPTIMIZATION_CACHE=true:
10
+ * - First request: Loads from "assets", processes, caches in "optimized-images", returns
11
+ * - Subsequent requests: Returns from "optimized-images" cache (no processing)
12
+ *
13
+ * When AZURE_IMAGE_OPTIMIZATION_CACHE=false:
14
+ * - Every request processes the image (no caching)
15
+ */
16
+ declare function createImageOptimizationHandler(): Promise<OpenNextHandler<InternalEvent, InternalResult>>;
17
+ declare const handler: OpenNextHandler<InternalEvent, InternalResult>;
18
+ declare function defaultHandler(event: InternalEvent, options?: OpenNextHandlerOptions): Promise<InternalResult>;
19
+
20
+ export { createImageOptimizationHandler, defaultHandler, handler };
@@ -0,0 +1,20 @@
1
+ import { InternalEvent, InternalResult } from '@opennextjs/aws/types/open-next.js';
2
+ import { OpenNextHandler, OpenNextHandlerOptions } from '@opennextjs/aws/types/overrides.js';
3
+
4
+ /**
5
+ * Azure Image Optimization Handler
6
+ *
7
+ * Wraps OpenNext's default image optimization handler with Azure Blob caching.
8
+ *
9
+ * When AZURE_IMAGE_OPTIMIZATION_CACHE=true:
10
+ * - First request: Loads from "assets", processes, caches in "optimized-images", returns
11
+ * - Subsequent requests: Returns from "optimized-images" cache (no processing)
12
+ *
13
+ * When AZURE_IMAGE_OPTIMIZATION_CACHE=false:
14
+ * - Every request processes the image (no caching)
15
+ */
16
+ declare function createImageOptimizationHandler(): Promise<OpenNextHandler<InternalEvent, InternalResult>>;
17
+ declare const handler: OpenNextHandler<InternalEvent, InternalResult>;
18
+ declare function defaultHandler(event: InternalEvent, options?: OpenNextHandlerOptions): Promise<InternalResult>;
19
+
20
+ export { createImageOptimizationHandler, defaultHandler, handler };
@@ -0,0 +1,11 @@
1
+ async function createImageOptimizationHandler() {
2
+ const { defaultHandler: defaultHandler2 } = await import('@opennextjs/aws/adapters/image-optimization-adapter.js');
3
+ const { createCachedImageOptimizationHandler } = await import('../overrides/imageOptimization/azure-cached.js');
4
+ return createCachedImageOptimizationHandler(defaultHandler2);
5
+ }
6
+ const handler = await createImageOptimizationHandler();
7
+ async function defaultHandler(event, options) {
8
+ return handler(event, options);
9
+ }
10
+
11
+ export { createImageOptimizationHandler, defaultHandler, handler };
@@ -3,7 +3,6 @@ import { Writable } from 'node:stream';
3
3
  const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 204, 205, 304]);
4
4
  const STATIC_ASSET_PATTERNS = [
5
5
  /^\/_next\/static\//,
6
- /^\/_next\/data\//,
7
6
  /^\/favicon\.ico$/,
8
7
  /^\/robots\.txt$/,
9
8
  /^\/sitemap\.xml$/,
@@ -17,11 +16,12 @@ const handler = async (handler2, converter) => async (context, request) => {
17
16
  const internalEvent = await converter.convertFrom(request);
18
17
  if (isStaticAssetRequest(internalEvent.rawPath)) {
19
18
  const blobUrl = `https://${process.env.AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net/assets${internalEvent.rawPath}`;
19
+ const cacheControl = internalEvent.rawPath.startsWith("/_next/static/") ? "public, max-age=31536000, immutable" : "public, max-age=0, must-revalidate";
20
20
  context.res = {
21
21
  status: 301,
22
22
  headers: {
23
23
  Location: blobUrl,
24
- "Cache-Control": "public, max-age=31536000, immutable"
24
+ "Cache-Control": cacheControl
25
25
  }
26
26
  };
27
27
  return;
@@ -0,0 +1,10 @@
1
+ import { InternalEvent, InternalResult } from '@opennextjs/aws/types/open-next.js';
2
+ import { WrapperHandler } from '@opennextjs/aws/types/overrides.js';
3
+
4
+ declare const _default: {
5
+ name: string;
6
+ wrapper: WrapperHandler<InternalEvent, InternalResult>;
7
+ supportStreaming: boolean;
8
+ };
9
+
10
+ export { _default as default };
@@ -0,0 +1,10 @@
1
+ import { InternalEvent, InternalResult } from '@opennextjs/aws/types/open-next.js';
2
+ import { WrapperHandler } from '@opennextjs/aws/types/overrides.js';
3
+
4
+ declare const _default: {
5
+ name: string;
6
+ wrapper: WrapperHandler<InternalEvent, InternalResult>;
7
+ supportStreaming: boolean;
8
+ };
9
+
10
+ export { _default as default };
@@ -0,0 +1,151 @@
1
+ import { Writable } from 'node:stream';
2
+ import { createHash } from 'node:crypto';
3
+ import { BlobServiceClient } from '@azure/storage-blob';
4
+
5
+ const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 204, 205, 304]);
6
+ const { AZURE_STORAGE_CONNECTION_STRING, AZURE_STORAGE_ACCOUNT_NAME } = process.env;
7
+ const CACHE_CONTAINER = "optimized-images";
8
+ function getBlobClient(key) {
9
+ if (!AZURE_STORAGE_CONNECTION_STRING && !AZURE_STORAGE_ACCOUNT_NAME) {
10
+ throw new Error("Azure Storage connection string or account name must be defined");
11
+ }
12
+ const blobServiceClient = AZURE_STORAGE_CONNECTION_STRING ? BlobServiceClient.fromConnectionString(AZURE_STORAGE_CONNECTION_STRING) : new BlobServiceClient(`https://${AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net`);
13
+ const containerClient = blobServiceClient.getContainerClient(CACHE_CONTAINER);
14
+ return containerClient.getBlockBlobClient(key);
15
+ }
16
+ function computeCacheKey(event) {
17
+ const { query } = event;
18
+ const url = Array.isArray(query?.url) ? query.url[0] : query?.url || "";
19
+ const width = Array.isArray(query?.w) ? query.w[0] : query?.w || "0";
20
+ const quality = Array.isArray(query?.q) ? query.q[0] : query?.q || "75";
21
+ const hash = createHash("sha256").update(url).digest("hex").substring(0, 16);
22
+ return `${hash}/w${width}_q${quality}.cache`;
23
+ }
24
+ async function getCachedImage(cacheKey) {
25
+ try {
26
+ const blobClient = getBlobClient(cacheKey);
27
+ const exists = await blobClient.exists();
28
+ if (!exists) {
29
+ return null;
30
+ }
31
+ const downloadResponse = await blobClient.download();
32
+ if (!downloadResponse.readableStreamBody) {
33
+ return null;
34
+ }
35
+ const chunks = [];
36
+ for await (const chunk of downloadResponse.readableStreamBody) {
37
+ chunks.push(Buffer.from(chunk));
38
+ }
39
+ return Buffer.concat(chunks);
40
+ } catch (error) {
41
+ return null;
42
+ }
43
+ }
44
+ async function setCachedImage(cacheKey, buffer, contentType) {
45
+ try {
46
+ const blobClient = getBlobClient(cacheKey);
47
+ await blobClient.upload(buffer, buffer.length, {
48
+ blobHTTPHeaders: {
49
+ blobContentType: contentType,
50
+ blobCacheControl: "public,max-age=31536000,immutable"
51
+ }
52
+ });
53
+ process.stderr.write(`[ImageCache] \u2713 Cached ${buffer.length} bytes
54
+ `);
55
+ } catch (error) {
56
+ process.stderr.write(`[ImageCache] \u2717 Cache failed: ${error.message}
57
+ `);
58
+ }
59
+ }
60
+ const handler = async (handler2, converter) => async (context, request) => {
61
+ try {
62
+ const internalEvent = await converter.convertFrom(request);
63
+ const cacheKey = computeCacheKey(internalEvent);
64
+ const cachedBuffer = await getCachedImage(cacheKey);
65
+ if (cachedBuffer) {
66
+ process.stderr.write(`[ImageCache] \u2713 Cache HIT
67
+ `);
68
+ context.res = {
69
+ status: 200,
70
+ headers: {
71
+ "Content-Type": "image/webp",
72
+ "Cache-Control": "public,max-age=31536000,immutable"
73
+ },
74
+ body: cachedBuffer
75
+ };
76
+ return;
77
+ }
78
+ process.stderr.write(`[ImageCache] Cache MISS - processing...
79
+ `);
80
+ let streamFinished = null;
81
+ let resolveStream = null;
82
+ let processedBuffer = null;
83
+ let responseContentType = "image/webp";
84
+ const streamCreator = {
85
+ writeHeaders(prelude) {
86
+ const { statusCode, cookies, headers } = prelude;
87
+ responseContentType = headers["Content-Type"] || headers["content-type"] || "image/webp";
88
+ const responseHeaders = { ...headers };
89
+ if (cookies.length > 0) {
90
+ responseHeaders["set-cookie"] = cookies.join(", ");
91
+ }
92
+ if (NULL_BODY_STATUSES.has(statusCode)) {
93
+ context.res = {
94
+ status: statusCode,
95
+ headers: responseHeaders
96
+ };
97
+ return new Writable({
98
+ write(_chunk, _encoding, callback) {
99
+ callback();
100
+ }
101
+ });
102
+ }
103
+ streamFinished = new Promise((resolve) => {
104
+ resolveStream = resolve;
105
+ });
106
+ const chunks = [];
107
+ const writable = new Writable({
108
+ write(chunk, _encoding, callback) {
109
+ chunks.push(Buffer.from(chunk));
110
+ callback();
111
+ },
112
+ final(callback) {
113
+ const body = Buffer.concat(chunks);
114
+ processedBuffer = body;
115
+ context.res = {
116
+ status: statusCode,
117
+ headers: responseHeaders,
118
+ body
119
+ };
120
+ callback();
121
+ resolveStream?.();
122
+ }
123
+ });
124
+ return writable;
125
+ }
126
+ };
127
+ await handler2(internalEvent, { streamCreator });
128
+ if (streamFinished) {
129
+ await streamFinished;
130
+ }
131
+ if (processedBuffer && responseContentType) {
132
+ await setCachedImage(cacheKey, processedBuffer, responseContentType);
133
+ }
134
+ } catch (error) {
135
+ console.error("Image optimization error:", error);
136
+ context.res = {
137
+ status: 500,
138
+ headers: {
139
+ "Content-Type": "text/plain"
140
+ },
141
+ body: "Internal server error"
142
+ };
143
+ }
144
+ };
145
+ const azureImageOptimization = {
146
+ name: "azure-image-optimization",
147
+ wrapper: handler,
148
+ supportStreaming: true
149
+ };
150
+
151
+ export { azureImageOptimization as default };
package/dist/cli/index.js CHANGED
@@ -222,6 +222,27 @@ async function checkStorageAccount(appName, resourceGroup, environment) {
222
222
  details: `Status: ${account.Status}`
223
223
  };
224
224
  }
225
+ try {
226
+ const { stdout: staticFileStdout } = await execAsync$1(
227
+ `az storage blob list --account-name ${account.Name} --container-name assets --prefix _next/static --query '[0].name' -o tsv --only-show-errors 2>/dev/null || echo ""`
228
+ );
229
+ const sampleFile = staticFileStdout.trim();
230
+ if (sampleFile) {
231
+ const { stdout: cacheStdout } = await execAsync$1(
232
+ `az storage blob show --account-name ${account.Name} --container-name assets --name '${sampleFile}' --query 'properties.contentSettings.cacheControl' -o tsv --only-show-errors 2>/dev/null || echo ""`
233
+ );
234
+ const cacheControl = cacheStdout.trim();
235
+ const expectedCache = "public, max-age=31536000, immutable";
236
+ if (cacheControl !== expectedCache) {
237
+ return {
238
+ passed: false,
239
+ message: "Static asset cache headers misconfigured",
240
+ details: `${sampleFile.split("/").pop()} has '${cacheControl || "none"}' (expected '${expectedCache}')`
241
+ };
242
+ }
243
+ }
244
+ } catch {
245
+ }
225
246
  return {
226
247
  passed: true,
227
248
  message: "Storage account healthy",
@@ -296,11 +317,23 @@ async function checkFunctionAppStatus(appName, resourceGroup, environment) {
296
317
  details: `State: ${funcApp.State}`
297
318
  };
298
319
  }
299
- return {
300
- passed: true,
301
- message: "Function app running",
302
- details: `URL: https://${funcApp.DefaultHostName}`
303
- };
320
+ const url = `https://${funcApp.DefaultHostName}`;
321
+ try {
322
+ const startTime = Date.now();
323
+ await execAsync$1(`curl -I -s -o /dev/null -w "%{http_code}" --max-time 10 "${url}" || echo "000"`);
324
+ const responseTime = Date.now() - startTime;
325
+ return {
326
+ passed: true,
327
+ message: "Function app running and responding",
328
+ details: `URL: ${url} (${responseTime}ms)`
329
+ };
330
+ } catch {
331
+ return {
332
+ passed: true,
333
+ message: "Function app running",
334
+ details: `URL: ${url} (HTTP check failed, but app is running)`
335
+ };
336
+ }
304
337
  } catch (error) {
305
338
  return {
306
339
  passed: false,
@@ -1,3 +1,3 @@
1
1
  import '@opennextjs/aws/types/open-next.js';
2
- export { d as defineAzureConfig, g as getAzureConfig } from '../shared/opennextjs-azure.d619537c.mjs';
2
+ export { d as defineAzureConfig, g as getAzureConfig } from '../shared/opennextjs-azure.279bd73d.mjs';
3
3
  import '@opennextjs/aws/types/overrides.js';
@@ -1,3 +1,3 @@
1
1
  import '@opennextjs/aws/types/open-next.js';
2
- export { d as defineAzureConfig, g as getAzureConfig } from '../shared/opennextjs-azure.d619537c.js';
2
+ export { d as defineAzureConfig, g as getAzureConfig } from '../shared/opennextjs-azure.279bd73d.js';
3
3
  import '@opennextjs/aws/types/overrides.js';
@@ -14,6 +14,9 @@ function defineAzureConfig(config = {}) {
14
14
  middleware: config.middleware || {
15
15
  external: false
16
16
  },
17
+ imageOptimization: {
18
+ loader: resolveImageLoader(config.imageLoader)
19
+ },
17
20
  dangerous: config.dangerous,
18
21
  buildCommand: config.buildCommand,
19
22
  buildOutputPath: config.buildOutputPath || ".",
@@ -48,6 +51,15 @@ function resolveQueue(value) {
48
51
  }
49
52
  return () => value;
50
53
  }
54
+ function resolveImageLoader(value) {
55
+ if (!value || value === "azure-blob") {
56
+ return () => import('../overrides/imageLoader/azure-blob.js').then((m) => m.default);
57
+ }
58
+ if (typeof value === "function") {
59
+ return value;
60
+ }
61
+ return () => value;
62
+ }
51
63
  function getAzureConfig() {
52
64
  return {
53
65
  deployment: {
package/dist/deploy.js CHANGED
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { exec } from 'node:child_process';
5
5
  import { promisify } from 'node:util';
6
6
  import { build as build$1 } from '@opennextjs/aws/build.js';
7
- import fs$1 from 'node:fs';
7
+ import fs$1, { existsSync } from 'node:fs';
8
8
  import readline from 'node:readline';
9
9
 
10
10
  const execAsync$3 = promisify(exec);
@@ -61,6 +61,42 @@ async function scaffoldProject(targetDir, options = {}) {
61
61
  });
62
62
  await execAsync$3("pnpm install", { cwd: targetDir });
63
63
  console.log("Dependencies installed\n");
64
+ const pagePath = path.join(targetDir, srcDir ? "src/app/page.tsx" : "app/page.tsx");
65
+ let pageContent = await fs.readFile(pagePath, "utf-8");
66
+ pageContent = pageContent.replace(
67
+ /<Image className="dark:invert" src="\/next\.svg" alt="Next\.js logo" width=\{180\} height=\{38\} priority \/>/,
68
+ `<div className="flex items-center gap-4">
69
+ <Image className="dark:invert" src="/next.svg" alt="Next.js logo" width={180} height={38} priority />
70
+ <span className="text-2xl text-gray-400 dark:text-gray-600">+</span>
71
+ <Image src="/azure.png" alt="Azure logo" width={38} height={38} priority />
72
+ </div>`
73
+ );
74
+ pageContent = pageContent.replace(
75
+ /href="https:\/\/vercel\.com\/new[^"]*"/,
76
+ 'href="https://github.com/zpg6/opennextjs-azure"'
77
+ );
78
+ pageContent = pageContent.replace(
79
+ /className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background/,
80
+ 'className="rounded-full border border-solid border-blue-400 transition-colors flex items-center justify-center bg-blue-500/10'
81
+ );
82
+ pageContent = pageContent.replace(/hover:bg-\[#383838\] dark:hover:bg-\[#ccc\]/, "hover:bg-blue-500/20");
83
+ pageContent = pageContent.replace(
84
+ /<Image className="dark:invert" src="\/vercel\.svg" alt="Vercel logomark" width=\{20\} height=\{20\} \/>/,
85
+ '<Image src="/azure.png" alt="Azure logomark" width={20} height={20} />'
86
+ );
87
+ pageContent = pageContent.replace(/>Deploy now</, ">Deploy to Azure<");
88
+ await fs.writeFile(pagePath, pageContent);
89
+ const publicDir = path.join(targetDir, "public");
90
+ const azureLogoSource = path.join(
91
+ path.dirname(new URL(import.meta.url).pathname),
92
+ "../../examples/basic-app/public/azure.png"
93
+ );
94
+ const azureLogoDest = path.join(publicDir, "azure.png");
95
+ try {
96
+ await fs.copyFile(azureLogoSource, azureLogoDest);
97
+ } catch (error) {
98
+ console.warn("Warning: Could not copy Azure logo. You can add it manually to public/azure.png");
99
+ }
64
100
  console.log("Creating open-next.config.ts...");
65
101
  const openNextConfig = `// @ts-nocheck
66
102
  export default {
@@ -78,6 +114,17 @@ export default {
78
114
  middleware: {
79
115
  external: false,
80
116
  },
117
+ imageOptimization: {
118
+ loader: () => import("./node_modules/opennextjs-azure/dist/overrides/imageLoader/azure-blob.js").then(m => m.default),
119
+ override: {
120
+ wrapper: () => import("./node_modules/opennextjs-azure/dist/adapters/wrappers/azure-image-optimization.js").then(m => m.default),
121
+ converter: () => import("./node_modules/opennextjs-azure/dist/adapters/converters/azure-http.js").then(m => m.default),
122
+ },
123
+ install: {
124
+ packages: ["@img/sharp-linux-x64@0.33.5", "sharp@0.33.5"],
125
+ additionalArgs: "--force --ignore-scripts",
126
+ },
127
+ },
81
128
  buildOutputPath: ".",
82
129
  appPath: ".",
83
130
  };
@@ -253,8 +300,48 @@ async function prepareFunctions() {
253
300
  entryPoint: "handler"
254
301
  };
255
302
  await fs.writeFile(path.join(functionDir, "function.json"), JSON.stringify(functionJson, null, 2));
256
- console.log(` ${greenCheck()} Azure Functions metadata created
257
- `);
303
+ const imageOptDir = path.join(process.cwd(), ".open-next/image-optimization-function");
304
+ try {
305
+ await fs.access(imageOptDir);
306
+ console.log(" Adding image optimization function...");
307
+ const imageFunctionDir = path.join(functionsDir, "image-optimization");
308
+ await fs.mkdir(imageFunctionDir, { recursive: true });
309
+ const imageFunctionJson = {
310
+ bindings: [
311
+ {
312
+ authLevel: "anonymous",
313
+ type: "httpTrigger",
314
+ direction: "in",
315
+ name: "req",
316
+ methods: ["get", "head"],
317
+ route: "_next/image"
318
+ },
319
+ {
320
+ type: "http",
321
+ direction: "out",
322
+ name: "res"
323
+ }
324
+ ],
325
+ scriptFile: "../index-image.mjs",
326
+ entryPoint: "handler"
327
+ };
328
+ await fs.writeFile(path.join(imageFunctionDir, "function.json"), JSON.stringify(imageFunctionJson, null, 2));
329
+ await fs.copyFile(path.join(imageOptDir, "index.mjs"), path.join(functionsDir, "index-image.mjs"));
330
+ await fs.cp(path.join(imageOptDir, ".next"), path.join(functionsDir, ".next"), {
331
+ recursive: true,
332
+ force: false
333
+ });
334
+ try {
335
+ await fs.copyFile(
336
+ path.join(imageOptDir, "open-next.config.mjs"),
337
+ path.join(functionsDir, "open-next.config.mjs")
338
+ );
339
+ } catch {
340
+ }
341
+ console.log(` ${greenCheck()} Image optimization function added`);
342
+ } catch {
343
+ }
344
+ console.log(` ${greenCheck()} Azure Functions metadata created`);
258
345
  console.log("Installing minimal runtime dependencies...");
259
346
  try {
260
347
  const originalPackageJson = JSON.parse(await fs.readFile(path.join(functionsDir, "package.json"), "utf-8"));
@@ -274,23 +361,33 @@ async function prepareFunctions() {
274
361
  await execAsync$2("npm install --production --no-package-lock --loglevel=error", {
275
362
  cwd: functionsDir
276
363
  });
277
- console.log(` ${greenCheck()} Runtime dependencies installed
278
- `);
364
+ console.log(` ${greenCheck()} Runtime dependencies installed`);
279
365
  } catch (error) {
280
366
  console.error("Failed to install dependencies:", error.message);
281
367
  throw error;
282
368
  }
369
+ const imageOptDir2 = path.join(process.cwd(), ".open-next/image-optimization-function");
370
+ try {
371
+ await fs.access(imageOptDir2);
372
+ console.log("Installing Sharp with Linux x64 binaries for image optimization...");
373
+ await execAsync$2(
374
+ "npm install --force sharp@0.33.5 @img/sharp-linux-x64@0.33.5 @img/sharp-libvips-linux-x64@1.0.4",
375
+ { cwd: functionsDir }
376
+ );
377
+ console.log(` ${greenCheck()} Sharp with Linux x64 binaries installed`);
378
+ } catch (error) {
379
+ }
283
380
  }
284
381
 
285
382
  async function build(configPath) {
286
- console.log("Building Next.js app for Azure...\n");
383
+ console.log("Building Next.js app for Azure...");
287
384
  const baseDir = process.cwd();
288
385
  const userConfigPath = configPath || "open-next.config.ts";
289
386
  const absoluteUserConfigPath = path.join(baseDir, userConfigPath);
290
387
  let resolvedConfigPath = userConfigPath;
291
388
  let tempConfigPath = null;
292
389
  if (!fs$1.existsSync(absoluteUserConfigPath)) {
293
- console.log("No open-next.config.ts found, using default Azure configuration\n");
390
+ console.log("No open-next.config.ts found, using default Azure configuration");
294
391
  const { createRequire } = await import('node:module');
295
392
  const require = createRequire(import.meta.url);
296
393
  const packagePath = path.dirname(require.resolve("opennextjs-azure/package.json"));
@@ -328,20 +425,17 @@ export default {
328
425
  if (fs$1.existsSync(openNextPath)) {
329
426
  console.log("Cleaning previous build output...");
330
427
  fs$1.rmSync(openNextPath, { recursive: true, force: true });
331
- console.log(` ${greenCheck()} Previous build cleaned
332
- `);
428
+ console.log(` ${greenCheck()} Previous build cleaned`);
333
429
  }
334
430
  console.log("Running OpenNext build...");
335
431
  const externals = ["@opennextjs/aws"].join(",");
336
432
  await build$1(resolvedConfigPath, externals);
337
- console.log(` ${greenCheck()} OpenNext build complete
338
- `);
339
433
  await prepareFunctions();
340
434
  console.log("Build completed successfully!");
341
- console.log("\nOutput: .open-next/");
435
+ console.log("Output: .open-next/");
342
436
  console.log(" \u251C\u2500\u2500 server-functions/default (Azure Functions app)");
343
- console.log(" \u2514\u2500\u2500 assets (Static files)\n");
344
- console.log("Next: opennextjs-azure deploy\n");
437
+ console.log(" \u2514\u2500\u2500 assets (Static files)");
438
+ console.log("Next: opennextjs-azure deploy");
345
439
  } catch (error) {
346
440
  console.error("Build failed:", error);
347
441
  process.exit(1);
@@ -367,8 +461,7 @@ async function deploy$1(options) {
367
461
  environment = "dev",
368
462
  skipInfrastructure = false
369
463
  } = options;
370
- console.log(`Deploying ${appName} to Azure (${environment} environment)
371
- `);
464
+ console.log(`Deploying ${appName} to Azure (${environment} environment)`);
372
465
  try {
373
466
  await checkAzureCLI();
374
467
  await checkAzureLogin();
@@ -386,8 +479,7 @@ async function deploy$1(options) {
386
479
  console.log("Provisioning Azure infrastructure...");
387
480
  console.log(` Resource Group: ${resourceGroup}`);
388
481
  console.log(` Location: ${location}`);
389
- console.log(` Environment: ${environment}
390
- `);
482
+ console.log(` Environment: ${environment}`);
391
483
  deploymentOutputs = await provisionInfrastructure({
392
484
  appName,
393
485
  resourceGroup,
@@ -395,20 +487,17 @@ async function deploy$1(options) {
395
487
  environment,
396
488
  applicationInsights: options.applicationInsights ?? false
397
489
  });
398
- console.log(` ${greenCheck()} Infrastructure ready
399
- `);
490
+ console.log(` ${greenCheck()} Infrastructure ready`);
400
491
  } else {
401
- console.log("Skipping infrastructure provisioning\n");
492
+ console.log("Skipping infrastructure provisioning");
402
493
  }
403
494
  console.log("Uploading static assets...");
404
495
  await uploadStaticAssets(appName, resourceGroup);
405
- console.log(` ${greenCheck()} Assets uploaded
406
- `);
496
+ console.log(` ${greenCheck()} Assets uploaded`);
407
497
  console.log("Deploying Function App...");
408
498
  const functionAppName = deploymentOutputs?.functionApp || `${appName}-func-${environment}`;
409
499
  await deployFunctionApp(functionAppName, resourceGroup);
410
- console.log(` ${greenCheck()} Function App deployed
411
- `);
500
+ console.log(` ${greenCheck()} Function App deployed`);
412
501
  await performPostflightChecks(
413
502
  resourceGroup,
414
503
  functionAppName,
@@ -438,11 +527,17 @@ async function checkAzureLogin() {
438
527
  }
439
528
  }
440
529
  async function checkRequiredProviders(applicationInsights) {
441
- const requiredProviders = ["Microsoft.Web", "Microsoft.Storage", "Microsoft.Compute", "Microsoft.Quota"];
530
+ const requiredProviders = [
531
+ "Microsoft.Web",
532
+ "Microsoft.Storage",
533
+ "Microsoft.Compute",
534
+ "Microsoft.Quota",
535
+ "Microsoft.ServiceLinker"
536
+ ];
442
537
  if (applicationInsights) {
443
538
  requiredProviders.push("Microsoft.AlertsManagement");
444
539
  }
445
- console.log("Checking Azure resource providers...\n");
540
+ console.log("Checking Azure resource providers...");
446
541
  for (const provider of requiredProviders) {
447
542
  const { stdout } = await execAsync$1(
448
543
  `az provider show --namespace ${provider} --query "registrationState" -o tsv`
@@ -451,8 +546,7 @@ async function checkRequiredProviders(applicationInsights) {
451
546
  if (state !== "Registered") {
452
547
  console.log(`Registering ${provider}...`);
453
548
  await execAsync$1(`az provider register --namespace ${provider} --wait`);
454
- console.log(` ${greenCheck()} ${provider} registered
455
- `);
549
+ console.log(` ${greenCheck()} ${provider} registered`);
456
550
  }
457
551
  }
458
552
  }
@@ -480,16 +574,13 @@ async function checkQuotaAvailability(location, environment) {
480
574
  console.error(`
481
575
  ${redX()} Quota Error: No quota available for ${environment} environment`);
482
576
  console.error(` Required: ${requiredSku.name}`);
483
- console.error(` Current Limit: ${requiredSku.quota}
484
- `);
577
+ console.error(` Current Limit: ${requiredSku.quota}`);
485
578
  if (y1Limit > 0 && environment !== "dev") {
486
579
  console.log(` Suggestion: Deploy to dev environment instead (has quota: ${y1Limit})`);
487
- console.log(` Command: opennextjs-azure deploy --environment dev
488
- `);
580
+ console.log(` Command: opennextjs-azure deploy --environment dev`);
489
581
  } else if (ep1Limit > 0 && environment === "dev") {
490
582
  console.log(` Suggestion: Deploy to prod environment instead (has quota: ${ep1Limit})`);
491
- console.log(` Command: opennextjs-azure deploy --environment prod
492
- `);
583
+ console.log(` Command: opennextjs-azure deploy --environment prod`);
493
584
  } else {
494
585
  console.log(` To request quota increase:`);
495
586
  console.log(
@@ -502,16 +593,13 @@ ${redX()} Quota Error: No quota available for ${environment} environment`);
502
593
  }
503
594
  throw new Error(`No ${requiredSku.type} quota available for ${environment} environment in ${location}`);
504
595
  }
505
- console.log(` ${greenCheck()} ${requiredSku.name}: ${requiredSku.quota} instances available
506
- `);
596
+ console.log(` ${greenCheck()} ${requiredSku.name}: ${requiredSku.quota} instances available`);
507
597
  if (environment === "dev" && ep1Limit > 0) {
508
598
  console.log(
509
- ` Premium tier also available (${ep1Limit} instances) - use --environment prod for better performance
510
- `
599
+ ` Premium tier also available (${ep1Limit} instances) - use --environment prod for better performance`
511
600
  );
512
601
  } else if (environment !== "dev" && y1Limit > 0) {
513
- console.log(` Consumption tier available (${y1Limit} instances) - use --environment dev for lower cost
514
- `);
602
+ console.log(` Consumption tier available (${y1Limit} instances) - use --environment dev for lower cost`);
515
603
  }
516
604
  } catch (error) {
517
605
  if (error.message?.includes("No") && error.message?.includes("quota available")) {
@@ -552,8 +640,7 @@ async function checkLocation(location) {
552
640
  Available regions: ${available.trim().split("\n").slice(0, 10).join(", ")}`
553
641
  );
554
642
  }
555
- console.log(` ${greenCheck()} Region: ${locations[0].DisplayName} (${location})
556
- `);
643
+ console.log(` ${greenCheck()} Region: ${locations[0].DisplayName} (${location})`);
557
644
  } catch (error) {
558
645
  if (error.message.includes("Invalid location")) {
559
646
  throw error;
@@ -584,8 +671,7 @@ Run 'opennextjs-azure build' to regenerate`
584
671
  );
585
672
  }
586
673
  }
587
- console.log(` ${greenCheck()} Build output structure valid
588
- `);
674
+ console.log(` ${greenCheck()} Build output structure valid`);
589
675
  }
590
676
  async function checkExistingInfrastructure(appName, resourceGroup, environment) {
591
677
  console.log("Checking existing infrastructure...");
@@ -641,16 +727,16 @@ async function performPostflightChecks(resourceGroup, functionAppName, location,
641
727
  console.log("\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
642
728
  console.log(`${greenCheck()} Deployment Complete!`);
643
729
  console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
644
- console.log("\nApplication:");
730
+ console.log("Application:");
645
731
  console.log(` App URL: ${functionUrl}`);
646
732
  console.log(` Assets URL: ${assetsUrl}`);
647
733
  console.log(` Status: ${funcApp.State}`);
648
734
  console.log(` Type: ${funcApp.Kind}`);
649
- console.log("\nInfrastructure:");
735
+ console.log("Infrastructure:");
650
736
  console.log(` Resource Group: ${resourceGroup}`);
651
737
  console.log(` Region: ${location}`);
652
738
  console.log(` Environment: ${environment}`);
653
- console.log("\nConfiguration:");
739
+ console.log("Configuration:");
654
740
  console.log(` App Service Plan: ${plan.Tier} (${plan.Sku})`);
655
741
  console.log(` Storage Account: ${storage.Name} (${storage.Sku})`);
656
742
  console.log(` Capacity: ${plan.Capacity || 1} instance(s)`);
@@ -665,9 +751,7 @@ async function performPostflightChecks(resourceGroup, functionAppName, location,
665
751
  }
666
752
  }
667
753
  console.log("\nQuick Actions:");
668
- console.log(
669
- ` View logs: az functionapp log tail --name ${functionAppName} --resource-group ${resourceGroup}`
670
- );
754
+ console.log(` View logs: npx opennextjs-azure@latest tail`);
671
755
  console.log(` Open in portal: ${portalUrl}`);
672
756
  console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
673
757
  if (funcApp.State !== "Running") {
@@ -705,15 +789,36 @@ async function provisionInfrastructure(options) {
705
789
  );
706
790
  return JSON.parse(stdout);
707
791
  }
792
+ async function patchCSSForBlobStorage(assetsPath) {
793
+ const cssPath = path.join(assetsPath, "_next/static/css");
794
+ if (!existsSync(cssPath)) {
795
+ return;
796
+ }
797
+ const files = await fs.readdir(cssPath);
798
+ const cssFiles = files.filter((f) => f.endsWith(".css"));
799
+ for (const file of cssFiles) {
800
+ const filePath = path.join(cssPath, file);
801
+ let content = await fs.readFile(filePath, "utf-8");
802
+ content = content.replace(/url\(\s*(['"]?)(\/_next\/static\/media\/[^'")\s]+)\1\s*\)/g, "url($1/assets$2$1)");
803
+ await fs.writeFile(filePath, content, "utf-8");
804
+ }
805
+ }
708
806
  async function uploadStaticAssets(appName, resourceGroup) {
709
807
  const assetsPath = path.join(process.cwd(), ".open-next/assets");
808
+ await patchCSSForBlobStorage(assetsPath);
710
809
  const { stdout } = await execAsync$1(
711
810
  `az storage account list --resource-group ${resourceGroup} --query "[0].name" -o tsv`
712
811
  );
713
812
  const storageAccountName = stdout.trim();
714
813
  await execAsync$1(
715
- `az storage blob upload-batch --account-name ${storageAccountName} --destination assets --source ${assetsPath} --overwrite`
814
+ `az storage blob upload-batch --account-name ${storageAccountName} --destination assets --source ${assetsPath} --content-cache-control "public, max-age=0, must-revalidate" --overwrite`
716
815
  );
816
+ const nextStaticPath = path.join(assetsPath, "_next/static");
817
+ if (existsSync(nextStaticPath)) {
818
+ await execAsync$1(
819
+ `az storage blob upload-batch --account-name ${storageAccountName} --destination assets --source ${nextStaticPath} --destination-path _next/static --content-cache-control "public, max-age=31536000, immutable" --overwrite`
820
+ );
821
+ }
717
822
  }
718
823
  async function deployFunctionApp(functionAppName, resourceGroup) {
719
824
  const functionsPath = path.join(process.cwd(), ".open-next/server-functions/default");
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AzureConfig, a as AzureDeploymentTarget, d as defineAzureConfig } from './shared/opennextjs-azure.d619537c.mjs';
1
+ export { A as AzureConfig, a as AzureDeploymentTarget, d as defineAzureConfig } from './shared/opennextjs-azure.279bd73d.mjs';
2
2
  export { default as azureBlobCache } from './overrides/incrementalCache/azure-blob.mjs';
3
3
  export { default as azureTableTagCache } from './overrides/tagCache/azure-table.mjs';
4
4
  export { default as azureQueueRevalidation } from './overrides/queue/azure-queue.mjs';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AzureConfig, a as AzureDeploymentTarget, d as defineAzureConfig } from './shared/opennextjs-azure.d619537c.js';
1
+ export { A as AzureConfig, a as AzureDeploymentTarget, d as defineAzureConfig } from './shared/opennextjs-azure.279bd73d.js';
2
2
  export { default as azureBlobCache } from './overrides/incrementalCache/azure-blob.js';
3
3
  export { default as azureTableTagCache } from './overrides/tagCache/azure-table.js';
4
4
  export { default as azureQueueRevalidation } from './overrides/queue/azure-queue.js';
@@ -96,6 +96,14 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
96
96
  publicAccess: 'Blob'
97
97
  }
98
98
  }
99
+
100
+ // Container for optimized images (public CDN access)
101
+ resource optimizedImagesContainer 'containers' = {
102
+ name: 'optimized-images'
103
+ properties: {
104
+ publicAccess: 'Blob'
105
+ }
106
+ }
99
107
  }
100
108
 
101
109
  // Table service for tag cache
@@ -194,6 +202,10 @@ resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
194
202
  name: 'AZURE_QUEUE_NAME'
195
203
  value: queueName
196
204
  }
205
+ {
206
+ name: 'AZURE_IMAGE_OPTIMIZATION_CACHE'
207
+ value: 'true'
208
+ }
197
209
  {
198
210
  name: 'NODE_ENV'
199
211
  value: 'production'
@@ -0,0 +1,5 @@
1
+ import { ImageLoader } from '@opennextjs/aws/types/overrides.js';
2
+
3
+ declare const azureBlobImageLoader: ImageLoader;
4
+
5
+ export { azureBlobImageLoader as default };
@@ -0,0 +1,5 @@
1
+ import { ImageLoader } from '@opennextjs/aws/types/overrides.js';
2
+
3
+ declare const azureBlobImageLoader: ImageLoader;
4
+
5
+ export { azureBlobImageLoader as default };
@@ -0,0 +1,37 @@
1
+ import { Readable } from 'node:stream';
2
+
3
+ const { AZURE_STORAGE_ACCOUNT_NAME } = process.env;
4
+ const azureBlobImageLoader = {
5
+ name: "azure-blob",
6
+ load: async (key) => {
7
+ if (!AZURE_STORAGE_ACCOUNT_NAME) {
8
+ throw new Error("AZURE_STORAGE_ACCOUNT_NAME must be defined");
9
+ }
10
+ const cleanKey = key.replace(/^\//, "");
11
+ const blobUrl = `https://${AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net/assets/${cleanKey}`;
12
+ try {
13
+ const response = await fetch(blobUrl);
14
+ if (response.status === 404) {
15
+ throw new Error(`Image not found in blob storage: ${cleanKey}`);
16
+ }
17
+ if (!response.ok) {
18
+ throw new Error(`Failed to fetch image. Status: ${response.status}`);
19
+ }
20
+ if (!response.body) {
21
+ throw new Error("No body in fetch response");
22
+ }
23
+ const arrayBuffer = await response.arrayBuffer();
24
+ const buffer = Buffer.from(arrayBuffer);
25
+ const body = Readable.from(buffer);
26
+ return {
27
+ body,
28
+ contentType: response.headers.get("content-type") ?? void 0,
29
+ cacheControl: response.headers.get("cache-control") ?? void 0
30
+ };
31
+ } catch (error) {
32
+ throw new Error(`Failed to load image from Azure Blob: ${error.message}`);
33
+ }
34
+ }
35
+ };
36
+
37
+ export { azureBlobImageLoader as default };
@@ -0,0 +1,6 @@
1
+ import { InternalEvent, InternalResult } from '@opennextjs/aws/types/open-next.js';
2
+ import { OpenNextHandler } from '@opennextjs/aws/types/overrides.js';
3
+
4
+ declare function createCachedImageOptimizationHandler(defaultHandler: OpenNextHandler<InternalEvent, InternalResult>): OpenNextHandler<InternalEvent, InternalResult>;
5
+
6
+ export { createCachedImageOptimizationHandler };
@@ -0,0 +1,6 @@
1
+ import { InternalEvent, InternalResult } from '@opennextjs/aws/types/open-next.js';
2
+ import { OpenNextHandler } from '@opennextjs/aws/types/overrides.js';
3
+
4
+ declare function createCachedImageOptimizationHandler(defaultHandler: OpenNextHandler<InternalEvent, InternalResult>): OpenNextHandler<InternalEvent, InternalResult>;
5
+
6
+ export { createCachedImageOptimizationHandler };
@@ -0,0 +1,115 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { BlobServiceClient } from '@azure/storage-blob';
3
+ import { ReadableStream } from 'node:stream/web';
4
+
5
+ const { AZURE_STORAGE_CONNECTION_STRING, AZURE_STORAGE_ACCOUNT_NAME } = process.env;
6
+ const CACHE_CONTAINER = "optimized-images";
7
+ function getBlobClient(key) {
8
+ if (!AZURE_STORAGE_CONNECTION_STRING && !AZURE_STORAGE_ACCOUNT_NAME) {
9
+ throw new Error("Azure Storage connection string or account name must be defined");
10
+ }
11
+ const blobServiceClient = AZURE_STORAGE_CONNECTION_STRING ? BlobServiceClient.fromConnectionString(AZURE_STORAGE_CONNECTION_STRING) : new BlobServiceClient(`https://${AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net`);
12
+ const containerClient = blobServiceClient.getContainerClient(CACHE_CONTAINER);
13
+ return containerClient.getBlockBlobClient(key);
14
+ }
15
+ function computeCacheKey(event) {
16
+ const { query } = event;
17
+ const url = Array.isArray(query?.url) ? query.url[0] : query?.url || "";
18
+ const width = Array.isArray(query?.w) ? query.w[0] : query?.w || "0";
19
+ const quality = Array.isArray(query?.q) ? query.q[0] : query?.q || "75";
20
+ const hash = createHash("sha256").update(url).digest("hex").substring(0, 16);
21
+ return `${hash}/w${width}_q${quality}.cache`;
22
+ }
23
+ async function getCachedImage(cacheKey) {
24
+ try {
25
+ const blobClient = getBlobClient(cacheKey);
26
+ const exists = await blobClient.exists();
27
+ if (!exists) {
28
+ return null;
29
+ }
30
+ const downloadResponse = await blobClient.download();
31
+ const properties = await blobClient.getProperties();
32
+ if (!downloadResponse.readableStreamBody) {
33
+ return null;
34
+ }
35
+ const chunks = [];
36
+ for await (const chunk of downloadResponse.readableStreamBody) {
37
+ chunks.push(Buffer.from(chunk));
38
+ }
39
+ const buffer = Buffer.concat(chunks);
40
+ return {
41
+ type: "core",
42
+ statusCode: 200,
43
+ headers: {
44
+ "Content-Type": properties.contentType || "image/webp",
45
+ "Cache-Control": properties.cacheControl || "public,max-age=31536000,immutable",
46
+ Vary: "Accept"
47
+ },
48
+ body: new ReadableStream({
49
+ start(controller) {
50
+ controller.enqueue(buffer);
51
+ controller.close();
52
+ }
53
+ }),
54
+ isBase64Encoded: true
55
+ };
56
+ } catch (error) {
57
+ return null;
58
+ }
59
+ }
60
+ async function setCachedImage(cacheKey, result) {
61
+ try {
62
+ if (!result.body) {
63
+ return;
64
+ }
65
+ const chunks = [];
66
+ for await (const chunk of result.body) {
67
+ chunks.push(Buffer.from(chunk));
68
+ }
69
+ const buffer = Buffer.concat(chunks);
70
+ const blobClient = getBlobClient(cacheKey);
71
+ const contentTypeRaw = result.headers?.["Content-Type"] || result.headers?.["content-type"];
72
+ const contentType = Array.isArray(contentTypeRaw) ? contentTypeRaw[0] : contentTypeRaw || "image/webp";
73
+ const cacheControlRaw = result.headers?.["Cache-Control"] || result.headers?.["cache-control"];
74
+ const cacheControl = Array.isArray(cacheControlRaw) ? cacheControlRaw[0] : cacheControlRaw || "public,max-age=31536000,immutable";
75
+ await blobClient.upload(buffer, buffer.length, {
76
+ blobHTTPHeaders: {
77
+ blobContentType: contentType,
78
+ blobCacheControl: cacheControl
79
+ }
80
+ });
81
+ } catch (error) {
82
+ console.error("Failed to cache optimized image:", error);
83
+ }
84
+ }
85
+ function createCachedImageOptimizationHandler(defaultHandler) {
86
+ return async (event, options) => {
87
+ const cacheKey = computeCacheKey(event);
88
+ const cached = await getCachedImage(cacheKey);
89
+ if (cached) {
90
+ return cached;
91
+ }
92
+ const result = await defaultHandler(event, options);
93
+ if (result.statusCode === 200 && result.body) {
94
+ const chunks = [];
95
+ for await (const chunk of result.body) {
96
+ chunks.push(Buffer.from(chunk));
97
+ }
98
+ const buffer = Buffer.concat(chunks);
99
+ const resultWithBuffer = {
100
+ ...result,
101
+ body: new ReadableStream({
102
+ start(controller) {
103
+ controller.enqueue(buffer);
104
+ controller.close();
105
+ }
106
+ })
107
+ };
108
+ await setCachedImage(cacheKey, resultWithBuffer);
109
+ return resultWithBuffer;
110
+ }
111
+ return result;
112
+ };
113
+ }
114
+
115
+ export { createCachedImageOptimizationHandler };
@@ -1,5 +1,5 @@
1
1
  import { RoutePreloadingBehavior, OpenNextConfig } from '@opennextjs/aws/types/open-next.js';
2
- import { IncrementalCache, TagCache, Queue } from '@opennextjs/aws/types/overrides.js';
2
+ import { IncrementalCache, TagCache, Queue, ImageLoader } from '@opennextjs/aws/types/overrides.js';
3
3
 
4
4
  type AzureDeploymentTarget = "functions" | "static-web-apps" | "container-apps";
5
5
  interface AzureDeploymentConfig {
@@ -19,6 +19,8 @@ interface AzureConfig {
19
19
  incrementalCache?: "azure-blob" | IncrementalCache;
20
20
  tagCache?: "azure-table" | TagCache;
21
21
  queue?: "azure-queue" | Queue;
22
+ imageLoader?: "azure-blob" | ImageLoader | (() => Promise<ImageLoader>);
23
+ enableImageOptimizationCache?: boolean;
22
24
  routePreloadingBehavior?: RoutePreloadingBehavior;
23
25
  middleware?: OpenNextConfig["middleware"];
24
26
  dangerous?: OpenNextConfig["dangerous"];
@@ -1,5 +1,5 @@
1
1
  import { RoutePreloadingBehavior, OpenNextConfig } from '@opennextjs/aws/types/open-next.js';
2
- import { IncrementalCache, TagCache, Queue } from '@opennextjs/aws/types/overrides.js';
2
+ import { IncrementalCache, TagCache, Queue, ImageLoader } from '@opennextjs/aws/types/overrides.js';
3
3
 
4
4
  type AzureDeploymentTarget = "functions" | "static-web-apps" | "container-apps";
5
5
  interface AzureDeploymentConfig {
@@ -19,6 +19,8 @@ interface AzureConfig {
19
19
  incrementalCache?: "azure-blob" | IncrementalCache;
20
20
  tagCache?: "azure-table" | TagCache;
21
21
  queue?: "azure-queue" | Queue;
22
+ imageLoader?: "azure-blob" | ImageLoader | (() => Promise<ImageLoader>);
23
+ enableImageOptimizationCache?: boolean;
22
24
  routePreloadingBehavior?: RoutePreloadingBehavior;
23
25
  middleware?: OpenNextConfig["middleware"];
24
26
  dangerous?: OpenNextConfig["dangerous"];
@@ -96,6 +96,14 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
96
96
  publicAccess: 'Blob'
97
97
  }
98
98
  }
99
+
100
+ // Container for optimized images (public CDN access)
101
+ resource optimizedImagesContainer 'containers' = {
102
+ name: 'optimized-images'
103
+ properties: {
104
+ publicAccess: 'Blob'
105
+ }
106
+ }
99
107
  }
100
108
 
101
109
  // Table service for tag cache
@@ -194,6 +202,10 @@ resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
194
202
  name: 'AZURE_QUEUE_NAME'
195
203
  value: queueName
196
204
  }
205
+ {
206
+ name: 'AZURE_IMAGE_OPTIMIZATION_CACHE'
207
+ value: 'true'
208
+ }
197
209
  {
198
210
  name: 'NODE_ENV'
199
211
  value: 'production'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opennextjs-azure",
3
- "version": "0.1.2",
4
- "description": "Azure adapter for Next.js applications using OpenNext",
3
+ "version": "0.1.3",
4
+ "description": "True serverless Next.js on Azure Functions with a Vercel-grade developer experience",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -69,7 +69,6 @@
69
69
  "bugs": {
70
70
  "url": "https://github.com/zpg6/opennextjs-azure/issues"
71
71
  },
72
- "homepage": "https://opennext.js.org/azure",
73
72
  "dependencies": {
74
73
  "@opennextjs/aws": "^3.8.5",
75
74
  "@azure/functions": "^4.5.1",