Files
photog/CLAUDE.md
T
puttaalu c169c030d6 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.
2026-08-19 23:36:14 +02:00

5.9 KiB
Raw Blame History

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
  • Templateshtml/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)
  • NixOSflake.nix + module.nix, exports nixosModules.default

Sidecar format

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
  • 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

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:

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

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)

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/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