Sanity Hotspot Implementation Documentation
Overview
This document describes the implementation of predictable image cropping using Sanity hotspots in the Feyenoord frontend. The solution ensures that images cropped and hotspotted in Sanity Studio are displayed consistently in the frontend by maintaining precise aspect ratios across different viewport sizes.
Problem Statement
Sanity's image hotspot functionality allows content editors to define which part of an image should remain visible when cropping occurs. However, unpredictable cropping can happen when:
- Only the width is passed to Sanity's
imageBuilderwithout specifying height - Sanity defaults to using the original image's width and height (aspect ratio) instead of the frontend's display width and height (aspect ratio)
- Large differences exist between the original image's aspect ratio and the displayed aspect ratio in the frontend
This leads to unexpected cropping behavior where the hotspot-defined area may not be visible as intended.
Solution Architecture
Fixed Aspect Ratios
To ensure predictable cropping and hotspot implementation, we implement fixed aspect ratios for all image components. Each component defines specific aspect ratios for different viewports:
- Mobile/Tablet
- Desktop
Dual Image Rendering
Components render both mobile and desktop versions of images simultaneously, showing/hiding them with CSS media queries. This approach:
- Prevents layout shift: Ensures images don't jump when a different aspect ratio is loaded on the server versus client
- Enables dual aspect ratios: Allows passing separate aspect ratios for mobile/tablet and desktop, ensuring we always know the correct height based on width, thus properly respecting Sanity hotspots and cropping
- Maintains consistent aspect ratios across viewports
- Ensures hotspot cropping matches frontend display
Dynamic Height Calculation
The getResponsiveImage function dynamically calculates image heights based on:
- Dynamic width provided by Next.js Image loader
- Specified aspect ratios
- Maintains image optimization through sizes prop with loader
Implementation Details
Core Functions
getResponsiveImage
Located in /apps/web/lib/sanity/image.ts, this function creates responsive images with fixed aspect ratios that properly respect Sanity hotspots and cropping:
export function getResponsiveImage({
image,
priority = false,
aspectRatio, // e.g., "16/9", "4/3", "2/1"
sizes, // Responsive sizes string
}: GetResponsiveImageProps): FieldImage | undefined;Key features:
- Uses Next.js Image loader for dynamic width handling
- Calculates height using
getImageHeightWithAspectRatio(width, aspectRatio) - Passes both width and height to Sanity's imageBuilder
- Generates optimized srcSet for different screen sizes
getImageHeightWithAspectRatio
Helper function in /apps/web/lib/sanity/image.helpers.ts that calculates height from width and aspect ratio:
export function getImageHeightWithAspectRatio(
width: number,
aspectRatio: AspectRatio, // e.g., "16/9"
): number {
const [aspectWidth, aspectHeight] = aspectRatio.split("/").map(Number);
return Math.round((width / aspectWidth) * aspectHeight);
}Aspect Ratio Types
Predefined aspect ratios available in /apps/web/lib/sanity/image.helpers.ts:
export type AspectRatio =
| "1/1" // Square
| "3/2" // Landscape
| "3/4" // Portrait
| "4/3" // Standard
| "9/16" // Vertical video
| "16/9" // Widescreen
| "1/2" // Half height
| "2/1"; // Double widthComponent Implementation Examples
CampaignHero Component (/apps/web/components/images/CampaignHeroImage.tsx)
const imageWithHotspot = useMemo(() => {
const BASE_IMAGE_CONFIG = {
fallbackImage: { src: "", alt: "" },
priority: true,
};
const mobileImage = getResponsiveImage({
image: imageMobile ?? image,
currentAspectRatio: ASPECT_RATIO.mobile,
sizes: "(max-width: 768px) 100vw, 768px",
...BASE_IMAGE_CONFIG,
});
const desktopImage = getResponsiveImage({
image,
currentAspectRatio: ASPECT_RATIO.tablet,
sizes: "(max-width: 1440px) 100vw, 1440px",
...BASE_IMAGE_CONFIG,
});
return {
mobileImage,
image: desktopImage,
};
}, [image, imageMobile]);Benefits
Predictable Cropping
- Hotspots defined in Sanity Studio translate directly to frontend display
- No unexpected cropping due to aspect ratio mismatches
- Consistent behavior across all devices and screen sizes
Performance Optimization
- Images remain optimized through Next.js Image component
- Proper srcSet generation for different viewport sizes
- Maintains lazy loading and priority loading capabilities
Maintainable Architecture
- Centralized aspect ratio definitions
- Consistent implementation pattern across components
- Type-safe aspect ratio handling
Usage Guidelines
For New Components
- Choose appropriate aspect ratios based on design requirements
- Use
getResponsiveImagefunction for image processing - Define separate mobile and desktop aspect ratios when needed
- Pass correct
sizesattribute for responsive behavior - Render both mobile and desktop images with CSS media query visibility
Media Query Implementation
Components should handle image visibility through CSS:
/* Mobile image visible by default */
.mobile {
display: block;
}
.desktop {
display: none;
}
/* Desktop breakpoint */
@media (min-width: 769px) {
.mobile {
display: none;
}
.desktop {
display: block;
}
}Technical Considerations
Image Optimization
- Next.js Image component handles lazy loading automatically
- srcSet generation includes retina display support
- Quality settings (75) balance file size and visual quality
Responsive Behavior
sizesattribute ensures correct image selection for different viewports- Dynamic width calculation maintains aspect ratio consistency
- Fallback to original aspect ratio only when no specific ratio is defined
Sanity Integration
- Works with existing Sanity hotspot functionality
- No changes required to content schema
- Compatible with all Sanity image field configurations
Future Enhancements
Potential improvements to consider:
- Dynamic aspect ratio detection for components with variable layouts
- Additional aspect ratio presets based on emerging design patterns