diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bab007b..707d7c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: node-version: [22.x] - astro-version: ["3", "4", "5", "6"] + astro-version: ["3", "4", "5", "6", "7"] include: - astro-version: "3" node-adapter: "@astrojs/node@^6" @@ -23,12 +23,14 @@ jobs: node-adapter: "@astrojs/node@^9" - astro-version: "6" node-adapter: "@astrojs/node@^10" + - astro-version: "7" + node-adapter: "@astrojs/node@^11" steps: - uses: actions/checkout@v4 - name: Install pnpm - uses: pnpm/action-setup@v4 + run: npm install -g pnpm@9.5.0 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7314db0..9d7b404 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v4 - name: Install pnpm - uses: pnpm/action-setup@v4 + run: npm install -g pnpm@9.5.0 - uses: actions/setup-node@v4 with: diff --git a/README.md b/README.md index 8a862f7..4b396da 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ The service is **host-aware**. For each image, it inspects the `src` and routes This means you can drop the integration into an existing Astro project without breaking any of its existing local-asset usage. Local assets keep working via sharp; ImageKit URLs get the full ImageKit treatment. +Both paths work in every output mode — `static`, `server`, and prerendered routes inside a server build. At build time the sharp path emits optimized files into `dist/_astro/`, while the ImageKit path always emits CDN URLs and never writes files. + ## Installation ```bash diff --git a/imagekit-astro/CHANGELOG.md b/imagekit-astro/CHANGELOG.md index a2d1c23..ca9018f 100644 --- a/imagekit-astro/CHANGELOG.md +++ b/imagekit-astro/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to `@imagekit/astro` will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.2] - 2026-09-09 + +### Fixed + +- **Static builds and prerendered routes no longer fail for ImageKit images.** Because the image service delegates local assets to sharp (and therefore exposes sharp's `transform`), Astro classifies it as a *local* service. During `astro build` with `output: 'static'` — or for any prerendered route in `output: 'server'` — Astro handed every image to its static image pipeline, which rewrote the ImageKit URL to a `/_astro/` file and then tried to read the source from disk. Bare ImageKit paths (`src="/photo.jpg"`) crashed the build with `ENOENT: no such file or directory, open '/photo.jpg'`, and absolute ImageKit URLs were silently fetched and re-encoded by sharp, dropping every transformation. The service now keeps ImageKit-eligible images out of that pipeline so they resolve to CDN URLs in every output mode, while local imports and allow-listed third-party hosts still go through sharp. + +### Changed + +- **Astro 7 is now part of the test matrix.** End-to-end tests run against Astro 3, 4, 5, 6, and 7. +- **End-to-end coverage for prerendered routes.** The test app now includes a prerendered page in its server-output build, so the static image pipeline bypass is exercised on every Astro version in CI. + ## [1.0.1] - 2026-05-05 ### Fixed @@ -38,3 +49,4 @@ First stable release of the official ImageKit SDK for Astro. Drop-in components [1.0.0]: https://github.com/imagekit-developer/imagekit-astro/releases/tag/v1.0.0 [1.0.1]: https://github.com/imagekit-developer/imagekit-astro/releases/tag/v1.0.1 +[1.0.2]: https://github.com/imagekit-developer/imagekit-astro/releases/tag/v1.0.2 diff --git a/imagekit-astro/package.json b/imagekit-astro/package.json index dc9bb14..90b0565 100644 --- a/imagekit-astro/package.json +++ b/imagekit-astro/package.json @@ -1,6 +1,6 @@ { "name": "@imagekit/astro", - "version": "1.0.1", + "version": "1.0.2", "type": "module", "license": "MIT", "description": "Astro SDK for ImageKit.io - optimized image & video delivery with transformations", diff --git a/imagekit-astro/src/services/imagekit-service.ts b/imagekit-astro/src/services/imagekit-service.ts index e4e2e42..f0a243c 100644 --- a/imagekit-astro/src/services/imagekit-service.ts +++ b/imagekit-astro/src/services/imagekit-service.ts @@ -195,6 +195,76 @@ function getIKHosts(imageConfig: AstroConfig['image']): string[] { return Array.from(new Set([...hosts, 'ik.imagekit.io'].map((h) => h.toLowerCase()))); } +/** + * Builds the final ImageKit URL for an IK-eligible transform. + */ +function buildIKUrl(opts: IKImageTransform, imageConfig: AstroConfig['image']): string { + const src = typeof opts.src === 'string' ? opts.src : opts.src.src; + const { urlEndpoint, transformationPosition } = resolveConfig(opts, imageConfig); + + return buildSrc({ + src, + urlEndpoint, + transformation: buildIKTransformations(opts), + queryParameters: opts.queryParameters, + transformationPosition, + }); +} + +/** + * URLs we generated for IK-eligible transforms, keyed by the exact transform + * object Astro handed to `getURL`. Astro passes that same object to + * `addStaticImage`, which lets the wrapper below short-circuit it. + */ +const ikUrlByTransform = new WeakMap(); + +/** Last image config seen by the service; used as a fallback by the wrapper. */ +let lastImageConfig: AstroConfig['image'] | undefined; + +const WRAPPED_FLAG = '__imagekitAstroWrapped'; + +/** + * Keep IK URLs out of Astro's static image pipeline. + * + * Because this service exposes sharp's `transform`, Astro classifies it as a + * *local* service. During `astro build` (static output, or any prerendered + * route in server output) Astro's `getImage()` therefore hands every result + * to `globalThis.astroAsset.addStaticImage`, which replaces the URL with a + * `/_astro/.` path and later tries to read the source from disk + * (bare IK paths → ENOENT) or fetch it and re-encode it with sharp (absolute + * IK URLs → transformations silently dropped). + * + * Astro only skips that step when the service returned the src unchanged, + * which is never the case for IK URLs carrying transformations. So we wrap + * `addStaticImage` and return our CDN URL for the transforms we own, letting + * everything else (local assets, allow-listed third-party hosts) flow through + * to sharp untouched. `addStaticImage` only exists at build time, so this is + * a no-op in dev and in on-demand SSR rendering. + */ +function bypassStaticImagePipeline(): void { + const astroAsset = (globalThis as any).astroAsset; + const original = astroAsset?.addStaticImage; + if (typeof original !== 'function' || original[WRAPPED_FLAG]) return; + + const wrapped = function (this: unknown, options: IKImageTransform, ...rest: unknown[]) { + const known = ikUrlByTransform.get(options); + if (known !== undefined) return known; + + // Fallback in case Astro passes a transform object we haven't seen. + if (lastImageConfig) { + const src = typeof options.src === 'string' ? options.src : options.src?.src; + if (typeof src === 'string' && isImageKitSrc(src, getIKHosts(lastImageConfig))) { + return buildIKUrl(options, lastImageConfig); + } + } + + return original.call(this, options, ...rest); + } as ((...args: unknown[]) => unknown) & Record; + wrapped[WRAPPED_FLAG] = true; + + astroAsset.addStaticImage = wrapped; +} + const service: LocalImageService = { validateOptions(options: ImageTransform, imageConfig: AstroConfig['image']) { const src = typeof options.src === 'string' ? options.src : options.src.src; @@ -217,15 +287,15 @@ const service: LocalImageService = { return sharpService.getURL!(options, imageConfig); } - const { urlEndpoint, transformationPosition } = resolveConfig(opts, imageConfig); + const url = buildIKUrl(opts, imageConfig); - return buildSrc({ - src, - urlEndpoint, - transformation: buildIKTransformations(opts), - queryParameters: opts.queryParameters, - transformationPosition, - }); + // Remember this transform so the build-time static image pipeline + // returns our CDN URL instead of trying to process the file with sharp. + ikUrlByTransform.set(opts, url); + lastImageConfig = imageConfig; + bypassStaticImagePipeline(); + + return url; }, getHTMLAttributes(options: ImageTransform, imageConfig: AstroConfig['image']) { @@ -320,11 +390,11 @@ const service: LocalImageService = { // --- Local-service hooks (delegated to sharp) --- // // We register as a `LocalImageService` so Astro's `/_image` endpoint - // accepts requests for non-IK srcs (local assets, foreign hosts in - // `image.domains`/`remotePatterns`). These hooks only fire when our - // `getURL` returned a `/_image?...` URL — which only happens for - // sharp-delegated srcs. IK URLs go straight to `ik.imagekit.io` and - // never hit `/_image`, so this is purely additive. + // (SSR) and static image generation (build) handle non-IK srcs: local + // assets and foreign hosts in `image.domains`/`remotePatterns`. These + // hooks only fire for sharp-delegated srcs. IK URLs go straight to the + // CDN — `bypassStaticImagePipeline()` keeps them out of Astro's build-time + // pipeline, and they never hit `/_image` at runtime. parseURL: sharpService.parseURL, transform: sharpService.transform, propertiesToHash: sharpService.propertiesToHash, diff --git a/scripts/test-all-versions.sh b/scripts/test-all-versions.sh index 703ee59..a29eb10 100755 --- a/scripts/test-all-versions.sh +++ b/scripts/test-all-versions.sh @@ -33,7 +33,7 @@ while [[ $# -gt 0 ]]; do ;; *) echo "Unknown option: $1" - echo "Usage: $0 [--update] [--versions 3 4 5 6]" + echo "Usage: $0 [--update] [--versions 3 4 5 6 7]" exit 1 ;; esac @@ -41,7 +41,7 @@ done # Default: all supported versions if [[ ${#VERSIONS[@]} -eq 0 ]]; then - VERSIONS=("3" "4" "5" "6") + VERSIONS=("3" "4" "5" "6" "7") fi # --- Adapter lookup --- @@ -52,6 +52,7 @@ get_adapter() { 4) echo "@astrojs/node@^8" ;; 5) echo "@astrojs/node@^9" ;; 6) echo "@astrojs/node@^10" ;; + 7) echo "@astrojs/node@^11" ;; *) echo "@astrojs/node@latest" ;; esac } @@ -85,7 +86,7 @@ trap restore_deps EXIT # --- Install Playwright browsers (once) --- echo "==> Installing Playwright browsers..." -(cd "$TEST_APP_DIR" && pnpm exec playwright install --with-deps) +(cd "$TEST_APP_DIR" && pnpm exec playwright install --with-deps chromium) # --- Run for each version --- FAILED_VERSIONS=() diff --git a/test-app/README.md b/test-app/README.md index 810134d..754148e 100644 --- a/test-app/README.md +++ b/test-app/README.md @@ -7,6 +7,7 @@ This is a demo Astro application for testing the `@imagekit/astro` package. - `/` - Home page with navigation links - `/videos` - Test cases for the `Video` component - `/images` - Test cases for the `Image` component +- `/prerendered` - Prerendered route (`export const prerender = true`) inside the server-output app; verifies ImageKit srcs stay on the CDN and local imports go through sharp at build time ## Running the App diff --git a/test-app/e2e/__snapshot__/astro-3/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt b/test-app/e2e/__snapshot__/astro-3/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt new file mode 100644 index 0000000..f77b8a2 --- /dev/null +++ b/test-app/e2e/__snapshot__/astro-3/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt @@ -0,0 +1 @@ +

Prerendered Page Tests

This page is prerendered at build time (export const prerender = true) in a server-output app. ImageKit srcs must resolve to CDN URLs; local imports must be processed by sharp.

Bare ImageKit path

Bare ImageKit path

Absolute ImageKit URL with transformation

Absolute ImageKit URL

additionalEndpoints host

Additional endpoint host

Picture with multiple formats

ImageKit picture

Local imported asset (sharp)

Local hero image processed by sharp
\ No newline at end of file diff --git a/test-app/e2e/__snapshot__/astro-4/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt b/test-app/e2e/__snapshot__/astro-4/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt new file mode 100644 index 0000000..233529c --- /dev/null +++ b/test-app/e2e/__snapshot__/astro-4/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt @@ -0,0 +1 @@ +

Prerendered Page Tests

This page is prerendered at build time (export const prerender = true) in a server-output app. ImageKit srcs must resolve to CDN URLs; local imports must be processed by sharp.

Bare ImageKit path

Bare ImageKit path

Absolute ImageKit URL with transformation

Absolute ImageKit URL

additionalEndpoints host

Additional endpoint host

Picture with multiple formats

ImageKit picture

Local imported asset (sharp)

Local hero image processed by sharp
\ No newline at end of file diff --git a/test-app/e2e/__snapshot__/astro-5/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt b/test-app/e2e/__snapshot__/astro-5/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt new file mode 100644 index 0000000..be27e88 --- /dev/null +++ b/test-app/e2e/__snapshot__/astro-5/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt @@ -0,0 +1 @@ +

Prerendered Page Tests

This page is prerendered at build time (export const prerender = true) in a server-output app. ImageKit srcs must resolve to CDN URLs; local imports must be processed by sharp.

Bare ImageKit path

Bare ImageKit path

Absolute ImageKit URL with transformation

Absolute ImageKit URL

additionalEndpoints host

Additional endpoint host

Picture with multiple formats

ImageKit picture

Local imported asset (sharp)

Local hero image processed by sharp
\ No newline at end of file diff --git a/test-app/e2e/__snapshot__/astro-6/images.spec.ts/Image-page-renders-correctly-1.txt b/test-app/e2e/__snapshot__/astro-6/images.spec.ts/Image-page-renders-correctly-1.txt index 0800821..9e543dd 100644 --- a/test-app/e2e/__snapshot__/astro-6/images.spec.ts/Image-page-renders-correctly-1.txt +++ b/test-app/e2e/__snapshot__/astro-6/images.spec.ts/Image-page-renders-correctly-1.txt @@ -1 +1 @@ -

Image Component Tests

Image uses Astro's built-in Image component for optimized image delivery with ImageKit transformations. The urlEndpoint is configured globally in astro.config.mjs; cases below only pass it explicitly when overriding it.

Basic image (urlEndpoint from integration config)

Basic image

urlEndpoint prop override

Image with urlEndpoint override

Leading slash in src

Image with leading slash

Absolute URL src (urlEndpoint ignored, transformations still applied)

Absolute URL src

Transformation prop

With transformation

queryParameters prop

Image with queryParameters

Responsive image with sizes

Responsive image with sizes

Responsive image with fixed sizes (300px)

Responsive image with fixed sizes

Custom class

Image with custom class

loading="eager"

Image with loading eager

transformationPosition="path"

Image with path transformation

transformationPosition="path" + custom transformations

Image with path transformation + custom transformations

Data attributes

Image with data attributes

Multiple chained transformations

Image with multiple chained transformations

Astro Image Component Props

The following tests verify that Astro's built-in Image component props work correctly with Image.

format="avif"

Image with AVIF format

quality="high"

Image with high quality preset

quality=50

Image with numeric quality

decoding="sync"

Image with sync decoding

fetchpriority="high"

Image with high fetch priority

densities=[1.5, 2] (requires layout="none" to override config default)

Image with density srcset

widths=[240, 540, 720]

Image with specific widths srcset

layout="constrained"

Image with constrained layout

layout="full-width"

Image with full-width layout

layout="fixed"

Image with fixed layout

layout="none" (responsive off)

Image with responsive off

priority (eager loading shorthand)

Image with priority flag

Inline style

Image with inline style

id attribute

Image with id

Quality = 0 (minimum)

quality 0

Quality = 100 (maximum)

quality 100

Format = webp

webp format

Format = avif

avif format

Format = svg

svg format

Multiple complex transformations

complex transformations

transformationPosition="path" + transformations

transformation path position

transformationPosition="query" + queryParameters

transformation query position

Quality preset = "low"

quality low preset

Quality preset = "mid"

quality mid preset

Quality preset = "max"

quality max preset

Sharp delegation (non-ImageKit srcs)

The following cases use srcs that are not ImageKit URLs. The integration delegates them to Astro's default sharp service, so they're served via Astro's /_image endpoint instead of being rewritten to ik.imagekit.io.

Local imported asset (sharp)

Local hero image processed by sharp

Allow-listed external URL (sharp)

External URL on an allow-listed host, processed by sharp
\ No newline at end of file +

Image Component Tests

Image uses Astro's built-in Image component for optimized image delivery with ImageKit transformations. The urlEndpoint is configured globally in astro.config.mjs; cases below only pass it explicitly when overriding it.

Basic image (urlEndpoint from integration config)

Basic image

urlEndpoint prop override

Image with urlEndpoint override

Leading slash in src

Image with leading slash

Absolute URL src (urlEndpoint ignored, transformations still applied)

Absolute URL src

Transformation prop

With transformation

queryParameters prop

Image with queryParameters

Responsive image with sizes

Responsive image with sizes

Responsive image with fixed sizes (300px)

Responsive image with fixed sizes

Custom class

Image with custom class

loading="eager"

Image with loading eager

transformationPosition="path"

Image with path transformation

transformationPosition="path" + custom transformations

Image with path transformation + custom transformations

Data attributes

Image with data attributes

Multiple chained transformations

Image with multiple chained transformations

Astro Image Component Props

The following tests verify that Astro's built-in Image component props work correctly with Image.

format="avif"

Image with AVIF format

quality="high"

Image with high quality preset

quality=50

Image with numeric quality

decoding="sync"

Image with sync decoding

fetchpriority="high"

Image with high fetch priority

densities=[1.5, 2] (requires layout="none" to override config default)

Image with density srcset

widths=[240, 540, 720]

Image with specific widths srcset

layout="constrained"

Image with constrained layout

layout="full-width"

Image with full-width layout

layout="fixed"

Image with fixed layout

layout="none" (responsive off)

Image with responsive off

priority (eager loading shorthand)

Image with priority flag

Inline style

Image with inline style

id attribute

Image with id

Quality = 0 (minimum)

quality 0

Quality = 100 (maximum)

quality 100

Format = webp

webp format

Format = avif

avif format

Format = svg

svg format

Multiple complex transformations

complex transformations

transformationPosition="path" + transformations

transformation path position

transformationPosition="query" + queryParameters

transformation query position

Quality preset = "low"

quality low preset

Quality preset = "mid"

quality mid preset

Quality preset = "max"

quality max preset

Sharp delegation (non-ImageKit srcs)

The following cases use srcs that are not ImageKit URLs. The integration delegates them to Astro's default sharp service, so they're served via Astro's /_image endpoint instead of being rewritten to ik.imagekit.io.

Local imported asset (sharp)

Local hero image processed by sharp

Allow-listed external URL (sharp)

External URL on an allow-listed host, processed by sharp
\ No newline at end of file diff --git a/test-app/e2e/__snapshot__/astro-6/md.spec.ts/Markdown-page-renders-correctly-1.txt b/test-app/e2e/__snapshot__/astro-6/md.spec.ts/Markdown-page-renders-correctly-1.txt index fd83daa..b267f83 100644 --- a/test-app/e2e/__snapshot__/astro-6/md.spec.ts/Markdown-page-renders-correctly-1.txt +++ b/test-app/e2e/__snapshot__/astro-6/md.spec.ts/Markdown-page-renders-correctly-1.txt @@ -1,12 +1,12 @@

ImageKit URL (primary endpoint host)

-

Imagekit URL

+

Imagekit URL

Custom IK domain (additionalEndpoints host)

-

Another custom IK URL

+

Another custom IK URL

Local imported asset (delegated to sharp)

Local hero asset

Allow-listed external URL (delegated to sharp)

-

Allowed external URL

+

Allowed external URL

Non-allow-listed external URL (rendered as-is by Astro)

Not allowed external URL

\ No newline at end of file diff --git a/test-app/e2e/__snapshot__/astro-6/pictures.spec.ts/Picture-page-renders-correctly-1.txt b/test-app/e2e/__snapshot__/astro-6/pictures.spec.ts/Picture-page-renders-correctly-1.txt index 5c259e0..79f02ce 100644 --- a/test-app/e2e/__snapshot__/astro-6/pictures.spec.ts/Picture-page-renders-correctly-1.txt +++ b/test-app/e2e/__snapshot__/astro-6/pictures.spec.ts/Picture-page-renders-correctly-1.txt @@ -1 +1 @@ -

Picture Component Tests

Picture uses Astro's built-in Picture component for multi-format image delivery with ImageKit transformations. The urlEndpoint is configured globally in astro.config.mjs; cases below only pass it explicitly when overriding it.

Basic picture (urlEndpoint from integration config)

Basic picture

urlEndpoint prop override

Picture with urlEndpoint override

Leading slash in src

Picture with leading slash

Absolute URL src (urlEndpoint ignored, transformations still applied)

Absolute URL src

Transformation prop

With transformation

queryParameters prop

Picture with queryParameters

Responsive picture with sizes

Responsive picture with sizes

Custom class

Picture with custom class

loading="eager"

Picture with loading eager

transformationPosition="path"

Picture with path transformation

transformationPosition="path" + custom transformations

Picture with path transformation + custom transformations

Multiple chained transformations

Picture with multiple chained transformations

Astro Picture Component Props

The following tests verify that Astro's built-in Picture component props work correctly with ImageKit.

formats=['webp'] (single format)

Picture with webp only

formats=['avif', 'webp', 'jpeg']

Picture with multiple formats

fallbackFormat="png"

Picture with png fallback

pictureAttributes (class + data attribute on <picture>)

Picture with pictureAttributes

quality="high"

Picture with high quality preset

quality=50

Picture with numeric quality

decoding="sync"

Picture with sync decoding

fetchpriority="high"

Picture with high fetch priority

densities=[1.5, 2] (requires layout="none" to override config default)

Picture with density srcset

widths=[240, 540, 720]

Picture with specific widths srcset

layout="constrained"

Picture with constrained layout

layout="full-width"

Picture with full-width layout

layout="fixed"

Picture with fixed layout

layout="none" (responsive off)

Picture with responsive off

priority (eager loading shorthand)

Picture with priority flag

Inline style

Picture with inline style

id attribute

Picture with id

Sharp delegation (non-ImageKit srcs)

The following cases use srcs that are not ImageKit URLs. The integration delegates them to Astro's default sharp service.

Local imported asset (sharp)

Local hero image processed by sharp

Allow-listed external URL (sharp)

External URL on an allow-listed host, processed by sharp
\ No newline at end of file +

Picture Component Tests

Picture uses Astro's built-in Picture component for multi-format image delivery with ImageKit transformations. The urlEndpoint is configured globally in astro.config.mjs; cases below only pass it explicitly when overriding it.

Basic picture (urlEndpoint from integration config)

Basic picture

urlEndpoint prop override

Picture with urlEndpoint override

Leading slash in src

Picture with leading slash

Absolute URL src (urlEndpoint ignored, transformations still applied)

Absolute URL src

Transformation prop

With transformation

queryParameters prop

Picture with queryParameters

Responsive picture with sizes

Responsive picture with sizes

Custom class

Picture with custom class

loading="eager"

Picture with loading eager

transformationPosition="path"

Picture with path transformation

transformationPosition="path" + custom transformations

Picture with path transformation + custom transformations

Multiple chained transformations

Picture with multiple chained transformations

Astro Picture Component Props

The following tests verify that Astro's built-in Picture component props work correctly with ImageKit.

formats=['webp'] (single format)

Picture with webp only

formats=['avif', 'webp', 'jpeg']

Picture with multiple formats

fallbackFormat="png"

Picture with png fallback

pictureAttributes (class + data attribute on <picture>)

Picture with pictureAttributes

quality="high"

Picture with high quality preset

quality=50

Picture with numeric quality

decoding="sync"

Picture with sync decoding

fetchpriority="high"

Picture with high fetch priority

densities=[1.5, 2] (requires layout="none" to override config default)

Picture with density srcset

widths=[240, 540, 720]

Picture with specific widths srcset

layout="constrained"

Picture with constrained layout

layout="full-width"

Picture with full-width layout

layout="fixed"

Picture with fixed layout

layout="none" (responsive off)

Picture with responsive off

priority (eager loading shorthand)

Picture with priority flag

Inline style

Picture with inline style

id attribute

Picture with id

Sharp delegation (non-ImageKit srcs)

The following cases use srcs that are not ImageKit URLs. The integration delegates them to Astro's default sharp service.

Local imported asset (sharp)

Local hero image processed by sharp

Allow-listed external URL (sharp)

External URL on an allow-listed host, processed by sharp
\ No newline at end of file diff --git a/test-app/e2e/__snapshot__/astro-6/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt b/test-app/e2e/__snapshot__/astro-6/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt new file mode 100644 index 0000000..ccd0a45 --- /dev/null +++ b/test-app/e2e/__snapshot__/astro-6/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt @@ -0,0 +1 @@ +

Prerendered Page Tests

This page is prerendered at build time (export const prerender = true) in a server-output app. ImageKit srcs must resolve to CDN URLs; local imports must be processed by sharp.

Bare ImageKit path

Bare ImageKit path

Absolute ImageKit URL with transformation

Absolute ImageKit URL

additionalEndpoints host

Additional endpoint host

Picture with multiple formats

ImageKit picture

Local imported asset (sharp)

Local hero image processed by sharp
\ No newline at end of file diff --git a/test-app/e2e/__snapshot__/astro-7/images.spec.ts/Image-page-renders-correctly-1.txt b/test-app/e2e/__snapshot__/astro-7/images.spec.ts/Image-page-renders-correctly-1.txt new file mode 100644 index 0000000..89acf2a --- /dev/null +++ b/test-app/e2e/__snapshot__/astro-7/images.spec.ts/Image-page-renders-correctly-1.txt @@ -0,0 +1 @@ +

Image Component Tests

Image uses Astro's built-in Image component for optimized image delivery with ImageKit transformations. The urlEndpoint is configured globally in astro.config.mjs; cases below only pass it explicitly when overriding it.

Basic image (urlEndpoint from integration config)

Basic image

urlEndpoint prop override

Image with urlEndpoint override

Leading slash in src

Image with leading slash

Absolute URL src (urlEndpoint ignored, transformations still applied)

Absolute URL src

Transformation prop

With transformation

queryParameters prop

Image with queryParameters

Responsive image with sizes

Responsive image with sizes

Responsive image with fixed sizes (300px)

Responsive image with fixed sizes

Custom class

Image with custom class

loading="eager"

Image with loading eager

transformationPosition="path"

Image with path transformation

transformationPosition="path" + custom transformations

Image with path transformation + custom transformations

Data attributes

Image with data attributes

Multiple chained transformations

Image with multiple chained transformations

Astro Image Component Props

The following tests verify that Astro's built-in Image component props work correctly with Image.

format="avif"

Image with AVIF format

quality="high"

Image with high quality preset

quality=50

Image with numeric quality

decoding="sync"

Image with sync decoding

fetchpriority="high"

Image with high fetch priority

densities=[1.5, 2] (requires layout="none" to override config default)

Image with density srcset

widths=[240, 540, 720]

Image with specific widths srcset

layout="constrained"

Image with constrained layout

layout="full-width"

Image with full-width layout

layout="fixed"

Image with fixed layout

layout="none" (responsive off)

Image with responsive off

priority (eager loading shorthand)

Image with priority flag

Inline style

Image with inline style

id attribute

Image with id

Quality = 0 (minimum)

quality 0

Quality = 100 (maximum)

quality 100

Format = webp

webp format

Format = avif

avif format

Format = svg

svg format

Multiple complex transformations

complex transformations

transformationPosition="path" + transformations

transformation path position

transformationPosition="query" + queryParameters

transformation query position

Quality preset = "low"

quality low preset

Quality preset = "mid"

quality mid preset

Quality preset = "max"

quality max preset

Sharp delegation (non-ImageKit srcs)

The following cases use srcs that are not ImageKit URLs. The integration delegates them to Astro's default sharp service, so they're served via Astro's /_image endpoint instead of being rewritten to ik.imagekit.io.

Local imported asset (sharp)

Local hero image processed by sharp

Allow-listed external URL (sharp)

External URL on an allow-listed host, processed by sharp
\ No newline at end of file diff --git a/test-app/e2e/__snapshot__/astro-7/md.spec.ts/Markdown-page-renders-correctly-1.txt b/test-app/e2e/__snapshot__/astro-7/md.spec.ts/Markdown-page-renders-correctly-1.txt new file mode 100644 index 0000000..b267f83 --- /dev/null +++ b/test-app/e2e/__snapshot__/astro-7/md.spec.ts/Markdown-page-renders-correctly-1.txt @@ -0,0 +1,12 @@ +
+

ImageKit URL (primary endpoint host)

+

Imagekit URL

+

Custom IK domain (additionalEndpoints host)

+

Another custom IK URL

+

Local imported asset (delegated to sharp)

+

Local hero asset

+

Allow-listed external URL (delegated to sharp)

+

Allowed external URL

+

Non-allow-listed external URL (rendered as-is by Astro)

+

Not allowed external URL

+
\ No newline at end of file diff --git a/test-app/e2e/__snapshot__/astro-7/pictures.spec.ts/Picture-page-renders-correctly-1.txt b/test-app/e2e/__snapshot__/astro-7/pictures.spec.ts/Picture-page-renders-correctly-1.txt new file mode 100644 index 0000000..4966c46 --- /dev/null +++ b/test-app/e2e/__snapshot__/astro-7/pictures.spec.ts/Picture-page-renders-correctly-1.txt @@ -0,0 +1 @@ +

Picture Component Tests

Picture uses Astro's built-in Picture component for multi-format image delivery with ImageKit transformations. The urlEndpoint is configured globally in astro.config.mjs; cases below only pass it explicitly when overriding it.

Basic picture (urlEndpoint from integration config)

Basic picture

urlEndpoint prop override

Picture with urlEndpoint override

Leading slash in src

Picture with leading slash

Absolute URL src (urlEndpoint ignored, transformations still applied)

Absolute URL src

Transformation prop

With transformation

queryParameters prop

Picture with queryParameters

Responsive picture with sizes

Responsive picture with sizes

Custom class

Picture with custom class

loading="eager"

Picture with loading eager

transformationPosition="path"

Picture with path transformation

transformationPosition="path" + custom transformations

Picture with path transformation + custom transformations

Multiple chained transformations

Picture with multiple chained transformations

Astro Picture Component Props

The following tests verify that Astro's built-in Picture component props work correctly with ImageKit.

formats=['webp'] (single format)

Picture with webp only

formats=['avif', 'webp', 'jpeg']

Picture with multiple formats

fallbackFormat="png"

Picture with png fallback

pictureAttributes (class + data attribute on <picture>)

Picture with pictureAttributes

quality="high"

Picture with high quality preset

quality=50

Picture with numeric quality

decoding="sync"

Picture with sync decoding

fetchpriority="high"

Picture with high fetch priority

densities=[1.5, 2] (requires layout="none" to override config default)

Picture with density srcset

widths=[240, 540, 720]

Picture with specific widths srcset

layout="constrained"

Picture with constrained layout

layout="full-width"

Picture with full-width layout

layout="fixed"

Picture with fixed layout

layout="none" (responsive off)

Picture with responsive off

priority (eager loading shorthand)

Picture with priority flag

Inline style

Picture with inline style

id attribute

Picture with id

Sharp delegation (non-ImageKit srcs)

The following cases use srcs that are not ImageKit URLs. The integration delegates them to Astro's default sharp service.

Local imported asset (sharp)

Local hero image processed by sharp

Allow-listed external URL (sharp)

External URL on an allow-listed host, processed by sharp
\ No newline at end of file diff --git a/test-app/e2e/__snapshot__/astro-7/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt b/test-app/e2e/__snapshot__/astro-7/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt new file mode 100644 index 0000000..3378fe1 --- /dev/null +++ b/test-app/e2e/__snapshot__/astro-7/prerendered.spec.ts/Prerendered-page-renders-correctly-1.txt @@ -0,0 +1 @@ +

Prerendered Page Tests

This page is prerendered at build time (export const prerender = true) in a server-output app. ImageKit srcs must resolve to CDN URLs; local imports must be processed by sharp.

Bare ImageKit path

Bare ImageKit path

Absolute ImageKit URL with transformation

Absolute ImageKit URL

additionalEndpoints host

Additional endpoint host

Picture with multiple formats

ImageKit picture

Local imported asset (sharp)

Local hero image processed by sharp
\ No newline at end of file diff --git a/test-app/e2e/__snapshot__/astro-7/videos.spec.ts/Videos-page-renders-correctly-1.txt b/test-app/e2e/__snapshot__/astro-7/videos.spec.ts/Videos-page-renders-correctly-1.txt new file mode 100644 index 0000000..b2e7869 --- /dev/null +++ b/test-app/e2e/__snapshot__/astro-7/videos.spec.ts/Videos-page-renders-correctly-1.txt @@ -0,0 +1 @@ +

Video Component Tests

Basic Video with urlEndpoint prop

Video with leading slash in src

Video with transformations

Video with different urlEndpoint

Video with transformationPosition="path"

Video with custom class

Video with queryParameters

Video with chained transformations

Video without controls (autoplay, muted, loop)

transformationPosition="path" with transformations

transformationPosition="query" + queryParameters

All HTML attributes combined

\ No newline at end of file diff --git a/test-app/e2e/prerendered.spec.ts b/test-app/e2e/prerendered.spec.ts new file mode 100644 index 0000000..dab1379 --- /dev/null +++ b/test-app/e2e/prerendered.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from "@playwright/test"; + +/** + * Covers the build-time routing fix: on a prerendered route inside an + * `output: 'server'` app, Astro runs images through its static image + * pipeline. ImageKit srcs must stay on the CDN with their transformations, + * while local imports must still be emitted by sharp into `/_astro/`. + * + * Before the fix, `astro build` itself failed for this page (ENOENT for the + * bare ImageKit path), so the web server never started. + */ +test("Prerendered page routes ImageKit srcs to the CDN and local assets to sharp", async ({ page }) => { + await page.goto("/prerendered"); + + const ikCases = [ + "prerender-ik-bare-path", + "prerender-ik-absolute-url", + "prerender-ik-additional-endpoint", + ]; + for (const id of ikCases) { + const img = page.locator(`[data-test-id="${id}"] img`); + const src = await img.getAttribute("src"); + const srcset = await img.getAttribute("srcset"); + expect(src, id).toMatch(/^https:\/\/ik\.(imagekit\.io|imgkit\.net)\//); + expect(src, id).toContain("tr="); + expect(src, id).not.toContain("/_astro/"); + expect(src, id).not.toContain("/_image"); + if (srcset) { + expect(srcset, id).not.toContain("/_astro/"); + expect(srcset, id).not.toContain("/_image"); + } + } + + const absolute = page.locator('[data-test-id="prerender-ik-absolute-url"] img'); + expect(await absolute.getAttribute("src")).toContain("h-100,w-100"); + + const sources = page.locator('[data-test-id="prerender-ik-picture"] source'); + expect(await sources.count()).toBe(2); + for (const format of ["avif", "webp"]) { + const srcset = await page + .locator(`[data-test-id="prerender-ik-picture"] source[type="image/${format}"]`) + .getAttribute("srcset"); + expect(srcset).toContain("https://ik.imagekit.io/"); + expect(srcset).toContain(`f-${format}`); + } + + const local = page.locator('[data-test-id="prerender-sharp-local-import"] img'); + expect(await local.getAttribute("src")).toMatch(/^\/_astro\/hero\..*\.webp$/); + expect(await local.getAttribute("src")).not.toContain("ik.imagekit.io"); +}); + +test("Prerendered page renders correctly", async ({ page }) => { + await page.goto("/prerendered"); + + const container = page.locator(".container"); + const outputHtml = await container.evaluate((el) => el.outerHTML); + + expect(outputHtml).toMatchSnapshot(); +}); diff --git a/test-app/package.json b/test-app/package.json index 16ca023..f4eab76 100644 --- a/test-app/package.json +++ b/test-app/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "astro dev", "build": "astro build", - "start": "npm run build && astro preview", + "start": "npm run build && PORT=${PORT:-4321} node ./dist/server/entry.mjs", "preview": "astro preview", "test:e2e": "playwright test", "test:e2e-update": "playwright test --update-snapshots" diff --git a/test-app/src/pages/index.astro b/test-app/src/pages/index.astro index c9d6fd8..8048cf2 100644 --- a/test-app/src/pages/index.astro +++ b/test-app/src/pages/index.astro @@ -51,6 +51,7 @@
  • Video Component Tests
  • OG Image Tags (<OgImage>)
  • Markdown Image Test
  • +
  • Prerendered Page Tests

  • diff --git a/test-app/src/pages/prerendered.astro b/test-app/src/pages/prerendered.astro new file mode 100644 index 0000000..d6622a7 --- /dev/null +++ b/test-app/src/pages/prerendered.astro @@ -0,0 +1,82 @@ +--- +// Prerendered route inside `output: 'server'`. This exercises Astro's +// build-time static image pipeline (`addStaticImage`), which the image +// service must bypass for ImageKit srcs while still letting local assets +// through to sharp. Before the fix, building this page failed with +// ENOENT for the bare ImageKit path and re-encoded the absolute URL locally. +export const prerender = true; + +import { Image, Picture } from 'astro:assets'; +import heroImage from '../assets/hero.jpg'; +--- + + + + + + + Prerendered Page Tests + + +
    +

    Prerendered Page Tests

    +

    This page is prerendered at build time (export const prerender = true) in a server-output app. ImageKit srcs must resolve to CDN URLs; local imports must be processed by sharp.

    + + +
    +

    Bare ImageKit path

    + Bare ImageKit path +
    + + +
    +

    Absolute ImageKit URL with transformation

    + Absolute ImageKit URL +
    + + +
    +

    additionalEndpoints host

    + Additional endpoint host +
    + + +
    +

    Picture with multiple formats

    + +
    + + +
    +

    Local imported asset (sharp)

    + Local hero image processed by sharp +
    +
    + +