ImagesCDN8 min read

Images

Hotel room images are served from a CDN in multiple variants. This guide covers available sizes, the WebP format, responsive image patterns, and caching best practices.

Image endpoints

Images are associated with room types. Individual rooms inherit images from their room type, but may also have room-specific photos.

GET/api/v1/room-types/:id/imagesRoom type images
GET/api/v1/rooms/:number/images?property_id=Room-specific images
fetch-images.js
javascript
"color:#c4b5fd">const BASE_URL = "https:">//hotel-data.k3s.ahsm-krakow.com/api/v1";

"color:#6b7280">// Images for a room type
"color:#c4b5fd">const response = "color:#c4b5fd">await fetch("color:#86efac">`${BASE_URL}/room-types/apt-penthouse/images`);
"color:#c4b5fd">const { data: images } = "color:#c4b5fd">await response.json();

"color:#6b7280">// Images for a specific room
"color:#c4b5fd">const roomImages = "color:#c4b5fd">await fetch(
  "color:#86efac">`${BASE_URL}/rooms/301/images?property_id=apt`
).then((r) => r.json());

Example response

json
{
  "data": [
    {
      "id": "img-001",
      "room_type_id": "apt-penthouse",
      "filename": "penthouse-living-room.jpg",
      "alt": "Penthouse suite living room with panoramic view",
      "sort_order": 1,
      "variants": {
        "thumbnail": "https://cdn.hotel-data.example.com/images/img-001/thumbnail.webp",
        "medium": "https://cdn.hotel-data.example.com/images/img-001/medium.webp",
        "large": "https://cdn.hotel-data.example.com/images/img-001/large.webp",
        "original": "https://cdn.hotel-data.example.com/images/img-001/original.jpg"
      }
    }
  ]
}

Image variants

Every image is processed into four variants at upload time. CDN URLs are stable and immutable — the same URL will always return the same image.

VariantDimensionsFormatBest used for
thumbnail~320 × 240 pxWebPLazy load placeholders, grid thumbnails, blur-up technique
medium~800 × 600 pxWebPDefault display size for cards and listings
large~1600 × 1200 pxWebPLightbox / full-screen view on high-DPI displays
originalSource resolutionJPEGDownloads, print, or if you need the full-fidelity source

WebP first. The thumbnail, medium, and large variants are served as WebP, which delivers 25-35% smaller file sizes compared to JPEG at equivalent quality. All modern browsers support WebP. Use the original variant only when you explicitly need JPEG.

Usage in Next.js

Use the next/image component with the thumbnail as a blur placeholder for a smooth loading experience.

RoomGallery.tsx
javascript
"color:#c4b5fd">import Image "color:#c4b5fd">from "next/image";

interface RoomImage {
  id: string;
  alt: string;
  variants: {
    thumbnail: string;
    medium: string;
    large: string;
    original: string;
  };
}

"color:#c4b5fd">function RoomGallery({ images }: { images: RoomImage[] }) {
  "color:#c4b5fd">return (
    <div className="grid grid-cols-3 gap-2">
      {images.map((image) => (
        <Image
          key={image.id}
          src={image.variants.medium}
          alt={image.alt}
          width={400}
          height={300}
          "color:#6b7280">// Use thumbnail for initial load, switch to medium
          placeholder="blur"
          blurDataURL={image.variants.thumbnail}
          className="rounded-lg object-cover w-full aspect-4/3"
          loading="lazy"
        />
      ))}
    </div>
  );
}

Responsive images without Next.js

Use a picture element with srcset to let the browser choose the most appropriate size.

ResponsiveRoomImage.tsx
javascript
"color:#6b7280">// Use srcset for responsive images without Next.js Image
"color:#c4b5fd">function ResponsiveRoomImage({ image }) {
  "color:#c4b5fd">return (
    <picture>
      {/* WebP variants for modern browsers */}
      <source
        srcSet={"color:#86efac">`
          ${image.variants.thumbnail} 320w,
          ${image.variants.medium}    800w,
          ${image.variants.large}     1600w
        `}
        sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
        type="image/webp"
      />
      {/* JPEG fallback */}
      <img
        src={image.variants.medium}
        alt={image.alt}
        loading="lazy"
        decoding="">async"
        className="w-full h-auto rounded-lg"
      />
    </picture>
  );
}

Caching strategy

CDN image URLs are immutable — the filename includes a content hash, so you can cache them aggressively for up to one year. API responses that include image metadata should be cached for 5 minutes.

cache-headers.ts
javascript
"color:#6b7280">// If you proxy images through your own server,
"color:#6b7280">// set aggressive cache headers since CDN URLs are immutable.
res.setHeader(
  "Cache-Control",
  "public, max-age=31536000, immutable"
);

"color:#6b7280">// For API responses (not the images themselves),
"color:#6b7280">// use shorter cache with stale-while-revalidate:
res.setHeader(
  "Cache-Control",
  "public, max-age=300, stale-while-revalidate=600"
);
CDN image URLs
Cache for up to 1 year.
max-age=31536000, immutable
Image list API response
Cache for 5 minutes.
max-age=300, stale-while-revalidate=600

Performance checklist

  • Use the thumbnail variant as a blur placeholder during initial load
  • Serve the medium variant for standard card/grid views
  • Only load the large variant in lightbox or full-screen contexts
  • Set loading="lazy" on images below the fold
  • Use WebP variants (thumbnail, medium, large) — not original — in browsers
  • Set explicit width and height to prevent layout shifts (CLS)
  • Preload hero images with <link rel="preload"> for LCP optimization