From d9f02a9387698056c6af50dddc057ef1616697c7 Mon Sep 17 00:00:00 2001 From: Vitor Date: Tue, 10 Nov 2026 09:48:54 -0300 Subject: [PATCH 1/2] fix(storefront): Keep CMS preview working when a new image isn't deployed yet Editing a banner with a newly uploaded image made the whole CMS preview fail with a gateway timeout: the image is not on the deployed images manifest yet, so `useSSRPicture` could not resolve its size and `getPicture` threw "`aspectRatio` must be provided for remote images" in the middle of the SSR stream, hanging the response until the Cloud Run request timeout. When the image size is unknown, `useSSRPicture` now renders the original file (plain ``, no optimized sources) and logs a warning instead of throwing. Images with known size keep the same optimized output. Co-Authored-By: Claude Opus 5.5 --- .../storefront/src/images/use-ssr-picture.ts | 31 +++++++---- packages/storefront/tests/ssr-picture.test.ts | 53 +++++++++++++++++++ 2 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 packages/storefront/tests/ssr-picture.test.ts diff --git a/packages/storefront/src/images/use-ssr-picture.ts b/packages/storefront/src/images/use-ssr-picture.ts index 5aebe6dfc..fe207e475 100644 --- a/packages/storefront/src/images/use-ssr-picture.ts +++ b/packages/storefront/src/images/use-ssr-picture.ts @@ -209,16 +209,27 @@ const useSSRPicture = async (params: UsePictureParams) => { sizes = `${(widths[0] / 2)}px`; } - const { image, sources } = await getPicture({ - src, - widths, - formats, - aspectRatio, - fit, - background, - position, - alt, - }); + let image: GetPictureResult['image']; + let sources: GetPictureResult['sources']; + if (aspectRatio) { + ({ image, sources } = await getPicture({ + src, + widths, + formats, + aspectRatio, + fit, + background, + position, + alt, + })); + } else { + // Unknown image size, e.g. a new CMS upload not deployed yet (CMS preview). + // `getPicture` would throw in the middle of the SSR stream and hang the response, + // so render the original file instead + console.warn(`Could not get size of image ${src}, rendering original file`); + image = { src }; + sources = []; + } delete image.width; delete image.height; if (import.meta.env.PROD && assetsPrefix && image.src?.charAt(0) === '/') { diff --git a/packages/storefront/tests/ssr-picture.test.ts b/packages/storefront/tests/ssr-picture.test.ts new file mode 100644 index 000000000..4b60ab6fc --- /dev/null +++ b/packages/storefront/tests/ssr-picture.test.ts @@ -0,0 +1,53 @@ +import { + describe, + test, + expect, + vi, +} from 'vitest'; +import { useSSRPicture } from '~/images/use-ssr-picture'; + +const builtPicture = { + image: { src: '/_astro/banner_abc.jpg', width: 1024, height: 576 }, + sources: [ + { type: 'image/avif', srcset: '/_astro/banner_abc.avif 640w,/_astro/banner_def.avif 1024w' }, + { type: 'image/webp', srcset: '/_astro/banner_abc.webp 640w,/_astro/banner_def.webp 1024w' }, + ], +}; + +describe('SSR attributes', () => { + test('renders the original file when the image size is unknown', async () => { + // e.g. a new upload on CMS preview, not listed on the deployed images manifest + vi.spyOn(console, 'warn').mockImplementation(() => {}); + const getPicture = vi.fn(async () => { + throw new Error('`aspectRatio` must be provided for remote images'); + }); + const { sources, imgAttrs } = await useSSRPicture({ + src: '/img/uploads/new-banner.jpg', + alt: 'New banner', + widths: [640, 1024], + tryImageSize: async () => ({}), + getPicture, + }); + expect(getPicture).not.toHaveBeenCalled(); + expect(sources).toEqual([]); + expect(imgAttrs?.src).toBe('/img/uploads/new-banner.jpg'); + expect(imgAttrs?.alt).toBe('New banner'); + }); + + test('keeps optimized sources when the image size is known', async () => { + const getPicture = vi.fn(async () => structuredClone(builtPicture)); + const { sources, imgAttrs } = await useSSRPicture({ + src: '/img/uploads/banner.jpg', + alt: 'Banner', + widths: [640, 1024], + tryImageSize: async () => ({ width: 1600, height: 900 }), + getPicture, + }); + expect(getPicture).toHaveBeenCalledWith(expect.objectContaining({ + src: '/img/uploads/banner.jpg', + aspectRatio: 1600 / 900, + })); + expect(sources).toEqual(builtPicture.sources); + expect(imgAttrs?.src).toBeNull(); + }); +}); From 750a46dd86c39392eb95425b513981d5ac560b36 Mon Sep 17 00:00:00 2001 From: Vitor Date: Tue, 10 Nov 2026 12:51:55 -0300 Subject: [PATCH 2/2] fix(storefront): Keep original image URL valid when it is rendered without optimization When the image size is unknown, `Picture.astro` now renders the original file, but still appended the `&V=` version suffix that assumes an `/_image?...` URL, producing `/img/banner.jpg&V=123`. Use `?V=` when the URL has no query string. Also restore the `console.warn` spy between tests and assert the fallback warning is logged. Co-Authored-By: Claude Fable 5.2 --- packages/storefront/src/lib/components/Picture.astro | 7 +++++-- packages/storefront/tests/ssr-picture.test.ts | 8 +++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/storefront/src/lib/components/Picture.astro b/packages/storefront/src/lib/components/Picture.astro index 9543bcc82..c4967c4e0 100644 --- a/packages/storefront/src/lib/components/Picture.astro +++ b/packages/storefront/src/lib/components/Picture.astro @@ -83,8 +83,11 @@ const { }), assetsPrefix: Astro.locals.assetsPrefix, }); -if (imgAttrs?.src) { - imgAttrs.src += versionSuffix; +if (imgAttrs?.src && versionSuffix) { + // Original file fallback (unknown image size) has no query string yet + imgAttrs.src += imgAttrs.src.includes('?') + ? versionSuffix + : versionSuffix.replace('&', '?'); } --- diff --git a/packages/storefront/tests/ssr-picture.test.ts b/packages/storefront/tests/ssr-picture.test.ts index 4b60ab6fc..0157f1bba 100644 --- a/packages/storefront/tests/ssr-picture.test.ts +++ b/packages/storefront/tests/ssr-picture.test.ts @@ -3,6 +3,7 @@ import { test, expect, vi, + afterEach, } from 'vitest'; import { useSSRPicture } from '~/images/use-ssr-picture'; @@ -15,9 +16,13 @@ const builtPicture = { }; describe('SSR attributes', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + test('renders the original file when the image size is unknown', async () => { // e.g. a new upload on CMS preview, not listed on the deployed images manifest - vi.spyOn(console, 'warn').mockImplementation(() => {}); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); const getPicture = vi.fn(async () => { throw new Error('`aspectRatio` must be provided for remote images'); }); @@ -29,6 +34,7 @@ describe('SSR attributes', () => { getPicture, }); expect(getPicture).not.toHaveBeenCalled(); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('/img/uploads/new-banner.jpg')); expect(sources).toEqual([]); expect(imgAttrs?.src).toBe('/img/uploads/new-banner.jpg'); expect(imgAttrs?.alt).toBe('New banner');