This file defines the requirements for the Photos section of andyhiggs.uk.
All implementation work should conform to this spec.
Scope: Everything in this spec applies only to/photosand its assets. The rest of andyhiggs.uk must remain untouched.
A photo stories section at andyhiggs.uk/photos that migrates six existing stories from the legacy photos.andyhiggs.uk site. The section presents photography in a clean, white gallery aesthetic — distinct in feel from the main andyhiggs.uk site — while remaining visually connected via a shared (but adapted) header.
Landing page (/photos/):
Story page (/photos/<slug>/):
Navigation:
_data/photoStories.js using Eleventy pagination (one page per story)flex-grow = w/h × 1000), recreating the Exposure "fill-row" justified lookData lives in _data/photoStories.js as an exported array. Each story object:
{
slug: String, // URL segment, e.g. "boulder"
title: String, // Display title, e.g. "Boulder"
description: String, // Story-level intro text (shown in hero overlay and on cards)
date: String, // ISO date, e.g. "2019-12-29"
cover: String, // Path to cover image, e.g. "/img/photos/boulder/cover.webp"
sections: [
{
title: String | null, // Section heading, e.g. "Tibet" (null for untitled)
description: String | null, // Paragraph of text for this section (null if none)
textOnly: Boolean, // true for text-only sections (no photos); omitted (falsy) on photo sections
rows: [
// Each row is an array of photos shown side-by-side
[{ src: String, w: Number, h: Number }, ...]
]
}
]
}
A section with textOnly: true (empty rows array) renders as a text-only block (e.g. "About this collection" in A Decade of Travels).
Migrated from photos.andyhiggs.uk. Sections, titles, descriptions, and photo sets match the originals exactly:
boulder) — 18 photos, no named sectionson-hawaii) — 50 photos, 6 sections: O'ahu · surf, moco loco, sharks · queens bath · Nā Pali Coast, Air & Sea · Hanelei bay · beachside in waikikia-decade-of-travels) — 54 photos, 11 photo sections + 1 text-only section ("About this collection"), each with a paragraph of narrative texta-week-with-buffer) — 32 photos, 8 sectionsnew-zealand) — 42 photos, 6 sectionsnorth-of-limpopo) — 31 photos, 7 sectionsImages are stored as .webp files under /img/photos/<slug>/, numbered 001.webp, 002.webp, etc., plus a cover.webp.
| Role | Value |
|---|---|
| Background | #fff (white) |
| Text | #111 or #222 (near-black) |
| Secondary text / meta | #666 or #888 (mid-grey) |
| Borders / lines | #ddd or #e0e0e0 (light grey) |
| Header top border | #111 (thin black line) |
| Hover accents | #444 (dark grey) |
flex-grow = w/h × 1000), recreating the Exposure "fill-row" justified lookheight: auto, object-fit: unset)1rem vertical gap between rows; 1rem horizontal margin on the left and right of the photo area0.5rem horizontal padding on each photo-item (so 1rem total between adjacent photos)| Group size | Treatment | CSS class | Notes |
|---|---|---|---|
| 1 photo | Centred at natural dimensions; never stretched beyond its pixel width; whitespace fills the sides; shrinks smoothly when viewport is narrower than the image | photo-row--single |
flex: 0 1 auto, width/height: auto, object-fit: unset |
| 2–4 photos | Justified row at natural aspect ratio — all photos the same height, widths proportional to aspect ratio | (default) | Portrait photos appear tall; landscape photos appear wide; no cropping |
| 5+ photos | Same justified row treatment — photos are smaller thumbnails but still uncropped | (default) | Exposure sometimes grouped many photos into one row as a contact-sheet effect |
Determining row groupings: Extract from the original .webarchive using the Python plist parser to read computed width/height styles on .photo relative clearfix elements. Photos with the same rendered height belong to the same visual row.
photoStories data arraycss/photos.css) loaded exclusively via _includes/layouts/photos-base.njk, which is used only by photos pages.photos-site — a class applied to <body> in photos-base.njk — ensuring no bleed into the main siteindex.css sets p { min-width: 310px } globally. Any <p> inside a photos card or constrained container must override this with min-width: 0 to prevent overflowwidth and height attributes set, .webp formatalt text where possible, keyboard-navigable cardsWhen photos are exported from Exposure.co stories, the files are numbered in a round-robin pattern across all sections, not section-by-section. Understanding this is essential for correctly assigning files to sections when building the data.
File dimensions (actual):
001–021 (odd numbers only): 1000×621 — standard grid photos002–020 (even numbers only): 2000×1241 — wide/panoramic singles (except 008 = 2000×772, a landscape panoramic)022–053: 1000×621 — standard grid photos054: 1400×869 — slightly larger grid photoNumbering structure:
| Files | Description |
|---|---|
001, 003 |
Tibet grid photos 1–2 |
005, 007 |
Delhi grid photos 1–2 |
009 |
Chitwan grid photo 1 |
011, 013 |
Hong Kong grid photos 1–2 |
015 |
Daschi grid photo 1 |
017, 019 |
Cameron Highlands grid photos 1–2 |
021, 022 |
Space Houses grid photos 1–2 |
023 |
Agra grid photo 1 |
024, 025 |
Phewa Tal grid photos 1–2 |
026 |
Tokyo grid photo 1 |
027, 028 |
Sydney grid photos 1–2 |
029–037 |
Grid photo 3 for each section in order (Tibet→Sydney) |
038–045 |
Grid photo 4 for each section in order (Tibet→Sydney) |
046–053 |
Remaining grid photos filling sections short of 4 (in section order) |
054 |
Final grid photo (Tokyo) |
002, 004, 006, 008, 010, 012, 014, 016, 018, 020 |
Wide/panoramic singles, one per section (no Sydney wide) |
Wide single → section mapping (derived from visual content identification):
| File | Section |
|---|---|
002 |
Chitwan |
004 |
Daschi |
006 |
Agra |
008 (2000×772) |
Tokyo |
010 |
Tibet |
012 |
Hong Kong |
014 |
Delhi |
016 |
Space Houses |
018 |
Phewa Tal |
020 |
Cameron Highlands |
Key insight: The even-numbered files 002–020 are NOT placed in the section that corresponds to their numerical position. They must be identified by visual content and assigned accordingly. The wide singles are interleaved with the grid photos in the export numbering, which is what caused the confusion in initial implementations.
For other Exposure stories, check actual file dimensions first (using the Python WebP header parser or similar) to identify which files are wide singles vs standard grid photos. Then assign wides to sections by visual content identification. The round-robin grid photo pattern should hold across stories.
/photos