Update CLAUDE.md for pagination, medium images, swipe nav and the Android app

Also corrects the serif font, which is Crimson Pro rather than Vollkorn,
and notes that .gitignore patterns here have to be anchored or they eat
the Kotlin package directory.
This commit is contained in:
2026-08-19 23:36:14 +02:00
parent 15822ed227
commit c169c030d6
+57 -9
View File
@@ -2,6 +2,7 @@
Personal static photo gallery with Atom feed and upload API. Single-user.
No database, no framework — just a Go binary that watches a directory and regenerates a static site.
An Android app in `android/` posts to the same upload API.
## Architecture
@@ -9,7 +10,8 @@ No database, no framework — just a Go binary that watches a directory and rege
content/ # images + .toml sidecars (source of truth)
public/ # generated output (served by nginx / --serve flag)
templates/ # embedded HTML templates
static/ # embedded CSS
static/ # embedded CSS + JS
android/ # Kotlin/Compose uploader app, own flake
```
The binary does three things at once:
@@ -17,13 +19,15 @@ The binary does three things at once:
- Serves a multipart upload API at `POST /upload`
- In dev mode (`--serve`), also serves `public/` as static files
In production nginx sits in front: serves `public/` directly, proxies `/upload` to localhost, htpasswd on the upload endpoint.
Routes: `GET /upload` (HTML upload form), `POST /upload` (multipart), `GET /health`.
In production nginx sits in front: serves `public/` directly, proxies `/upload` to localhost, htpasswd on the upload endpoint. Note `/health` is **not** proxied — nginx only forwards `location /upload`, so anything outside the box can only reach `/upload`.
## Stack
- **Go** — stdlib + `BurntSushi/toml`, `fsnotify/fsnotify`, `golang.org/x/image`
- **Templates** — `html/template`, embedded via `embed.FS`
- **Images** — centre-crop to square, Catmull-Rom resize to 600×600 JPEG thumbnails
- **Images** — three renditions per photo: original copy, 600×600 centre-cropped square thumbnail, and a 1600px-longest-side "medium" for the detail page. Catmull-Rom resize. Outputs are skipped when newer than the source.
- **Feed** — Atom (`encoding/xml`)
- **NixOS** — `flake.nix` + `module.nix`, exports `nixosModules.default`
@@ -34,15 +38,36 @@ caption = "Tempelhof at sunrise"
date = "2026-06-14"
```
Missing sidecar → filename as caption, today as date.
Missing sidecar → filename as caption, today as date. Photos sort newest first by that date.
## Design
- Gruvbox dark hard colour scheme
- Vollkorn (serif) for headings and captions
- Manjari for Malayalam text (bio line)
- 3-column square grid → 2-column on mobile
- Single photo page with sidebar
- Crimson Pro (serif) for headings and captions — configurable via `-serif-family` / `-fonts-url`
- Manjari for Malayalam text (bio line), via `-ml-family`
- 3-column square grid → 2-column on mobile, 60 photos per page (`/`, `/page/2/`, …)
- Single photo page with sidebar and newer/older navigation
### Photo page navigation
`static/photo-nav.js` handles arrow keys and touch swipe. Both read their
destinations from the `[data-nav="newer"]` / `[data-nav="older"]` anchors the
template renders, so a photo with no sibling in that direction has nothing to
find and the gesture rubber-bands. If the script fails to load the links still
work.
Swipe left → older, right → newer, matching ArrowRight/ArrowLeft. Constraints
worth knowing before touching it:
- Swipes starting within 24px of a screen edge are ignored — that strip is the browser's own back-gesture.
- Only `pointerType === "touch"`, so desktop drag-to-save is untouched.
- Axis locks once after 10px so vertical drags stay scrolls; `touch-action: pan-y` tells the compositor the same.
- The image is wrapped in an `<a>` to the full-size original, so the click trailing a swipe is suppressed or it opens a new tab.
- `EXIT_MS` in the JS must stay in sync with the transition duration in `style.css`.
Anything added under `static/` is embedded and copied to `public/static/`
automatically — `assets.go` embeds the whole directory and `copyStatic()` walks
every entry, so new CSS/JS needs no generator change.
## Running locally
@@ -54,6 +79,20 @@ go run . --serve
# upload: http://localhost:8080/upload
```
## Android app
Kotlin + Jetpack Compose uploader: pick or share a photo, add caption and date, post. Credentials (HTTP Basic) are sealed with an Android Keystore AES-GCM key. See `android/README.md` for the detail.
Its own flake, deliberately separate from the root one so NixOS consumers of `nixosModules.default` don't pull the Android SDK into their lock:
```bash
cd android
nix develop
./gradlew assembleRelease # app/build/outputs/apk/release/app-release.apk
```
The server needs no changes to serve it — every gap (slug collisions, EXIF, HEIC, size caps) is closed client-side. Release builds need `android/keystore.properties`; without it the signing config is skipped and only debug builds work.
## NixOS module usage
```nix
@@ -86,12 +125,19 @@ nix build # succeeds
| File | Purpose |
|---|---|
| `main.go` | Flags, wires generator + watcher + server |
| `generator.go` | Scans content dir, renders HTML + Atom feed, generates thumbs |
| `generator.go` | Scans content dir, renders HTML + Atom feed, generates thumbs/mediums |
| `server.go` | Upload API + optional static file serving |
| `watcher.go` | fsnotify watcher with 500ms debounce |
| `assets.go` | `//go:embed` for templates and static |
| `templates/photo.html` | Single photo page, neighbour links and prefetch |
| `static/photo-nav.js` | Arrow-key and swipe navigation |
| `module.nix` | NixOS module (systemd service, tmpfiles, nginx vhost) |
| `flake.nix` | `buildGoModule` package + devShell + nixosModules |
| `android/flake.nix` | Android SDK dev shell (separate from the root flake) |
## Gotchas
- `.gitignore` patterns here must be anchored. An unanchored `photogallery` also matches the Kotlin package directory `android/app/src/main/java/ws/inflo/photogallery/` and silently swallows every source file in it.
## What's not done yet
@@ -99,3 +145,5 @@ nix build # succeeds
- Multi-image upload
- Delete/edit via UI
- Any auth beyond nginx htpasswd
- The Android app has never been run on real hardware
- Photo-page swipe has never been tested on a real touchscreen; the iOS Safari edge-gesture interaction is the untested part