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/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 new file mode 100644 index 000000000..0157f1bba --- /dev/null +++ b/packages/storefront/tests/ssr-picture.test.ts @@ -0,0 +1,59 @@ +import { + describe, + test, + expect, + vi, + afterEach, +} 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', () => { + 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 + const warn = 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(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'); + }); + + 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(); + }); +});