07 -- Responsive Image Setup
Prompt Template — Savoy Signature Hotels
Use when: Implementing responsive image handling in a new or existing module or component.
1. Context Loading
Section titled “1. Context Loading”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 componentapps/web/src/lib/cloudflare-image-loader.ts # Cloudflare Images custom loader for Next.jspackages/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)2. Prompt Template
Section titled “2. Prompt Template”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.3. Acceptance Criteria
Section titled “3. Acceptance Criteria”-
ResponsiveImagecomponent from@savoy/uiis used (not raw<img>or barenext/image) - Module types include
imageDesktop: ImageSource,imageMobile: ImageSource, andaltText: string -
ImageSourceinterface includesurl,width,height, and optionalfocalPoint: { 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 -
sizesprop reflects actual layout width (e.g.,(min-width: 1024px) 33vw, 100vwfor a 3-column card grid) - SCSS container uses
aspect-ratiofor the correct ratios at desktop (@media (min-width: 1024px)) and mobile (default) -
object-fit: coverapplied;object-positionset inline byResponsiveImagefrom 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
-
alttext 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
4. Common Pitfalls
Section titled “4. Common Pitfalls”| Pitfall | How to Avoid |
|---|---|
Using <img> or next/image directly instead of ResponsiveImage | Always import from @savoy/ui — the shared component handles the <picture> element, source switching at 1024px, and focal point positioning |
Setting priority on all images | Only 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 images | Calculate 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 points | If 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 SCSS | The container must switch aspect ratios at the 1024px breakpoint. Without this, mobile images either letterbox or overflow |
| Not mapping both desktop and mobile images | Every 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 budget | Cloudflare 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 text | Alt 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 |
5. Example
Section titled “5. Example”Setting up responsive images for M09 Featured Cards (card thumbnails with focal points)
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.mddocs/PRD/13_Media_and_Image_Pipeline.mdpackages/ui/src/ResponsiveImage/ResponsiveImage.tsxpackages/ui/src/FeaturedCards/FeaturedCards.types.tspackages/ui/src/FeaturedCards/FeaturedCards.mapper.tspackages/ui/src/FeaturedCards/FeaturedCards.tsxFilled-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:
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>))}