fix: restore aspect-ratio layouts and improve hero image quality (#180)

- Fix masonry/mosaic layout regression where tiles displayed uniform heights
  instead of respecting image aspect ratios. Changed from fixed 150-500px
  height constraints to dynamic constraints based on column width.

- Add hero image optimization pipeline generating 1920x1080 images for
  full-width hero sections instead of using low-quality thumbnails.

- New /hero/:photoId endpoint serves optimized hero images with watermark
  support and automatic generation/caching.

- Add hero_url field to photos API response for frontend consumption.

- Migration 069 adds hero_path column to photos table.
This commit is contained in:
Paul Nothaft
2026-02-15 22:43:18 +01:00
parent d63f67a2af
commit 5cef7fdd18
6 changed files with 286 additions and 9 deletions
@@ -137,8 +137,8 @@ export const HeroHeader: React.FC<HeroHeaderProps> = ({
{/* Hero Section */}
<div className="relative h-[60vh] sm:h-[70vh] lg:h-[80vh] max-h-[700px] -mx-4 sm:-mx-6 lg:-mx-8 mb-8">
<AuthenticatedImage
src={heroPhoto.url}
fallbackSrc={heroPhoto.thumbnail_url || undefined}
src={heroPhoto.hero_url || heroPhoto.url}
fallbackSrc={heroPhoto.url}
alt={heroPhoto.filename}
className="w-full h-full object-cover"
style={{ objectPosition: heroImageAnchor }}
@@ -62,10 +62,11 @@ const MasonryPhoto: React.FC<MasonryPhotoProps> = ({
const aspectRatio = photoWidth / photoHeight;
// Calculate height based on column width and aspect ratio
// Clamp to reasonable min/max heights for visual consistency
// Use dynamic constraints based on column width to preserve aspect ratio variation
// This allows panoramic images to be short and tall portraits to be tall
const calculatedHeight = columnWidth / aspectRatio;
const minHeight = 150;
const maxHeight = 500;
const minHeight = Math.max(80, columnWidth * 0.25); // Allow wide panoramics (4:1)
const maxHeight = columnWidth * 2.5; // Allow tall portraits (1:2.5)
return Math.max(minHeight, Math.min(maxHeight, calculatedHeight));
}, [photo.width, photo.height, columnWidth]);
@@ -361,11 +362,13 @@ export const MasonryGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
// Add photo to shortest column
cols[shortestCol].push(photo);
// Estimate height based on aspect ratio
// Estimate height based on aspect ratio (using same constraints as MasonryPhoto)
const photoWidth = photo.width || 800;
const photoHeight = photo.height || 600;
const aspectRatio = photoWidth / photoHeight;
const estimatedHeight = Math.max(150, Math.min(500, approxColWidth / aspectRatio));
const minHeightConstraint = Math.max(80, approxColWidth * 0.25);
const maxHeightConstraint = approxColWidth * 2.5;
const estimatedHeight = Math.max(minHeightConstraint, Math.min(maxHeightConstraint, approxColWidth / aspectRatio));
colHeights[shortestCol] += estimatedHeight + gutter;
});