Engineering a Photography Portfolio Without Sacrificing the Photographs

The hard part was not displaying twenty published photographs. It was preserving their detail, privacy, and 360-degree behavior without making every visitor download the archive.

Frontend Engineer & Photographer
Independent
2026
Low sunlight shines through a stone arch toward a garden.

Photography Is Part of the Product

This photography page lives inside a frontend-engineering portfolio. That makes image delivery part of the work itself: the photographs need to remain visually convincing while the page still behaves like a fast, accessible application.

The original gallery already had its contact-sheet layout, darkroom interactions, keyboard lightbox, responsive columns, and reduced-motion behavior. The implementation task was deliberately narrower: replace placeholder frames with a safe, reproducible asset system without redesigning the page.

One Folder, Several Different Problems

  • Large and inconsistent sources: The selected set totalled 172.5 MB across portrait, landscape, wide, alpha-channel PNG, JPEG, and DNG inputs.
  • Private metadata: Seven files exposed GPS markers. Camera and lens details could be useful; coordinates, serials, source paths, filenames, and editing identities could not enter the public bundle.
  • Different viewing contexts: A dense masonry grid, a small filmstrip, a full-screen viewer, and an 8K photosphere do not need the same bytes.
  • RAW and panorama branches: Two DNGs needed explicit fallback decisions, while two confirmed photospheres had to remain interactive rather than becoming flattened wide strips.

Safety Before Cleverness

The source folder is read-only. The pipeline never renames, moves, edits, strips, or deletes an original; it verifies every source checksum again after processing.

The first version stays local and static, keeps categories and tags empty, drafts factual alt text for review, uses no creative colour grading, never upscales, respects reduced motion, and loads panoramas only after a visitor opens one.

What the Audit Actually Found

22
541 MP
7

The set contained twenty standard rasters and two DNGs. GPano XMP confirmed two true equirectangular photospheres. Six other 2:1 files were flagged as possible panoramas for review, but they remain ordinary photographs because aspect ratio alone is not evidence of projection type.

SHA-256 found no exact duplicate groups. Filename association and perceptual review identified two likely edit/original groups. Both user-confirmed duplicates are now excluded by checksum while their sources remain untouched.

One Deterministic Path, Three Processing Branches

  1. Read-only source audit
  2. Duplicate + privacy review
  3. Raster / DNG / panorama
  4. Orientation + sRGB
  5. Role-based derivatives
  6. Typed manifest
  7. Gallery + viewer

A stable public ID combines a sanitized source stem with the first eight characters of its SHA-256 hash. The cache key combines the full source checksum, processing-profile version, Sharp version, and libvips version, so a warm rerun can skip unchanged work without relying on modification times.

The Standard Raster Path

Sharp 0.34.5 and libvips 8.17.3 apply EXIF orientation, flatten any alpha channel, convert to sRGB, and create four uncropped WebP roles. Thumb uses a 360 px long edge at quality 55; grid uses 1400 px at 72; viewer uses 2800 px at 84; and the 32 px placeholder uses quality 30.

Public derivatives use Sharp's default metadata-stripping behavior. Dominant colours come from the generated grid image for stable contact-sheet wells, while the tiny WebP placeholder becomes the viewer blur data URL. No creative sharpening or grading is applied.

RAW Files Without Pretending

The machine had no ExifTool, darktable, RawTherapee, or other full RAW developer. Both DNGs did expose sufficiently large embedded 8-bit previews: one at 3456 × 4608 after orientation and one at 1928 × 2560. The pipeline records both outcomes explicitly as extracted from embedded preview.

A small thumbnail would have triggered manual conversion instead. The pipeline does not treat any decodable DNG payload as automatically suitable for a full-screen result.

Keeping the Photospheres Spherical

Both confirmed panoramas retain an uncropped, 8192 px equirectangular JPEG at quality 88 with their GPano XMP re-injected after optimisation. Separate centre posters feed the thumb and grid roles, so browsing never downloads a compressed panoramic strip.

The validator reopened both public panoramas, confirmed ProjectionType=equirectangular and UsePanoramaViewer=True, and measured normalized left/right seam scores of 0.0237 and 0.0249 against a 0.35 review threshold.

When a panorama opens, Next.js dynamically loads the panorama component. That component then imports Three.js and requests the full equirectangular file. Until the texture is interactive, the normal poster remains visible as a fallback. Both photospheres now start at a 180-degree yaw from their previous default.

Download the Detail the Current Context Needs

Contact sheet
Full-screen viewer
360° photograph

The grid sizes string mirrors the real two-, three-, and four-column layouts after page gutters. Only the first grid frame is eager and high-priority; the other nineteen are lazy. The filmstrip uses 360 px thumbs instead of repeating grid assets.

The lightbox mounts only the active viewer asset. After that image decodes, a generation token starts prefetching the previous and next viewer files; rapid navigation invalidates stale work. Performance marks cover click-to-decode, adjacent readiness, and panorama open-to-interactive timing.

Only the selected frame receives a temporary photo-active View Transition name. During open and close, that identity moves between the loaded grid image and the active viewer or panorama poster; keeping the other nineteen frames in the root snapshot prevents them from floating above the lightbox.

The gallery does not use JavaScript virtualization. Next/Image already defers off-screen network work, and every frame reserves its aspect ratio, so unmounting masonry items would add observer state, blank back-scrolls, and repeat decodes for only twenty photographs. A scroll profile instead identified the route-wide Lenis 0.1 interpolation as the source of the heavy feeling, so Photography uses immediate scrolling while the rest of the portfolio keeps its smooth-scroll treatment.

Privacy and Accessibility Share the Manifest Boundary

The private audit keeps source paths, filenames, checksums, metadata findings, associations, warnings, and duplicate rationale outside public assets. The generated client manifest keeps only stable IDs, role URLs, dimensions, safe camera fields, colours, blur placeholders, and alt-review state.

All twenty published items have visually reviewed factual alt drafts, but remain marked draft until final editorial approval. The grid reserves exact aspect ratios and exposes the same descriptions through its image-opening controls. Keyboard arrows, Escape, focus return, reduced motion, and the panorama poster fallback remain intact.

Measured Output, Not a Claimed Score

172.5 MB
28.9 MB
1.43 ms

Median source size was 6.55 MB. Median generated files were 8.8 KB for thumbs, 116 KB for grid, and 464 KB for standard viewers. Across role duplication, median per-photo reduction was 45.85% and average reduction was 57.92%.

The two full panoramas account for 16.08 MB of the 28.9 MB output, which is why they are isolated behind interaction. The cold run took 28.53 seconds. The final warm run skipped all twenty published sources in 1.43 ms, reran the complete twenty-two-source audit, and validated all published outputs.

A cache-disabled local production run at 1440 × 1000 transferred 1.18 MB initially, including 201 KB of image transfer, with zero initial layout shift and no panorama request. The active viewer decoded 63.2 ms after click, adjacent prefetch completed 9.6 ms later, and the panorama became interactive in 541 ms. At 390 × 844 with a 3× device scale and emulated 4G, the initial route transferred 1.75 MB, recorded a 1.31 s LCP, and again recorded zero layout shift. A 180 px wheel step reached 157 px in 10 ms and settled in 60 ms on Photography; the unchanged smooth route reached 35 px at 10 ms and remained at 150 px after 250 ms. These are local lab observations, not field performance claims.

Decisions and Rejected Alternatives

  • Local assets over a CDN: Twenty published photographs fit a source-controlled workflow today. A CDN would add operational complexity before scale requires it.
  • WebP over AVIF: WebP keeps encoding fast and predictable in the existing Next.js path. No representative AVIF benchmark demonstrated a reason to add a second format in this pass.
  • Role-specific files over one master: The filmstrip, masonry grid, viewer, and panorama have materially different jobs. A single master would make the browser repeatedly decode or request unnecessary detail.
  • Adjacent prefetch over preload-all: Only two likely next images warm after the active frame is decoded. Hidden viewer files do not compete with initial grid delivery.
  • Interactive panoramas over flattened strips: The photospheres remain navigable photographs. Their larger files and rendering code are paid only by visitors who open them.

The Cost of Doing It Deliberately

The system adds a processing profile, cache, typed manifest, private review artifacts, validation reports, and four common roles per standard photograph. That is more metadata and storage than copying one JPEG into public.

Automatic RAW development remains intentionally conservative: embedded previews were adequate for this set, but future DNGs may require manual conversion or a dedicated developer. Panoramas remain the largest files, and alt text still needs human approval.

A Gallery With Context-Sized Detail

The gallery now downloads the detail appropriate to browsing, inspecting, or exploring a photosphere. The complete public derivative set is 83% smaller than the untouched selection in aggregate, while the role-aware comparison accounts honestly for duplicated viewing roles at a 57.92% average per-photo reduction.

More importantly, the originals remain unchanged, GPS never crosses the public boundary, layout space is known before download, and the two 360-degree photographs keep the interaction that makes them distinct.

What I Would Improve Next

  • Editorial approval: Finalize the factual alt-text drafts after editorial review.
  • RAW consistency: Add a colour-managed RAW developer when the selection includes files whose embedded previews are not sufficient.
  • Curated viewing: Store focal points, collections, and per-panorama starting views only when they are deliberately authored.
  • Delivery scale: Move derivatives to an image CDN only when collection size or deployment cost makes local assets the limiting factor.