Un carnet de voyage jour par jour, lieu par lieu. A travel journal that follows you day by day, place by place.
Des photos de mon voyage rattachées à leurs lieux par matching GPS, 10 jours en Crète, 27 lieux. Une application web mobile-first pour partager le voyage avec les proches, sans dépendre des réseaux sociaux. Photos from my trip matched to their places by GPS, 10 days in Crete, 27 places. A mobile-first web app to share the trip with loved ones, without depending on social networks.
Le pitch
Je reviens de 10 jours en Crète avec 170 photos et vidéos. Plutôt que de les noyer dans une pellicule ou de tout déverser sur Instagram, je les pose sur une carte et un fil jour par jour : chaque cliché retrouve son lieu tout seul grâce à ses coordonnées GPS. Et le tout existe en deux versions — un carnet privé pour la famille, et un book public qui masque automatiquement les photos où l'on apparaît.
Le parcours d'une photo
- Extraction — au build, un script Node (
exifr) lit les métadonnées EXIF de chaque média : GPS, date, orientation. - Rattachement — chaque photo est associée au lieu le plus proche par matching GPS (~2 km) puis rangée dans un
photos.jsonstatique, indexé par lieu. - Hébergement — les 170 médias vivent sur Cloudflare R2 et sont redimensionnés à la volée par Cloudflare Image Resizing, avec des paliers de largeur canoniques pour maximiser le taux de cache.
- Affichage — pages jour, pages lieu, carte Leaflet interactive (modes « photos » et « parcours ») et lightbox plein écran. Les journées proches sont fusionnées à l'affichage pour garder un fil lisible.
Deux sites, un seul code
Le vrai défi du projet, c'est ce dédoublement : le même dépôt sert deux sites distincts.
full— le carnet complet, toutes les photos, pour les proches.portfolio— le book public (crete.benjamin-goalabre.me) qui filtre partout les clichés marquésfamily: grilles, hero, carte, lightbox, compteurs.- Filtre étanche — tous les accès aux données passent par un point d'entrée unique qui applique le filtre. Impossible qu'une photo de famille fuite côté public.
- Pilotage — une seule variable d'environnement par projet Vercel, figée au build.
Ingénierie
- Next.js 15 (App Router, SSG) + React 19 — deux projets Vercel sur la même branche, distingués par
NEXT_PUBLIC_VARIANT. - Leaflet en import dynamique côté client (
ssr: false) + tuiles OpenStreetMap — pas de clé d'API, pas de quota Mapbox. - CI en matrice
variant: [full, portfolio]— lint, typecheck, tests Vitest et build validés pour les deux variantes avant auto-merge. - Preprod VPS OVH (Docker via rsync + docker compose) en plus des preprods Vercel.
- Non-indexé par design —
noindex / nofollow / noarchivevia metadata, balise HTML et header HTTP. Un voyage privé, pas un produit référencé.
Statut
En ligne sur crete.benjamin-goalabre.me, code source public sur GitHub.
The pitch
I come back from 10 days in Crete with 170 photos and videos. Instead of drowning them in a camera roll or dumping everything on Instagram, I lay them on a map and a day-by-day thread: each shot finds its own place from its GPS coordinates. And the whole thing exists in two versions — a private journal for the family, and a public book that automatically hides the photos we appear in.
A photo's journey
- Extraction — at build time, a Node script (
exifr) reads each media's EXIF metadata: GPS, date, orientation. - Matching — every photo is linked to the nearest place by GPS match (~2 km), then stored in a static
photos.json, indexed by place. - Hosting — the 170 media files live on Cloudflare R2 and are resized on the fly by Cloudflare Image Resizing, with canonical width steps to maximize cache hits.
- Display — day pages, place pages, an interactive Leaflet map ("photos" and "route" modes) and a full-screen lightbox. Adjacent days are merged in the display to keep a readable thread.
Two sites, one codebase
The real challenge of the project is this duality: the same repo powers two distinct sites.
full— the complete journal, all photos, for the family.portfolio— the public book (crete.benjamin-goalabre.me) that filters out every shot taggedfamilyeverywhere: grids, hero, map, lightbox, counts.- Airtight filter — all data access goes through a single entry point that applies the filter. No way for a family photo to leak on the public side.
- Control — a single env var per Vercel project, baked at build time.
Engineering
- Next.js 15 (App Router, SSG) + React 19 — two Vercel projects on the same branch, distinguished by
NEXT_PUBLIC_VARIANT. - Leaflet client-side dynamic import (
ssr: false) + OpenStreetMap tiles — no API key, no Mapbox quota. - Matrix CI
variant: [full, portfolio]— lint, typecheck, Vitest tests and build validated for both variants before auto-merge. - OVH VPS preprod (Docker via rsync + docker compose) alongside Vercel preprods.
- Non-indexed by design —
noindex / nofollow / noarchivevia metadata, HTML tag and HTTP header. A private trip, not an indexed product.
Status
Live at crete.benjamin-goalabre.me, source code public on GitHub.