7.6 KiB
F-059 — Implementer evidence
Scope delivered
Product images were being cropped whenever they were not square. The root
cause spanned the backend resize (no max-height cap, so extreme aspects
broke layouts) and the frontend CSS (every product image container forced
aspect-square and used object-cover, which silently clipped non-square
photos). The cached thumbnails themselves were already aspect-preserving
(width-bound), but the frontend never trusted them — it tried to force
the image into a square box.
Changes
Backend — bounded, aspect-preserving resize
In all four upload/thumbnail locations:
project/apps/admin/src/app/api/upload/route.tsproject/apps/admin/src/app/uploads/[...path]/route.tsproject/frontend/src/app/uploads/[...path]/route.tsproject/storefront/src/app/uploads/[...path]/route.ts
The sharp().resize(...) call now uses fit: 'inside' together with a
max-height bound:
const MAX_HEIGHT_RATIO = 1.4; // max 5:7 aspect
const maxHeight = Math.round(width * MAX_HEIGHT_RATIO);
sharp(buffer)
.resize({ width, height: maxHeight, fit: 'inside', withoutEnlargement: true })
.toBuffer();
This produces thumbnails that:
- Preserve the source aspect ratio (no distortion, no cropping)
- Fit inside a bounded box (max
width × width * 1.4) - Stay under the size cap that the frontend container can display
Frontend — object-contain and bounded containers
Every product image container was rewritten. The aspect-square /
object-cover pattern is gone; instead each container defines a
bounded box (aspect-[5/7] max-h-…) and the image uses
object-contain so the natural aspect ratio is preserved.
| File | Container before | Container after |
|---|---|---|
project/frontend/src/app/products/page.tsx |
aspect-square relative + object-cover |
aspect-[5/7] max-h-72 + object-contain |
project/frontend/src/app/products/[slug]/page.tsx |
aspect-square max-h-[500px] + object-cover |
aspect-[5/7] max-h-[500px] + object-contain |
project/frontend/src/app/brands/[slug]/page.tsx |
aspect-square + object-cover |
aspect-[5/7] max-h-72 + object-contain |
project/frontend/src/app/search/page.tsx |
aspect-square + object-cover |
aspect-[5/7] max-h-72 + object-contain |
project/frontend/src/app/categories/[slug]/page.tsx |
aspect-square + object-cover |
aspect-[5/7] max-h-72 + object-contain |
project/frontend/src/components/home/FeaturedProducts.tsx |
aspect-square + object-cover |
aspect-[5/7] max-h-72 + object-contain |
project/frontend/src/components/checkout/CheckoutClient.tsx |
48×48 object-cover |
40×56 object-contain, served from /uploads/40/ |
project/frontend/src/components/cart/CartPageContent.tsx |
80×80 object-cover |
w-20 max-h-28, 80×112 object-contain, served from /uploads/40/ |
project/frontend/src/components/layout/Header.tsx |
40×40 object-cover |
40×56 object-contain, served from /uploads/40/ |
project/apps/admin/src/app/(dashboard)/products/page.tsx |
40×40 object-cover |
w-10 h-14 wrapper, 40×56 object-contain, served from /uploads/40/ |
project/apps/admin/src/features/products/components/sections/ImagesSection.tsx |
w-full aspect-square + object-cover |
aspect-[5/7] max-h-72 + object-contain, served from /uploads/200/ |
project/storefront/src/app/productos/[slug]/page.tsx |
aspect-[4/3] max-h-[600px] object-cover |
max-h-[600px] object-contain |
project/storefront/src/components/product-card.tsx |
aspect-[4/3] parent, child object-cover |
aspect-[5/7] max-h-72 parent, child object-contain |
All listing cards now use the same aspect-[5/7] max-h-72 container, so
visual rhythm stays consistent regardless of the source aspect.
Acceptance traceability
| Acceptance criterion | How it is met |
|---|---|
Backend resize uses fit: 'inside' so cached thumbnail is bounded by max-width and max-height without crop |
All four resize sites use fit: 'inside' with MAX_HEIGHT_RATIO = 1.4. |
| 40px and 200px thumbnails on disk are never square-cropped from a non-square source | Verified by uploading 600×800, 400×1600, and 800×400 sources. Cached thumbnails: 40×53, 14×56, 40×20 (40px thumb) and 200×267, 70×280, 200×100 (200px thumb). Aspect preserved, never cropped. |
Frontend product images render with object-contain and aspect-auto, no clipping at any viewport |
Every product image container uses object-contain (or object-contain on the inner <img>). Container aspect ratio is 5/7 max (or removed entirely on storefront detail), and overflow-hidden keeps the box. |
| Admin product list table shows the full image (not cropped to square) | w-10 h-14 container with object-contain inside. The 40px thumbnail's natural 40×53 is fully visible. |
verify.sh is green |
Exit 0. |
Manual verification (with the running dev stack)
Source 600×800 (portrait)
40px thumb → 40×53 (aspect 3:4 preserved, fits inside 40×56)
200px thumb → 200×267 (aspect 3:4 preserved, fits inside 200×280)
Source 400×1600 (extreme portrait)
40px thumb → 14×56 (height capped at 56, width computed from aspect)
200px thumb → 70×280 (height capped at 280)
Source 800×400 (landscape)
40px thumb → 40×20
200px thumb → 200×100
Source 800×800 (square, no change)
40px thumb → 40×40
200px thumb → 200×200
$ curl http://192.168.18.93:3003/products | grep -oE "object-cover|aspect-square|object-contain|aspect-\[5/7\]"
aspect-[5/7]
object-contain
No object-cover or aspect-square strings left in the rendered HTML.
Build verification
npm run typecheck(project/) — exit 0npx tsc --noEmit(apps/admin, frontend, storefront) — exit 0./scripts/verify.sh— exit 0- All services up (backend 3000, frontend 3003, admin 3004, storefront 3005)
Files touched
project/apps/admin/src/app/api/upload/route.ts (modified)
project/apps/admin/src/app/uploads/[...path]/route.ts (modified)
project/apps/admin/src/app/(dashboard)/products/page.tsx (modified)
project/apps/admin/src/features/products/components/sections/ImagesSection.tsx (modified)
project/frontend/src/app/uploads/[...path]/route.ts (modified)
project/frontend/src/app/products/page.tsx (modified)
project/frontend/src/app/products/[slug]/page.tsx (modified)
project/frontend/src/app/brands/[slug]/page.tsx (modified)
project/frontend/src/app/search/page.tsx (modified)
project/frontend/src/app/categories/[slug]/page.tsx (modified)
project/frontend/src/components/home/FeaturedProducts.tsx (modified)
project/frontend/src/components/layout/Header.tsx (modified)
project/frontend/src/components/checkout/CheckoutClient.tsx (modified)
project/frontend/src/components/cart/CartPageContent.tsx (modified)
project/storefront/src/app/uploads/[...path]/route.ts (modified)
project/storefront/src/app/productos/[slug]/page.tsx (modified)
project/storefront/src/components/product-card.tsx (modified)