A wrapper component around Next.js Image that provides Cloudinary integration with automatic optimization and blur placeholders.
src/components/AppImage.tsx
AppImage is an optimized image component that handles Cloudinary transformations, automatic format selection, quality optimization, and blur placeholders for better user experience.
interface AppImageProps {
alt?: string;
sizes?: string;
width?: number | `${number}` | undefined;
height?: number | `${number}` | undefined;
imageSource?: IServerFile;
}alt: Alternative text for accessibilitysizes: Responsive image sizes (Next.js Image prop)width: Image width (Next.js Image prop)height: Image height (Next.js Image prop)imageSource: Server file object containing provider and key information
interface IServerFile {
provider: "cloudinary" | "r2" | string;
key: string;
}import AppImage from '@/components/AppImage';
function ArticleCover() {
const imageSource = {
provider: "cloudinary",
key: "articles/my-article-cover"
};
return (
<AppImage
imageSource={imageSource}
alt="Article cover image"
width={800}
height={400}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
);
}// Cloudinary image
const cloudinaryImage = {
provider: "cloudinary",
key: "profile-photos/user-avatar"
};
// R2/Other provider image
const r2Image = {
provider: "r2",
key: "https://example.com/image.jpg"
};
return (
<div>
<AppImage imageSource={cloudinaryImage} alt="User avatar" />
<AppImage imageSource={r2Image} alt="External image" />
</div>
);function ImageGallery({ images }: { images: IServerFile[] }) {
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{images.map((image, index) => (
<AppImage
key={index}
imageSource={image}
alt={`Gallery image ${index + 1}`}
width={300}
height={300}
sizes="(max-width: 768px) 50vw, (max-width: 1200px) 33vw, 25vw"
/>
))}
</div>
);
}- Automatic format: Selects optimal format (WebP, AVIF, etc.)
- Quality optimization: Auto quality based on content
- URL generation: Constructs optimized Cloudinary URLs
- Blur placeholder: Generates blurred version for loading states
- Lazy loading: Images load only when needed
- Responsive sizing: Proper sizes attribute for responsive images
- Format selection: Automatic modern format selection
- Quality adjustment: Optimal quality for file size balance
- Provider fallback: Handles non-Cloudinary providers
- Default placeholder: Falls back to local placeholder image
- Error handling: Graceful degradation for missing images
// Quality optimization
.quality("auto")
// Format optimization
.format("auto")
// Blur placeholder
.effect(blur(100000))// Original image
"https://res.cloudinary.com/techdiary-dev/image/upload/q_auto,f_auto/v1/path/to/image"
// Blur placeholder
"https://res.cloudinary.com/techdiary-dev/image/upload/q_auto,f_auto,e_blur:100000/v1/path/to/image"- Full transformation support
- Automatic optimization
- Blur placeholder generation
- Format and quality selection
- Direct URL passthrough
- Default placeholder image
- No transformations applied
- Basic Next.js Image functionality
const cld = new Cloudinary({
cloud: { cloudName: "techdiary-dev" },
});// Main image URL
const imageUrl = cld
.image(imageSource.key)
.quality("auto")
.format("auto")
.toURL();
// Blur placeholder URL
const blurUrl = cld
.image(imageSource.key)
.quality("auto")
.format("auto")
.effect(blur(100000))
.toURL();// Provide proper dimensions
<AppImage
width={800}
height={600}
sizes="(max-width: 768px) 100vw, 50vw"
/>
// Use aspect ratio classes for responsive design
<div className="aspect-video">
<AppImage imageSource={image} alt="Video thumbnail" />
</div>// Always provide meaningful alt text
<AppImage
imageSource={profilePhoto}
alt={`Profile photo of ${user.name}`}
/>
// Use empty alt for decorative images
<AppImage
imageSource={decorativeImage}
alt=""
role="presentation"
/>// Use appropriate sizes for responsive images
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
// Preload important images
<link
rel="preload"
as="image"
href={generateImageUrl(imageSource)}
/>function ArticleCover({ coverImage }: { coverImage: IServerFile }) {
return (
<div className="aspect-video overflow-hidden rounded-lg">
<AppImage
imageSource={coverImage}
alt="Article cover"
width={1200}
height={630}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 70vw"
/>
</div>
);
}function Avatar({ profilePhoto, userName }: {
profilePhoto: IServerFile;
userName: string;
}) {
return (
<div className="w-10 h-10 rounded-full overflow-hidden">
<AppImage
imageSource={profilePhoto}
alt={`${userName}'s avatar`}
width={40}
height={40}
/>
</div>
);
}function Gallery({ images }: { images: IServerFile[] }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{images.map((image, index) => (
<AppImage
key={index}
imageSource={image}
alt={`Gallery image ${index + 1}`}
width={400}
height={300}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
))}
</div>
);
}The component gracefully handles:
- Missing imageSource prop
- Invalid Cloudinary keys
- Network errors
- Unsupported image formats
Ensure Cloudinary is properly configured:
// Environment variables
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=techdiary-dev
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret- ImageDropzoneWithCropper: For image uploads with editing
- Next.js Image: Base component being wrapped
- Cloudinary SDK: For URL generation and transformations