Skip to content

fix(image): align next/image API with Next.js 16.0.0#385

Closed
Divkix wants to merge 15 commits intocloudflare:mainfrom
Divkix:fix/align-image-api-nextjs-16
Closed

fix(image): align next/image API with Next.js 16.0.0#385
Divkix wants to merge 15 commits intocloudflare:mainfrom
Divkix:fix/align-image-api-nextjs-16

Conversation

@Divkix
Copy link
Contributor

@Divkix Divkix commented Mar 9, 2026

Pre-requisite for #364 — This PR aligns the API surface; #364 adds Sharp-based build-time image optimization on top.

Closes #386

Summary

Aligns vinext's next/image implementation with the Next.js 16.0.0 API surface. Any library or app code written against Next.js 16's image API should now work on vinext without modification.

Changes

  • StaticImageData: Add blurWidth and blurHeight optional fields
  • imageSizes default: Remove 16[32, 48, 64, 96, 128, 256, 384] (matches Next.js 16)
  • contentDispositionType default: "inline""attachment" (security — prevents XSS via image endpoint)
  • preload prop: New prop replacing deprecated priority (both now trigger loading="eager" + fetchPriority="high")
  • qualities config: Add images.qualities support with findClosestQuality() rounding and server-side validation
  • AVIF quality offset: Apply Math.max(quality - 20, 1) for AVIF in handleImageOptimization (matches Next.js)
  • Deprecated props: Accept and ignore onLoadingComplete, layout, objectFit, objectPosition, lazyBoundary, lazyRoot
  • PlaceholderValue type: Widen to "blur" | "empty" | \data:image/${string}``
  • width/height type: Widen to number | \${number}`` with automatic string→number parsing
  • Type exports: Export StaticRequire, StaticImport, ImageLoaderProps, ImageProps
  • allowedQualities: Add quality validation to parseImageParams

What's deferred

localPatterns, minimumCacheTTL, loader/loaderFile global config, disableStaticImages, maximumRedirects, dangerouslyAllowLocalIP, images.unoptimized global, @hapi/accept negotiation, and strict w/q required params — these are either not yet needed or would break existing vinext users.

Test plan

  • pnpm test tests/image-component.test.ts — 41 tests pass (new tests for preload, deprecated props, string dims, data placeholder, StaticImport, type exports)
  • pnpm test tests/image-imports.test.ts — 20 tests pass
  • pnpm test tests/shims.test.ts — 577 pass (updated DEFAULT_IMAGE_SIZES and contentDisposition default assertions)
  • pnpm test tests/deploy.test.ts — 193 tests pass
  • pnpm test tests/image-config.test.ts — 33 tests pass
  • pnpm run typecheck — no new errors (pre-existing __NEXT_DATA__ errors only)
  • pnpm run lint — clean

Closes multiple compatibility gaps between vinext's Image shim and
Next.js 16's public API:

- Add blurWidth/blurHeight to StaticImageData interface
- Fix imageSizes default: remove 16 to match [32,48,64,96,128,256,384]
- Fix contentDispositionType default: "inline" → "attachment" (security)
- Add preload prop (Next.js 16 replacement for deprecated priority)
- Add qualities config support with findClosestQuality() rounding
- Apply AVIF quality offset (quality - 20) in handleImageOptimization
- Add deprecated props (onLoadingComplete, layout, objectFit, etc.)
- Widen PlaceholderValue type to accept data:image/ URLs
- Widen width/height to accept string numbers (number | `${number}`)
- Export StaticRequire, StaticImport, ImageLoaderProps, ImageProps types
- Add allowedQualities param to parseImageParams validation
Copilot AI review requested due to automatic review settings March 9, 2026 16:41
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 9, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@385

commit: 647026f

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aligns vinext's next/image shim with the Next.js 16.0.0 API surface. It updates defaults (image sizes, Content-Disposition), adds new props (preload, deprecated compat props, string width/height, data:image/ placeholder), exports new types, adds quality allowlist support (images.qualities), and applies the AVIF quality offset used in Next.js 16.

Changes:

  • Aligns next/image types and props with Next.js 16 (new preload prop, deprecated props, PlaceholderValue type, StaticImport/StaticRequire types, string dimensions)
  • Updates image config defaults: DEFAULT_IMAGE_SIZES drops 16, Content-Disposition changes from "inline" to "attachment" for XSS protection
  • Adds images.qualities config support with client-side findClosestQuality() rounding and server-side parseImageParams validation, plus AVIF quality offset in the optimization handler

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/vinext/src/shims/image.tsx Core Next.js 16 API alignment: new types, props, and quality URL building
packages/vinext/src/server/image-optimization.ts DEFAULT_IMAGE_SIZES update, allowedQualities param in parseImageParams, Content-Disposition default, AVIF quality offset
packages/vinext/src/server/prod-server.ts App Router server Content-Disposition default updated to "attachment"
packages/vinext/src/config/next-config.ts Adds qualities field and updates JSDoc for contentDispositionType
packages/vinext/src/global.d.ts Adds __VINEXT_IMAGE_QUALITIES process env type declaration
packages/vinext/src/index.ts Exposes images.qualities via Vite define
tests/shims.test.ts Updates assertions for new defaults (DEFAULT_IMAGE_SIZES, Content-Disposition)
tests/image-component.test.ts New tests for Next.js 16 API surface features

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +76 to +79
if (allowedWidths && w !== 0 && !allowedWidths.includes(w)) return null;
// Validate quality (1-100)
if (Number.isNaN(q) || q < 1 || q > 100) return null;
if (allowedQualities && !allowedQualities.includes(q)) return null;
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new allowedQualities parameter in parseImageParams has no test coverage. The existing test suite in tests/shims.test.ts thoroughly tests every other parseImageParams validation (widths, quality range, URL safety, etc.) but there are no tests verifying that:

  1. A quality value not in allowedQualities returns null.
  2. A quality value that is in allowedQualities is accepted.

This gap is significant because the quality validation code path (line 79) is also never exercised through handleImageOptimization (see separate comment), making it hard to detect the wiring issue.

Copilot uses AI. Check for mistakes.
*/
function findClosestQuality(quality: number, qualities?: number[]): number {
if (!qualities?.length) return quality;
return qualities.reduce((prev, cur) => (Math.abs(cur - quality) < Math.abs(prev - quality) ? cur : prev), 0);
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The findClosestQuality reduce call uses 0 as the initial accumulator value (second argument to reduce). Since 0 is not a valid quality value, when all valid quality values in the array are farther from quality than 0 is, the function returns 0 instead of the closest valid quality.

For example, with qualities = [25, 50, 75, 100] and quality = 10:

  • abs(25 - 10) = 15 vs abs(0 - 10) = 10 → 0 wins
  • Same for all subsequent values
  • Returns 0, which is an invalid quality that will cause the image URL to carry ?q=0, and be rejected by the server's quality validation (q < 1 → return null).

The initial accumulator should be qualities[0] to ensure a valid quality value is always returned. Since the guard if (!qualities?.length) return quality already ensures the array has at least one element, qualities[0] is always safe here.

Copilot uses AI. Check for mistakes.
imageConfig?.contentSecurityPolicy ?? IMAGE_CONTENT_SECURITY_POLICY,
"X-Content-Type-Options": "nosniff",
"Content-Disposition": imageConfig?.contentDispositionType ?? "inline",
"Content-Disposition": imageConfig?.contentDispositionType ?? "attachment",
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR correctly changes the default Content-Disposition to "attachment" for the App Router production server (line 601), but the Pages Router production server (startPagesRouterServer, around line 775 in prod-server.ts) was not updated and still defaults to "inline".

This means the security fix is incomplete: images served through the Pages Router's Node.js production server will still use Content-Disposition: inline, leaving XSS-via-image-endpoint open on that code path.

The "Content-Disposition": pagesImageConfig?.contentDispositionType ?? "inline" on line 775 should be ?? "attachment" to match all other call sites.

Copilot uses AI. Check for mistakes.
Comment on lines 62 to 68
export function parseImageParams(
url: URL,
allowedWidths?: number[],
allowedQualities?: number[],
): { imageUrl: string; width: number; quality: number } | null {
const imageUrl = url.searchParams.get("url");
if (!imageUrl) return null;
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allowedQualities was added as a new parameter to parseImageParams, but handleImageOptimization (the main entry point used by all workers) calls parseImageParams(url, allowedWidths) without passing allowedQualities. This means the quality validation added here is never actually enforced through handleImageOptimization. The PR description claims "server-side validation" for qualities, but it is only reachable if callers invoke parseImageParams directly. To fully enforce the allowlist, allowedQualities (or qualities as part of ImageConfig) needs to be threaded through handleImageOptimization.

Copilot uses AI. Check for mistakes.
Comment on lines 165 to 166
@@ -166,10 +166,12 @@ export interface NextConfig {
imageSizes?: number[];
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc comment for imageSizes still says Defaults to Next.js defaults: [16, 32, 48, 64, 96, 128, 256, 384], but the actual default was changed to [32, 48, 64, 96, 128, 256, 384] (removing 16). This comment should be updated to reflect the new default.

Copilot uses AI. Check for mistakes.
@james-elicx
Copy link
Collaborator

/bigbonk review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work aligning the image API with Next.js 16. The type exports, preload prop, deprecated prop handling, StaticImport/StaticRequire unwrapping, AVIF quality offset, and Content-Disposition default change are all solid.

Two issues need fixing before merge (one correctness bug, one security gap), plus a few minor items.


Security: Pages Router Content-Disposition still defaults to "inline"

packages/vinext/src/server/prod-server.ts:775 was not updated:

"Content-Disposition": pagesImageConfig?.contentDispositionType ?? "inline",

This should be ?? "attachment" to match the App Router prod server (line 601), the handleImageOptimization helper (line 176), and all the updated tests. Without this, the XSS-via-image-endpoint mitigation is incomplete for Pages Router production deployments.


Stale JSDoc on imageSizes

packages/vinext/src/config/next-config.ts:165 still says:

Defaults to Next.js defaults: [16, 32, 48, 64, 96, 128, 256, 384]

but the actual default was changed to [32, 48, 64, 96, 128, 256, 384].


allowedQualities not wired through handleImageOptimization (non-blocking)

parseImageParams accepts allowedQualities as a third argument, but handleImageOptimization on line 207 calls parseImageParams(url, allowedWidths) without passing it. The server-side quality validation is unreachable from the main request handler. Since the client-side findClosestQuality rounds quality before building the URL, this is defense-in-depth rather than a correctness issue. Worth either wiring it through or removing the dead code path, but not a blocker.

*/
function findClosestQuality(quality: number, qualities?: number[]): number {
if (!qualities?.length) return quality;
return qualities.reduce((prev, cur) => (Math.abs(cur - quality) < Math.abs(prev - quality) ? cur : prev), 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: reduce uses 0 as the initial accumulator, which is not a valid quality value and wins the comparison whenever the closest real quality is farther from quality than 0 is.

Example: findClosestQuality(10, [25, 50, 75, 100]) returns 0 because |0 - 10| = 10 < |25 - 10| = 15. That q=0 then gets rejected server-side (q < 1 → null → 400).

The guard on line 139 already ensures qualities has at least one element, so qualities[0] is safe:

Suggested change
return qualities.reduce((prev, cur) => (Math.abs(cur - quality) < Math.abs(prev - quality) ? cur : prev), 0);
return qualities.reduce((prev, cur) => (Math.abs(cur - quality) < Math.abs(prev - quality) ? cur : prev), qualities[0]);

});

// ─── Next.js 16 API surface alignment ────────────────────────────────

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage: findClosestQuality and allowedQualities server validation have no tests. The findClosestQuality bug (initial accumulator 0) would have been caught by a test like:

describe("findClosestQuality", () => {
  it("returns closest allowed quality for low input", () => {
    // With qualities [25, 50, 75, 100] and quality 10, should return 25
    expect(imageOptimizationUrl("/test.png", 100, 10)).toContain("q=25");
  });
});

Also consider a parseImageParams test for quality not in allowedQualities returning null.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 9, 2026

Review submitted successfully. Here's a summary of the feedback:

Requested changes on PR #385:

  1. Bug (blocking) — findClosestQuality initial accumulator: The reduce call uses 0 as the initial value, which is an invalid quality. When all allowed qualities are farther from the input than 0, the function returns 0, causing a server-side 400 error. Fix: use qualities[0] instead. Inline suggestion provided.

  2. Security gap (blocking) — Pages Router Content-Disposition: prod-server.ts:775 still defaults to "inline" for the Pages Router production server, while the App Router server, handleImageOptimization, and all tests were updated to "attachment". This leaves the XSS mitigation incomplete.

  3. Stale JSDoc (minor): next-config.ts:165 still documents imageSizes default as including 16, but the actual default was changed to start at 32.

  4. Dead code path (non-blocking observation): allowedQualities is accepted by parseImageParams but never passed from handleImageOptimization, making server-side quality validation unreachable.

  5. Missing test coverage (non-blocking): No tests for findClosestQuality edge cases or allowedQualities server validation — the accumulator bug would have been caught with a simple test.

github run

- Fix findClosestQuality accumulator bug: use qualities[0] instead of 0
  to prevent returning invalid quality when all allowed values are far
  from input
- Fix Pages Router Content-Disposition default from "inline" to
  "attachment" to close XSS-via-image-endpoint vector
- Wire allowedQualities through handleImageOptimization to parseImageParams
  so server-side quality validation is no longer dead code
- Fix stale JSDoc on imageSizes default (remove non-existent 16)
- Add findClosestQuality test coverage: low input, exact match, rounding,
  equidistant tiebreak, single-element array
@james-elicx
Copy link
Collaborator

/bigbonk re-review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work. The second commit addressed all the blocking issues from the prior reviews (the findClosestQuality accumulator bug, the Pages Router Content-Disposition gap, and the stale JSDoc). The implementation is clean and well-tested.

Two non-blocking observations — neither is worth blocking the PR over, but they'd be good follow-ups.


allowedQualities not wired from callers (non-blocking)

handleImageOptimization now accepts allowedQualities as the 5th parameter, but no caller passes it:

  • deploy.ts:454 (App Router worker) — passes allowedWidths only
  • deploy.ts:545 (Pages Router worker) — passes allowedWidths and imageConfig only
  • All tests call with at most 4 args

The client-side findClosestQuality rounding means this is defense-in-depth, not a correctness issue — the URL will already carry a valid quality value by the time it hits the server. But the dead code path is confusing. Consider either wiring allowedQualities through from config in a follow-up, or removing the server-side check until it's actually needed.

Remote image path doesn't handle data:image/ placeholder (minor)

The data:image/ placeholder handling was added to the local image path (image.tsx:410-413) and getImageProps (image.tsx:577-579), but the remote image code path (image.tsx:364-365) still only checks placeholder === "blur". A remote image with placeholder="data:image/png;base64,..." won't show the blur background when rendered via @unpic/react. This is a minor edge case, but worth noting for completeness.

handlers: ImageHandlers,
allowedWidths?: number[],
imageConfig?: ImageConfig,
allowedQualities?: number[],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: allowedQualities is accepted here but no caller passes it. deploy.ts:454 and deploy.ts:545 both omit the 5th argument, and the prod-server uses parseImageParams directly without qualities. This makes the server-side quality validation unreachable in practice.

Since findClosestQuality on the client side already rounds to a valid value before building the URL, this isn't a correctness issue. Consider either wiring it through from config in a follow-up or deferring the parameter until callers need it.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 9, 2026

Review submitted successfully. Here's a summary of what I found:

Approved — the PR is in good shape after the second commit addressed all blocking issues from earlier reviews.

What was fixed in the second commit

  • findClosestQuality accumulator bug (now correctly uses qualities[0] instead of 0)
  • Pages Router Content-Disposition default updated to "attachment"
  • JSDoc for imageSizes updated to reflect the new default (without 16)
  • Tests added for findClosestQuality edge cases

Two non-blocking observations

  1. allowedQualities is dead code server-sidehandleImageOptimization accepts it but no caller passes it (deploy.ts, prod-server, tests all omit it). The client-side findClosestQuality rounding makes this defense-in-depth, not a bug, but the unused parameter is confusing.

  2. Remote image path misses data:image/ placeholder — The new data:image/ placeholder support was added for local images and getImageProps, but the @unpic/react code path for remote images still only checks placeholder === "blur". Minor edge case.

github run

@Divkix
Copy link
Contributor Author

Divkix commented Mar 9, 2026

I'll work on the 2 non-blocking issues and also on readme and the skills command

…/ placeholder

- Remove `allowedQualities` from `parseImageParams()` and
  `handleImageOptimization()` — no caller ever passed it, and
  client-side `findClosestQuality` rounding is sufficient.
- Fix remote image code path to handle `data:image/` placeholder
  strings (Next.js 16), matching the local image and `getImageProps`
  code paths that already handled this.
- Add test for remote image `data:image/` placeholder (49/49 pass).
- Update README: next/image coverage, images config details, known
  limitations wording.
- Update TRACKING.md test counts (P5-2: 22→49, total: 230→257).
- Update migration skill compatibility doc.
@james-elicx
Copy link
Collaborator

/bigbonk re-review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thorough and well-structured alignment with the Next.js 16 image API. The three commits tell a clean story: initial implementation, review feedback fixes, and non-blocking follow-ups. All the blocking issues from prior reviews have been addressed.

A few minor observations below — none are blockers.

function toNumber(value: number | `${number}` | undefined): number | undefined {
if (value === undefined) return undefined;
if (typeof value === "number") return value;
return parseInt(value, 10);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: parseInt with radix 10 will parse "200px" as 200 and "" as NaN. The empty string case is harmless (returns undefined via the guard above), but a string like "200px" silently becoming 200 might mask user errors.

Next.js 16 types this as number | \${number}`` so in practice only pure numeric strings are expected — this is fine as-is, just noting the behavior.

blurDataURL ?? (typeof unwrapped === "object" ? unwrapped.blurDataURL : undefined);

// Both priority and preload trigger eager loading (Next.js 16 compat)
const isEager = priority || preload;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: priority || preload evaluates left-to-right and both are optional booleans defaulting to undefined. When both are falsy, isEager is undefined (falsy), which works correctly in the ternary on line 329. Just noting that isEager is boolean | undefined, not strictly boolean. Not a bug, but !!priority || !!preload or Boolean(priority || preload) would be slightly more explicit if you wanted a strict boolean.

}),
);
expect(html).toContain('loading="eager"');
expect(html).toContain('fetchPriority="high"');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good coverage of preload prop. Consider adding a test that verifies preload and priority together don't conflict (both true should still produce loading="eager" and fetchPriority="high" — not double attributes). Currently the test only tests them independently.

try {
// AVIF encodes more efficiently, so use a lower quality value for
// equivalent perceptual quality. Matches Next.js 16 behavior.
const adjustedQuality = format === "image/avif" ? Math.max(quality - 20, 1) : quality;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AVIF quality offset (quality - 20) is a significant behavioral change for production image serving. There's no test coverage verifying this adjustment — the existing handleImageOptimization transform test on line 6050 of shims.test.ts only tests WebP (which doesn't get the offset). A test with Accept: image/avif that captures the options passed to transformImage and asserts quality === 55 for input q=75 would lock in this behavior.

Not blocking since this matches documented Next.js 16 behavior, but worth adding to prevent accidental regression.

width={fill ? undefined : imgWidth}
height={fill ? undefined : imgHeight}
loading={priority ? "eager" : (loading ?? "lazy")}
loading={isEager ? "eager" : (loading ?? "lazy")}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom loader path sets loading and decoding but doesn't set fetchPriority like the local image path does (line 467). In Next.js 16, priority/preload adds fetchPriority="high" regardless of whether a custom loader is used. Minor gap.

Suggested change
loading={isEager ? "eager" : (loading ?? "lazy")}
loading={isEager ? "eager" : (loading ?? "lazy")}
fetchPriority={isEager ? "high" : undefined}
decoding="async"

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 9, 2026

Review submitted. Here's a summary of my findings:

Approved — the PR is solid after three commits that progressively addressed all prior review feedback.

What's good

  • Clean Next.js 16 API alignment: preload, qualities, string dimensions, data:image/ placeholders, deprecated prop handling, type exports
  • Security fix (Content-Disposition: attachment) applied consistently across all four code paths (App Router prod, Pages Router prod, handleImageOptimization helper, image config)
  • findClosestQuality accumulator bug was fixed (uses qualities[0] instead of 0) with good test coverage
  • Dead allowedQualities server-side parameter was cleaned up in the third commit
  • Remote data:image/ placeholder gap was addressed in the third commit

Minor observations (non-blocking)

  1. Missing fetchPriority on custom loader path — the loader code path sets loading="eager" for preload/priority but doesn't add fetchPriority="high", unlike the local and remote image paths. Suggested a fix.
  2. No test for AVIF quality offset — the -20 quality adjustment in handleImageOptimization has no test coverage. The existing transform test only uses WebP.
  3. Minor type loosenessisEager is boolean | undefined rather than strict boolean (works fine in practice).
  4. Missing combined preload+priority test — tests cover each independently but not both together.

github run

@Divkix
Copy link
Contributor Author

Divkix commented Mar 9, 2026

Will work on these and try to match with latest release v16.1 to verify no changes are there so it could be latest

…/preload tests

The custom loader <img> tag was missing fetchPriority={isEager ? "high" : undefined},
which the local image path already had. This caused priority/preload to set
loading="eager" but not fetchPriority="high" when using a custom loader.

Tests added:
- AVIF quality offset: verifies -20 adjustment (q=75→55) and floor clamp (q=20→1)
- WebP gets no AVIF offset (control test)
- Combined preload + priority props render without conflict
- Custom loader with priority/preload now correctly sets fetchPriority="high"
@Divkix
Copy link
Contributor Author

Divkix commented Mar 9, 2026

Implemented the Next.js 16 contract-layer pass for next/image on this branch.

What changed:

  • Reworked the next/image shim around a shared prop-normalization path so <Image> and getImageProps() follow the same behavior.
  • Aligned fixed-width srcSet generation with Next.js allSizes / 1x, 2x selection instead of the previous device-size-only logic.
  • Added overrideSrc, global images.unoptimized, data: / blob: unoptimized handling, static-import aspect-ratio inference, and Next 16.1 dpl propagation for local image URLs.
  • Switched preload / priority behavior to emit preload hints instead of forcing loading="eager" and fetchPriority="high".
  • Expanded the image config surface and normalization (loader, loaderFile, path, localPatterns, formats, qualities, minimumCacheTTL, maximumRedirects, maximumResponseBody, dangerouslyAllowLocalIP, disableStaticImages, etc.).
  • Tightened the shared optimizer contract for required w / q, local/remote allowlists, config-driven cache headers, format negotiation, and filename-bearing Content-Disposition.
  • Kept PR feat(image): build-time image optimization via Sharp #364 boundaries intact: no Sharp/build-time optimization work and no vinext-only public image metadata added here.

Verification:

  • pnpm test tests/image-component.test.ts tests/image-config.test.ts tests/image-imports.test.ts tests/next-config.test.ts
  • pnpm run typecheck

@Divkix
Copy link
Contributor Author

Divkix commented Mar 9, 2026

@james-elicx should i merge this in #364 instead? the vitest is failing because of work to be done in #364

@james-elicx
Copy link
Collaborator

Hmm I would have assumed we'd be able to do this without adding the new Sharp optimisation... Was it the aligning with next 16 that introduced that problem? Happy to go back a couple commits and merge based on the Bonk review a few commits ago if you want to keep the PRs separate?

@Divkix
Copy link
Contributor Author

Divkix commented Mar 9, 2026

Okay, I tried something and this should fix the ci

@james-elicx
Copy link
Collaborator

/bigbonk re-review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

CI is green, and the core image API alignment with Next.js 16 is well-executed — the getImgProps-style normalization, preload prop, qualities rounding, StaticImport unwrapping, AVIF quality offset, and Content-Disposition default change all look correct and match upstream.

I have a few concerns below, one security issue that should be addressed before merge, and some scope/architecture observations.

Security: fetchRemoteImage forwards all request headers to external hosts

This is the most important issue. The fetch(currentUrl, { headers: request.headers }) call at line 228 forwards the entire original request's headers — including Cookie, Authorization, and any custom tokens — to whatever external host is configured in remotePatterns. An attacker who gets a domain added to remotePatterns (or exploits an overly broad pattern) can exfiltrate session tokens. Next.js only forwards a selective subset of headers (primarily Accept). This should be fixed before merge.

Scope creep: resolveId refactoring

The resolveId hook was changed from the optimized { filter, handler } object form to a plain function, removing the Vite hook filter optimization. This means the hook now runs for every module resolution in the project, not just next/* and virtual:vinext-* imports. The new direct alias resolution, extensionless import resolution for node_modules, and .js variant generation are useful ecosystem fixes but are unrelated to next/image and would benefit from their own PR with targeted testing. Including them here makes the diff harder to review and the risk surface larger.

source.body reuse after transformImage failure

In the catch block at line 343-349, source.body is reused after being passed to transformImage. If transformImage partially consumed the ReadableStream before throwing, the stream is locked/consumed and cannot be passed to a new Response. This will produce a runtime error or truncated response. Consider buffering the source body into an ArrayBuffer before passing it to transformImage, or returning a 500 error in the catch path instead of attempting to serve the original.

isBlockedLocalHost has coverage gaps

The SSRF protection at line 192 misses several common bypass patterns:

  • 0.0.0.0 (equivalent to localhost on many systems)
  • IPv4-mapped IPv6 addresses like ::ffff:127.0.0.1 or ::ffff:10.0.0.1
  • The fc/fd prefix check at line 197 matches any hostname starting with those letters (e.g. fcbarcelona.com), not just IPv6 ULA addresses

The fc/fd check should verify the string is actually an IPv6 address before matching. Consider checking for : in the hostname.

Minor observations (non-blocking)

  1. No test for AVIF quality offset — the -20 adjustment in handleImageOptimization line 327 has no test coverage. A test with Accept: image/avif that verifies quality === 55 for input q=75 would lock in this behavior.

  2. Remote image fetching is untestedfetchRemoteImage, fetchSourceImage, isBlockedLocalHost, and toContentDispositionFilename have zero unit test coverage. These are security-critical code paths.

  3. __NEXT_DATA__ type declaration (global.d.ts) — This was added in this PR but is unrelated to image changes. The PR description doesn't mention it.

}

const response = await fetch(currentUrl, {
headers: request.headers,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security issue: Forwarding all request headers to external hosts leaks sensitive data (Cookie, Authorization, custom tokens) to any domain configured in remotePatterns. Next.js only forwards a selective subset.

Suggested change
headers: request.headers,
const response = await fetch(currentUrl, {
headers: { Accept: request.headers.get("Accept") ?? "*/*" },
redirect: "manual",
});

headers.set("Cache-Control", getCacheControl(imageConfig));
headers.set("Vary", "Accept");
setImageSecurityHeaders(headers, params.imageUrl, sourceMediaType ?? null, imageConfig);
return new Response(source.body, { status: 200, headers });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: source.body was already passed to handlers.transformImage() on line 329. If transformImage partially consumed the ReadableStream before throwing, this stream is locked/consumed and cannot be reused in a new Response. This will cause a runtime error or produce a truncated response.

Consider either:

  1. Buffering the source into an ArrayBuffer before passing to transformImage (so you can reconstruct a fresh stream on failure), or
  2. Returning a 500 error here instead of attempting to serve the already-consumed original.

Comment on lines +197 to +198
if (lower === "::1" || lower.startsWith("fc") || lower.startsWith("fd")) {
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The fc/fd prefix check matches any hostname starting with those letters, not just IPv6 ULA addresses. For example, fcbarcelona.com or fdny.gov would be incorrectly blocked.

This should verify the string is actually an IPv6 address (contains :) before checking the prefix:

Suggested change
if (lower === "::1" || lower.startsWith("fc") || lower.startsWith("fd")) {
return true;
if (lower === "::1" || (lower.includes(":") && (lower.startsWith("fc") || lower.startsWith("fd")))) {

Comment on lines 192 to 210
function isBlockedLocalHost(hostname: string): boolean {
const lower = hostname.toLowerCase();
if (lower === "localhost" || lower.endsWith(".localhost") || lower.endsWith(".local")) {
return true;
}
if (lower === "::1" || lower.startsWith("fc") || lower.startsWith("fd")) {
return true;
}
const ipv4 = lower.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
if (!ipv4) return false;
const [a, b] = [parseInt(ipv4[1], 10), parseInt(ipv4[2], 10)];
return (
a === 10 ||
a === 127 ||
(a === 169 && b === 254) ||
(a === 172 && b >= 16 && b <= 31) ||
(a === 192 && b === 168)
);
headers.set("X-Content-Type-Options", "nosniff");
headers.set("Content-Disposition", config?.contentDispositionType ?? "inline");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing check for 0.0.0.0 — on many systems this is equivalent to localhost. Also missing checks for IPv4-mapped IPv6 addresses (::ffff:127.0.0.1, ::ffff:10.x.x.x, etc.), which are common SSRF bypass techniques.

Consider adding:

if (lower === '0.0.0.0') return true;
if (lower.startsWith('::ffff:')) {
  const mapped = lower.slice(7);
  // Recurse with the mapped IPv4 address
  return isBlockedLocalHost(mapped);
}

Comment on lines +1342 to +1383
resolveId(id, importer) {
// Strip \0 prefix if present — @vitejs/plugin-rsc's generated
// browser entry imports our virtual module using the already-resolved
// ID (with \0 prefix). We need to re-resolve it so the client
// environment's import-analysis can find it.
const cleanId = id.startsWith("\0") ? id.slice(1) : id;

// Resolve shim and user aliases directly here instead of relying only
// on Vite's alias plugin. The dev module runner can otherwise fall
// back to native package resolution for dependency imports, which
// breaks ecosystem packages like nuqs and next-intl.
const directAlias = nextShimMap[cleanId];
if (directAlias) {
return directAlias;
}

// Handle extensionless relative imports inside node_modules packages.
// Some ESM packages (validator/es) publish `./foo` imports without an
// extension, which Node rejects during SSR if the module runner lets
// them escape Vite's resolver.
if (
importer &&
cleanId.startsWith(".") &&
path.extname(cleanId) === "" &&
importer.includes("node_modules")
) {
const importerPath = importer.startsWith("\0") ? importer.slice(1) : importer;
const importerFile = importerPath.split("?")[0];
if (path.isAbsolute(importerFile)) {
const basePath = path.resolve(path.dirname(importerFile), cleanId);
for (const ext of [".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".json"]) {
const fileCandidate = basePath + ext;
if (fs.existsSync(fileCandidate)) {
return fileCandidate;
}
const indexCandidate = path.join(basePath, `index${ext}`);
if (fs.existsSync(indexCandidate)) {
return indexCandidate;
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scope concern: This resolveId refactoring removes the Vite hook filter (filter: { id: /(?:next\/|virtual:vinext-)/ }) and replaces it with a plain function that runs for every module resolution in the project. The new direct alias resolution (line 1353), extensionless import resolution for node_modules (line 1362-1383), and .js variant generation (line 943 in configResolved) are useful ecosystem fixes, but they're unrelated to the next/image API alignment.

Running resolveId on every import (including react, vite, all node_modules, etc.) instead of only next/* and virtual:vinext-* is a performance regression for large projects. I'd suggest splitting this into its own PR where it can be tested independently — especially the fs.existsSync calls on every extensionless relative import from node_modules, which are synchronous I/O in a hot path.

unoptimized = true;
}

const qualityInt = getInt(quality);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: qualityInt is passed to generateImgAttrs and then to the loader, but when no quality prop is given, qualityInt is undefined. The default loader handles this via quality ?? 75 in defaultLoader (line 365), but custom loaders receive undefined as the quality property of ImageLoaderProps. This matches Next.js behavior (where quality is optional in ImageLoaderProps), but it's worth noting that custom loaders are responsible for defaulting quality themselves.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 9, 2026

Review posted successfully. Here's a summary of what I flagged:

Security (should fix before merge):

  • fetchRemoteImage forwards all request headers (including Cookie, Authorization) to external hosts — should only forward Accept
  • isBlockedLocalHost has SSRF bypass gaps: missing 0.0.0.0, IPv4-mapped IPv6, and the fc/fd prefix check incorrectly matches non-IPv6 hostnames like fcbarcelona.com

Bug:

  • source.body is reused in the catch block after transformImage may have partially consumed the ReadableStream — will cause runtime errors or truncated responses

Scope concern:

  • The resolveId refactoring removes the Vite hook filter optimization, making it run on every module resolution. This plus the extensionless import resolver and .js variant generation are unrelated to next/image and should ideally be in their own PR

Non-blocking observations:

  • No test coverage for AVIF quality offset, remote image fetching, SSRF protection, or Content-Disposition filename derivation
  • __NEXT_DATA__ type change is unrelated to image work

github run

@Divkix
Copy link
Contributor Author

Divkix commented Mar 9, 2026

Addressed the review items in this branch:

  • Fixed the remote image header leak in fetchRemoteImage() by forwarding only Accept upstream.
  • Hardened the literal local-host / private-IP checks for 0.0.0.0, IPv4-mapped IPv6, and IPv6 ULA/link-local ranges, while avoiding the fc* hostname false positive. Redirect hops are re-checked before following.
  • Fixed the transform fallback path by buffering the source image once and recreating a fresh stream for transformImage(), so a partially consumed source stream no longer breaks the fallback response.
  • Restored the filtered resolveId hook shape and removed the unrelated runtime resolver broadening; also removed the unrelated window.__NEXT_DATA__ redeclaration.

Coverage updates:

  • Added tests for remote-header forwarding, local-IP blocking, redirect-to-local blocking, remote filename derivation, and transform failure after stream consumption in tests/shims.test.ts.
  • AVIF quality-offset coverage was already present and remains in tests/shims.test.ts.
  • The custom-loader quality behavior is intentionally unchanged; custom loaders still receive optional quality, matching Next.js.

Validation run locally:

  • pnpm test tests/shims.test.ts
  • pnpm test tests/ecosystem.test.ts
  • pnpm run typecheck

@james-elicx
Copy link
Collaborator

/bonk re-review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Good work aligning the next/image API surface with Next.js 16. The shared normalizeProps path for <Image> and getImageProps(), the preload link-hint emission via ServerInsertedHTMLContext, the SSRF protection with per-hop redirect checking, the stream-buffering strategy for transform fallback, and the Accept-only header forwarding are all well-designed.

One bug that should be fixed before merge, plus several non-blocking observations.


Bug: App Router worker entry doesn't pass imageConfig to handleImageOptimization

The generated App Router worker entry in generateAppRouterWorkerEntry() (deploy.ts, line 454-460) calls handleImageOptimization(request, handlers, allowedWidths) with only 3 arguments. The 4th parameter (imageConfig) is missing. Compare with:

  • Pages Router worker entry (line 563): passes all 4 args including imageConfig
  • App Router prod-server (line 612-620): passes imageConfig
  • Pages Router prod-server (line 781-788): passes pagesImageConfig

The generated App Router worker entry also doesn't construct an imageConfig variable from vinextConfig (the Pages Router entry does this at lines 514-530). Additionally, line 452 hardcodes "/_vinext/image" while the Pages Router entry uses imageConfig?.path ?? "/_vinext/image" (line 555).

Impact: All image config settings (remote/local patterns, SVG policy, CSP, qualities validation, cache TTL, dangerouslyAllowLocalIP, custom path) are silently ignored for App Router Cloudflare Worker deployments.


Non-blocking observations

1. minimumCacheTTL default diverges 240x from Next.jsimageConfigDefault.minimumCacheTTL is 14_400 (4 hours) vs Next.js 16's 60 (1 minute). If this is intentional for Cloudflare Workers, add a comment. If not, it should match upstream.

2. findClosestQuality has zero test coverage — TRACKING.md claims 8 tests pass, but no test file contains any reference to findClosestQuality. The tracking entry is phantom. The function is on the critical path for URL generation.

3. OnLoadingComplete not re-exported from "next/image" module — Exported from image.tsx:41 but missing from next-shims.d.ts lines 133-141. Import will fail in TypeScript.

4. isBlockedIpv4 doesn't block 0.0.0.0/8 — The explicit 0.0.0.0 string check catches the exact address, but isBlockedIpv4 doesn't check a === 0. Addresses like 0.1.2.3 (which may resolve to localhost on some systems) bypass the check.

5. onLoadingComplete callback is never invoked — It's destructured and warned about in dev, but the callback itself is silently dropped. In Next.js, it's still wired to fire on image load for backward compat.

6. blurWidth/blurHeight declared but unusedStaticImageData declares these fields but normalizeProps never reads them. Minor feature gap vs Next.js.

loaderFile: "",
domains: [],
disableStaticImages: false,
minimumCacheTTL: 14_400,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional? Next.js 16 defaults minimumCacheTTL to 60 (seconds). This is 14_400 (4 hours) — a 240x difference. If this is a deliberate optimization for Cloudflare Workers (where re-optimization is expensive), it should have a comment explaining the deviation. If not, it should match upstream.

Suggested change
minimumCacheTTL: 14_400,
minimumCacheTTL: 14_400, // 4 hours — intentionally higher than Next.js default (60s) for Workers

| 37-38 | placeholder data URL (Next.js 16): local + remote | PASS | |
| 39-40 | StaticImport / StaticRequire support | PASS | |
| 41-42 | type exports (ImageLoaderProps, ImageProps) | PASS | |
| 43-49 | findClosestQuality (7 tests + 1 equidistant) | PASS | |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phantom test entry. This claims findClosestQuality has 7 tests + 1 equidistant, all PASS. However, no *.test.ts file in the repository contains any reference to findClosestQuality. These tests don't exist. Either add the tests or remove this tracking entry.

StaticImageData,
StaticImport,
StaticRequire,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: OnLoadingComplete is exported from image.tsx (line 41) but missing from this re-export list. Code doing import type { OnLoadingComplete } from "next/image" will get a TypeScript error.

Suggested change
};
export type {
ImageProps,
ImageLoader,
ImageLoaderProps,
PlaceholderValue,
StaticImageData,
StaticImport,
StaticRequire,
OnLoadingComplete,
};

(Also needs OnLoadingComplete added to the import on line 123.)

a === 127 ||
(a === 169 && b === 254) ||
(a === 172 && b >= 16 && b <= 31) ||
(a === 192 && b === 168)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low severity: isBlockedIpv4 doesn't check a === 0, so addresses in the 0.0.0.0/8 range (e.g., 0.1.2.3) bypass this. The explicit lower === "0.0.0.0" string check at line 294 only catches the exact address. On some systems, 0.x.x.x addresses resolve to localhost.

Suggested change
(a === 192 && b === 168)
return (
a === 0 ||
a === 10 ||
a === 127 ||
(a === 169 && b === 254) ||
(a === 172 && b >= 16 && b <= 31) ||
(a === 192 && b === 168)
);

With this change, the explicit 0.0.0.0 string check at line 294 becomes redundant and can be removed.

if (objectPosition) {
console.warn(`Image with src "${src}" has legacy prop "objectPosition".`);
}
if (onLoadingComplete) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: onLoadingComplete is warned about here but the callback is never actually invoked — it's destructured at line 434 and silently dropped. In Next.js, deprecated onLoadingComplete is still wired to fire when the image loads. Users migrating from older Next.js code will silently lose this behavior.

Consider wiring it through onLoad for backward compat:

onLoad: (e) => {
  onLoad?.(e);
  if (onLoadingComplete) onLoadingComplete(e.currentTarget);
},

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 9, 2026

Review submitted successfully. Here's a summary of the feedback:

Bug (should fix before merge)

  • App Router worker entry missing imageConfiggenerateAppRouterWorkerEntry() in deploy.ts calls handleImageOptimization with only 3 args, omitting the imageConfig parameter that the Pages Router entry correctly passes. This means all image security/config settings are silently ignored for App Router Cloudflare Worker deployments.

Non-blocking observations (with inline comments)

  1. minimumCacheTTL default is 14,400s vs Next.js's 60s — 240x higher, needs documentation if intentional
  2. findClosestQuality has zero test coverage — TRACKING.md claims 8 tests pass but they don't exist
  3. OnLoadingComplete type missing from next/image module re-exports — TypeScript import will fail
  4. isBlockedIpv4 doesn't block 0.0.0.0/8 range — suggestion to add a === 0 check
  5. onLoadingComplete callback is never invoked — warned in dev but silently dropped
  6. blurWidth/blurHeight declared but unused in normalizeProps

github run

- Fix App Router worker entry missing imageConfig: add virtual:vinext-image-config
  module, pass imageConfig as 4th arg to handleImageOptimization, use
  imageConfig.path instead of hardcoded /_vinext/image
- Block 0.0.0.0/8 range in isBlockedIpv4, remove redundant string check
- Wire deprecated onLoadingComplete through onLoad callback
- Export OnLoadingComplete type from next/image module declaration
- Add findClosestQuality tests (ported from Next.js test suite)
- Document minimumCacheTTL matches Next.js 16 upstream default
- Document blurWidth/blurHeight as known gap in StaticImageData
@Divkix
Copy link
Contributor Author

Divkix commented Mar 10, 2026

I think this should be good, worked through issues

Divkix added 6 commits March 9, 2026 21:45
Cover SSRF prevention (IPv4/IPv6 blocking, AWS metadata endpoint),
redirect/body-size limits, blurDataURL CSS injection sanitization,
config validation edge cases, allowedQualities enforcement,
local pattern server enforcement, loader:custom error path,
vw-based srcSet filtering, and matchLocalPattern/hasLocalMatch.

Exports 5 internal security functions from image-optimization.ts
for direct unit testing. Adds 82 new tests across 4 files.
Add 40+ tests covering previously untested image optimization functions:

- isSafeImageContentType: all safe types, blocked types, SVG with
  dangerouslyAllowSVG, content-type with params, null/empty
- Security headers via handleImageOptimization: X-Content-Type-Options,
  Content-Disposition (attachment/inline), CSP header, custom CSP, filename
- SVG end-to-end blocking: 400 when disallowed, 200 when allowed, CSP on SVG
- negotiateImageFormat: Accept header parsing, format priority, fallback
- Cache-Control: default TTL (14400), custom minimumCacheTTL, format
- Content-Disposition filename: extension mapping, basename extraction
- AVIF quality adjustment: -20 reduction, clamp to 1, no reduction for non-AVIF

Also includes oxfmt formatting fixes for image-config and next-config tests.
…mization, and component

- Security: SSRF redirect chains, protocol-relative blocking, backslash
  normalization, cookie/auth isolation, 302/307/308 redirects, 403/404
  upstream codes, Vary: Accept, dangerous URL schemes
- Optimization: parseImageParams edge cases, null body 404, text/html
  rejection, transformImage fallback, SVG pass-through verification,
  Content-Disposition filename edge cases
- Component: 20 dev-mode validation errors, fill mode rendering, legacy
  prop warnings, reverse dimension inference, SVG auto-unoptimized
- Document 6 feature gaps as TODOs (recursive URL guard, URL length
  limit, upstream timeout, animated detection, magic bytes, ETag)
…gaps

Implement 6 features from the Next.js 16 image optimizer that were missing:
- Content-type detection via magic bytes (16 formats) instead of trusting headers
- Animated image detection (GIF/WebP/APNG) to skip optimization
- Cache-Control parsing (s-maxage/max-age) from upstream responses
- Recursive URL guard preventing /_vinext/image optimization loops
- URL length limit (3072 chars) for DoS prevention
- Upstream fetch timeout (7s AbortSignal, returns 504)

Add 62 new tests across 5 files bringing total image tests to 342.
…generation

Port the SVG blur placeholder generation from Next.js 16 to vinext.
This function creates URL-encoded SVG strings with Gaussian blur filters
for use as image placeholders, supporting blurWidth/blurHeight scaling
and objectFit-aware preserveAspectRatio.
- Integrate getImageBlurSvg() for proper SVG blur placeholders with
  Gaussian blur filter, replacing raw blurDataURL usage
- Extract and pass blurWidth/blurHeight from static imports to SVG
  generator for optimal blur dimensions
- Add INVALID_BACKGROUND_SIZE_VALUES handling: map objectFit values
  like 'fill' → '100% 100%', 'none'/'scale-down' → 'cover'
- Add showAltText state param: skip color:transparent when true
- Add blurComplete state param: skip placeholder when true
- Add layout→style/sizes mapping for 'intrinsic' and 'responsive'
- Add ref prop warning and small image (<40x40) placeholder warning
- Export OnLoad, PlaceholderStyle types
- Remove objectFit/objectPosition from non-fill base style
- Export normalizeProps for direct testing
@Divkix
Copy link
Contributor Author

Divkix commented Mar 10, 2026

Closing for now — the merge of parallel worktree changes introduced too much risk of subtle bugs. Will revisit this alignment work in a future PR with a cleaner approach.

@Divkix Divkix closed this Mar 10, 2026
@Divkix Divkix deleted the fix/align-image-api-nextjs-16 branch March 10, 2026 07:55
@james-elicx
Copy link
Collaborator

No worries, thanks for your time so far

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Align next/image API surface with Next.js 16.0.0

3 participants