UNPKG

@ai-growth/nextjs

Version:

Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering

357 lines (356 loc) 11.9 kB
/** * @fileoverview Image Optimization Utilities for Next.js * * This module provides utilities for optimizing images with Next.js Image component, * including Sanity CDN integration, responsive sizing, and format optimization. */ import { buildSanityImageUrl } from './seo/image-processing'; // ============================================================================ // CONFIGURATION // ============================================================================ /** * Default image optimization configuration */ export const DEFAULT_IMAGE_CONFIG = { quality: 80, format: 'auto', lazyLoading: true, breakpoints: [ { name: 'mobile', minWidth: 0, imageWidth: 640 }, { name: 'tablet', minWidth: 768, imageWidth: 1024 }, { name: 'desktop', minWidth: 1024, imageWidth: 1280 }, { name: 'large', minWidth: 1280, imageWidth: 1920 }, ], blurDataURL: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=', }; /** * Common aspect ratios for different use cases */ export const ASPECT_RATIOS = { square: 1, landscape: 16 / 9, portrait: 3 / 4, ultrawide: 21 / 9, golden: 1.618, card: 4 / 3, hero: 21 / 9, thumbnail: 1, }; // ============================================================================ // SANITY IMAGE LOADER // ============================================================================ /** * Create a Sanity image loader for Next.js Image component * * @param projectId - Sanity project ID * @param dataset - Sanity dataset * @returns Next.js Image loader function */ export function createSanityImageLoader(_projectId = 'project', _dataset = 'dataset') { return ({ src, width, quality = 80 }) => { // Handle already processed URLs if (src.startsWith('http')) { const url = new URL(src); url.searchParams.set('w', width.toString()); url.searchParams.set('q', quality.toString()); return url.toString(); } // Handle Sanity asset references if (src.startsWith('image-')) { return buildSanityImageUrl(src, { width, quality, format: 'auto', }); } // Fallback for other URLs return src; }; } /** * Default Sanity image loader (uses placeholder project/dataset) */ export const sanityImageLoader = createSanityImageLoader(); // ============================================================================ // IMAGE PROCESSING FUNCTIONS // ============================================================================ /** * Process a Sanity image for Next.js Image component * * @param image - Sanity image object or string URL * @param options - Sizing and optimization options * @param config - Image optimization configuration * @returns Optimized image data for Next.js Image */ export function processImageForNextJS(image, options = {}, config = DEFAULT_IMAGE_CONFIG) { // Default dimensions const defaultWidth = 1200; const defaultHeight = 630; // Handle missing image if (!image) { return { src: '/placeholder-image.jpg', alt: 'Placeholder image', width: options.width || defaultWidth, height: options.height || defaultHeight, blurDataURL: config.blurDataURL, loader: sanityImageLoader, }; } // Handle string URLs if (typeof image === 'string') { const width = options.width || defaultWidth; const height = options.height || defaultHeight; return { src: image, alt: 'Image', width, height, sizes: generateSizesAttribute(config.breakpoints), blurDataURL: config.blurDataURL, loader: sanityImageLoader, }; } // Handle Sanity image objects if (image.asset?._ref) { const width = options.width || defaultWidth; const height = options.height || (options.aspectRatio ? width / options.aspectRatio : defaultHeight); const optimizedSrc = buildSanityImageUrl(image.asset._ref, { width, height, fit: options.fit || 'crop', format: options.format || config.format, quality: options.quality || config.quality, }); return { src: optimizedSrc, alt: image.alt || 'Image', width, height, sizes: generateSizesAttribute(config.breakpoints), blurDataURL: config.blurDataURL || generateBlurDataURL(image), loader: sanityImageLoader, }; } // Fallback return { src: '/placeholder-image.jpg', alt: 'Image', width: options.width || defaultWidth, height: options.height || defaultHeight, blurDataURL: config.blurDataURL, loader: sanityImageLoader, }; } /** * Generate responsive image data for different use cases * * @param image - Sanity image object or string URL * @param useCase - The intended use case * @param config - Image optimization configuration * @returns Optimized image data */ export function generateResponsiveImageData(image, useCase, config = DEFAULT_IMAGE_CONFIG) { let options; let priority = false; switch (useCase) { case 'hero': options = { width: 1920, height: 1080, aspectRatio: ASPECT_RATIOS.hero, fit: 'cover', quality: 90, }; priority = true; break; case 'featured': options = { width: 1200, height: 630, aspectRatio: ASPECT_RATIOS.landscape, fit: 'cover', quality: 85, }; break; case 'thumbnail': options = { width: 400, height: 400, aspectRatio: ASPECT_RATIOS.square, fit: 'cover', quality: 80, }; break; case 'avatar': options = { width: 200, height: 200, aspectRatio: ASPECT_RATIOS.square, fit: 'cover', quality: 85, }; break; case 'card': options = { width: 600, height: 400, aspectRatio: ASPECT_RATIOS.card, fit: 'cover', quality: 80, }; break; case 'gallery': options = { width: 800, height: 600, aspectRatio: ASPECT_RATIOS.card, fit: 'cover', quality: 85, }; break; default: options = { width: 800, height: 600, fit: 'cover', quality: 80, }; } const result = processImageForNextJS(image, options, config); result.priority = priority; return result; } /** * Generate sizes attribute for responsive images * * @param breakpoints - Responsive breakpoint configuration * @returns CSS sizes attribute string */ export function generateSizesAttribute(breakpoints) { const sizes = breakpoints.map((bp, index) => { if (index === breakpoints.length - 1) { // Last breakpoint doesn't need a condition return `${bp.imageWidth}px`; } return `(min-width: ${bp.minWidth}px) ${bp.imageWidth}px`; }); return sizes.join(', '); } /** * Generate a blur data URL for placeholder * * @param image - Sanity image object * @returns Base64 blur data URL */ export function generateBlurDataURL(image) { if (!image.asset?._ref) { return undefined; } // Generate a tiny version of the image for blur placeholder const tinyUrl = buildSanityImageUrl(image.asset._ref, { width: 10, height: 10, quality: 20, format: 'jpg', }); // In a real implementation, you might want to fetch this and convert to base64 // For now, return the URL (Next.js can handle this) return tinyUrl; } // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ /** * Calculate optimal image dimensions based on container and content * * @param containerWidth - Container width in pixels * @param containerHeight - Container height in pixels * @param imageAspectRatio - Image aspect ratio * @param fitMode - How the image should fit in the container * @returns Optimal dimensions */ export function calculateOptimalDimensions(containerWidth, containerHeight, imageAspectRatio, fitMode = 'cover') { const containerAspectRatio = containerWidth / containerHeight; if (fitMode === 'cover') { if (imageAspectRatio > containerAspectRatio) { // Image is wider, fit to height return { width: containerHeight * imageAspectRatio, height: containerHeight, }; } else { // Image is taller, fit to width return { width: containerWidth, height: containerWidth / imageAspectRatio, }; } } else { // contain mode if (imageAspectRatio > containerAspectRatio) { // Image is wider, fit to width return { width: containerWidth, height: containerWidth / imageAspectRatio, }; } else { // Image is taller, fit to height return { width: containerHeight * imageAspectRatio, height: containerHeight, }; } } } /** * Get device pixel ratio considerations for image sizing * * @param baseWidth - Base image width * @param baseHeight - Base image height * @param maxDPR - Maximum device pixel ratio to consider * @returns Adjusted dimensions */ export function getHighDPIDimensions(baseWidth, baseHeight, maxDPR = 2) { return { width: Math.round(baseWidth * maxDPR), height: Math.round(baseHeight * maxDPR), }; } /** * Validate image URL and format * * @param url - Image URL to validate * @returns Validation result */ export function validateImageUrl(url) { const errors = []; try { const urlObj = new URL(url); // Check if it's a valid image format const validExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.avif', '.gif', '.svg']; const hasValidExtension = validExtensions.some(ext => urlObj.pathname.toLowerCase().endsWith(ext)); if (!hasValidExtension && !url.includes('cdn.sanity.io')) { errors.push('URL does not appear to be a valid image format'); } // Check protocol if (!['http:', 'https:'].includes(urlObj.protocol)) { errors.push('Image URL must use HTTP or HTTPS protocol'); } const format = urlObj.pathname.split('.').pop()?.toLowerCase(); return { isValid: errors.length === 0, format, errors, }; } catch { return { isValid: false, errors: ['Invalid URL format'], }; } }