Skip to content

07 -- Responsive Image Setup

Prompt Template — Savoy Signature Hotels
Use when: Implementing responsive image handling in a new or existing module or component.


Before using this prompt, read the following files so you have full project context:

docs/dev-frontend-guides/06_RESPONSIVE_IMAGES_PATTERN.md # Full responsive image pattern (ResponsiveImage component, sizing, focal points)
docs/PRD/13_Media_and_Image_Pipeline.md # Media pipeline specification (Cloudflare, Umbraco, formats)
packages/ui/src/ResponsiveImage/ResponsiveImage.tsx # The shared ResponsiveImage component
apps/web/src/lib/cloudflare-image-loader.ts # Cloudflare Images custom loader for Next.js
packages/ui/src/{MODULE_NAME}/{MODULE_NAME}.tsx # The module receiving images (if it already exists)
packages/ui/src/{MODULE_NAME}/{MODULE_NAME}.types.ts # Module types (if they exist)
packages/ui/src/{MODULE_NAME}/{MODULE_NAME}.mapper.ts # Module mapper (if it exists)

I need to implement responsive images in a module using the Savoy ResponsiveImage component and Cloudflare Images pipeline.
**Module:** {MODULE_NAME}
**Image context:** {IMAGE_CONTEXT}
(One of: hero, card, gallery, thumbnail, room-detail, image-text, or custom)
### Image Dimensions
| Viewport | Width | Height | Aspect Ratio |
|----------|--------|--------|--------------|
| Desktop | {DESKTOP_WIDTH}px | {DESKTOP_HEIGHT}px | {DESKTOP_RATIO} |
| Mobile | {MOBILE_WIDTH}px | {MOBILE_HEIGHT}px | {MOBILE_RATIO} |
### Loading Behavior
- **Above the fold (LCP candidate):** {IS_ABOVE_FOLD}
(If yes, the first visible image gets `priority={true}`. If no, images use lazy loading.)
- **Number of images in this module:** {IMAGE_COUNT}
(e.g., 1 hero image, 3 card images, N gallery images)
### Focal Point Requirements
- **Focal point needed:** {NEEDS_FOCAL_POINT}
(If yes, the Umbraco editor sets a focal point and the image must respect it via `object-position`.)
- **Focal point notes:** {FOCAL_POINT_NOTES}
(e.g., "Hotel facade -- focal point typically centered on building entrance" or "Portrait crop on mobile -- focal point on subject's face")
### Art Direction
- **Different crop/composition per viewport:** {NEEDS_ART_DIRECTION}
(If yes, desktop and mobile are genuinely different images, not just different sizes of the same image.)
- **Art direction notes:** {ART_DIRECTION_NOTES}
(e.g., "Desktop shows wide landscape; mobile shows a tighter vertical crop of the same scene" or "Desktop and mobile use completely different photos")
### Tasks
1. Define `ImageSource` and image-related props in the module's `.types.ts`.
2. Implement the image mapper in `.mapper.ts` to extract `imageDesktop`, `imageMobile`, `altText`, and `focalPoint` from the Umbraco API response.
3. Use the `ResponsiveImage` component from `@savoy/ui` in the module's TSX -- do not use raw `<img>` or `next/image` directly.
4. Set `priority={true}` only on the first visible image if it is above the fold.
5. Set the correct `sizes` attribute based on the image's layout width (not always `100vw`).
6. Implement the SCSS container with the correct aspect ratios for desktop and mobile, using `object-fit: cover`.
7. Verify in Storybook that images render at the correct aspect ratios on both viewports.
8. Confirm the image budget: desktop images <= {DESKTOP_BUDGET}KB, mobile images <= {MOBILE_BUDGET}KB.

  • ResponsiveImage component from @savoy/ui is used (not raw &lt;img&gt; or bare next/image)
  • Module types include imageDesktop: ImageSource, imageMobile: ImageSource, and altText: string
  • ImageSource interface includes url, width, height, and optional focalPoint: { top: number; left: number }
  • Mapper correctly extracts both desktop and mobile images plus focal points from Umbraco response
  • priority={true} set on the first visible image if module is above the fold; all other images lazy-loaded
  • sizes prop reflects actual layout width (e.g., (min-width: 1024px) 33vw, 100vw for a 3-column card grid)
  • SCSS container uses aspect-ratio for the correct ratios at desktop (@media (min-width: 1024px)) and mobile (default)
  • object-fit: cover applied; object-position set inline by ResponsiveImage from focal point data
  • Cloudflare loader processes images (check Network tab — URLs go through /cdn-cgi/image/)
  • Image file sizes within budget: desktop <= 500KB, mobile <= 200KB
  • alt text is meaningful for informative images or empty ("") for decorative images
  • Images render correctly at all breakpoints in Storybook
  • No layout shift (CLS) from images — dimensions are always specified

PitfallHow to Avoid
Using &lt;img&gt; or next/image directly instead of ResponsiveImageAlways import from @savoy/ui — the shared component handles the &lt;picture&gt; element, source switching at 1024px, and focal point positioning
Setting priority on all imagesOnly the LCP image (first visible, above-fold) gets priority={true}. Cards below the fold, gallery images, and secondary images must lazy-load
Hardcoding sizes="100vw" on non-full-width imagesCalculate the actual rendered width. A card in a 3-column grid at desktop is roughly 33vw, not 100vw. Oversized sizes causes the browser to download unnecessarily large images
Ignoring focal pointsIf the mapper does not pass focalPoint through, the image defaults to object-position: 50% 50% which may crop out the subject on tightly-framed images
Forgetting mobile aspect ratio in SCSSThe container must switch aspect ratios at the 1024px breakpoint. Without this, mobile images either letterbox or overflow
Not mapping both desktop and mobile imagesEvery image slot in Umbraco has two fields (desktop + mobile). The mapper must extract both. If one is missing, provide a sensible fallback (e.g., use the desktop image for both)
Exceeding image budgetCloudflare optimizes on delivery, but source images uploaded to Umbraco must still respect upload constraints (desktop 1MB max, mobile 500KB max). The delivered budget is 500KB desktop / 200KB mobile
Missing alt textAlt text is mandatory in the Umbraco content model. The mapper should fall back to an empty string, never undefined, so the alt attribute is always present in the HTML

Section titled “Setting up responsive images for M09 Featured Cards (card thumbnails with focal points)”

Context files read:

docs/dev-frontend-guides/06_RESPONSIVE_IMAGES_PATTERN.md
docs/PRD/13_Media_and_Image_Pipeline.md
packages/ui/src/ResponsiveImage/ResponsiveImage.tsx
packages/ui/src/FeaturedCards/FeaturedCards.types.ts
packages/ui/src/FeaturedCards/FeaturedCards.mapper.ts
packages/ui/src/FeaturedCards/FeaturedCards.tsx

Filled-in prompt:

I need to implement responsive images in a module using the Savoy ResponsiveImage component and Cloudflare Images pipeline.
**Module:** FeaturedCards (M09)
**Image context:** card
### Image Dimensions
| Viewport | Width | Height | Aspect Ratio |
|----------|--------|--------|--------------|
| Desktop | 960px | 540px | 16:9 |
| Mobile | 750px | 422px | 16:9 |
### Loading Behavior
- **Above the fold (LCP candidate):** No
(Featured cards appear mid-page, below the hero. All images should lazy-load.)
- **Number of images in this module:** 3
(Three cards, each with one image.)
### Focal Point Requirements
- **Focal point needed:** Yes
(Each card image features a hotel area -- pool, restaurant, spa. The editor sets focal points to keep the subject centered when the image is cropped to 16:9.)
- **Focal point notes:** Card images are often cropped from wider source photos. The focal point ensures the subject (e.g., pool center, restaurant entrance) stays visible in the 16:9 crop.
### Art Direction
- **Different crop/composition per viewport:** No
(Same image at both viewports, just resized. The 16:9 ratio is maintained on both desktop and mobile.)
- **Art direction notes:** N/A -- same composition, same aspect ratio.
### Tasks
1. Define `ImageSource` and card props in `FeaturedCards.types.ts`:
- Each card: `imageDesktop: ImageSource`, `imageMobile: ImageSource`, `altText: string`
2. Implement the mapper in `FeaturedCards.mapper.ts` to extract images and focal points from Umbraco.
3. Use `ResponsiveImage` in `FeaturedCards.tsx` for each card's image.
4. All 3 images lazy-load (`priority` is not set since the module is below the fold).
5. Set `sizes="(min-width: 1024px) 33vw, 100vw"` since cards are in a 3-column grid on desktop.
6. SCSS container: `aspect-ratio: 16 / 9` at all breakpoints (no change needed between desktop and mobile).
7. Verify in Storybook that all 3 cards show images at 16:9, focal points are respected, and images lazy-load.
8. Confirm budget: desktop card images <= 500KB, mobile <= 200KB.

Expected types output:

FeaturedCards.types.ts
export interface ImageSource {
url: string;
width: number;
height: number;
focalPoint?: { top: number; left: number };
}
export interface FeaturedCard {
imageDesktop: ImageSource;
imageMobile: ImageSource;
altText: string;
title: string;
description: string;
link: { url: string; label: string };
}
export interface FeaturedCardsProps {
heading: string;
cards: FeaturedCard[];
}

Expected SCSS output:

.featured-cards {
&__card-image {
position: relative;
aspect-ratio: 16 / 9;
overflow: hidden;
border-radius: var(--radius-md);
img {
width: 100%;
height: 100%;
object-fit: cover;
// object-position set inline by ResponsiveImage via focalPoint
}
}
}

Expected component usage:

import { ResponsiveImage } from '@savoy/ui';
{cards.map((card, index) => (
<div key={index} className="featured-cards__card-image">
<ResponsiveImage
desktop={card.imageDesktop}
mobile={card.imageMobile}
alt={card.altText}
sizes="(min-width: 1024px) 33vw, 100vw"
/>
</div>
))}