# photogallery 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 ``` content/ # images + .toml sidecars (source of truth) public/ # generated output (served by nginx / --serve flag) templates/ # embedded HTML templates static/ # embedded CSS + JS android/ # Kotlin/Compose uploader app, own flake ``` The binary does three things at once: - Watches `content/` via fsnotify and rebuilds on any change - Serves a multipart upload API at `POST /upload` - In dev mode (`--serve`), also serves `public/` as static files 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** — 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` ## Sidecar format ```toml caption = "Tempelhof at sunrise" date = "2026-06-14" ``` Missing sidecar → filename as caption, today as date. Photos sort newest first by that date. ## Design - Gruvbox dark hard colour scheme - Crimson Pro (serif) for headings and captions — configurable via `-serif-family` / `-fonts-url` - Manjari for Malayalam text (bio line), via `-ml-family` - Two views of the same photos, both 3-column square grid → 2-column on mobile, both with infinite-scroll load-more. Page size is configurable via `-page-size` (default 60; NixOS `services.photogallery.pageSize`): - **Timeline is the site root** (`/`, `/page/2/`, …). Photos grouped by calendar day: one ISO date heading (`2026-06-14`) + count per day, then the square grid. `groupByDay` collapses the newest-first list into consecutive same-day runs; `chunkDayGroups` packs whole days into pages targeting `pageSize`, never splitting a day across a page boundary (so appended pages never duplicate a heading — the load-more JS relies on this). - **Grid** (`/grid/`, `/grid/page/2/`, …) is the flat square grid, linked from the profile header. - Each timeline date heading links to a **single-day page** (`/day//`) holding just that day's photos. `renderDayPages` writes one per `groupByDay` run; the ISO label doubles as the URL slug. - 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 `` 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 ```bash nix develop go mod tidy go run . --serve # gallery: http://localhost:8080 # 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 inputs.photogallery.url = "github:youruser/photogallery"; # in modules: inputs.photogallery.nixosModules.default # in configuration.nix: services.photogallery = { enable = true; baseURL = "https://photos.yourdomain.tld"; nginx = { enable = true; domain = "photos.yourdomain.tld"; htpasswdFile = "/etc/nginx/.htpasswd-gallery"; }; }; ``` ## Bootstrap (first nix build) ```bash go mod tidy # generates go.sum — commit it nix build # fails, prints correct vendorHash # paste hash into flake.nix nix build # succeeds ``` ## Key files | File | Purpose | |---|---| | `main.go` | Flags, wires generator + watcher + server | | `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/timeline.html` | Site root: photos grouped by day, paginated | | `templates/grid.html` | Flat square grid at `/grid/`, paginated | | `templates/day.html` | Single-day page at `/day//` | | `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 - ActivityPub federation (planned later — start with RSS/Atom) - 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