A new auth-gated web UI at /manage lists every post for inline editing of caption, date and tags, with per-row and bulk delete and bulk tag add/remove. It is backed by a small JSON API (all behind the same htpasswd Basic auth as /upload, and unlinked from the public site): GET /api/photos list posts (newest first) POST /api/edit rewrite one sidecar (caption, date, tags) POST /api/tags bulk add/remove tags across posts POST /api/delete move posts to content/.trash/ (reversible) GET /api/trash list trashed items POST /api/restore move a trashed item back (409 on live-slug clash) POST /api/trash/empty hard delete trashed items (id… or all=1) Design notes: - Editing only rewrites the .toml; the slug (and URL) never changes. - Delete is reversible: image + sidecar move to content/.trash/ with a timestamp suffix, excluded from the site (loadPhotos skips dirs). The only hard delete is emptying the trash. - Build now prunes: it drops the derived HTML trees (photo/ day/ album/ page/ grid/page/) and regenerates them, and pruneRenditions removes orphaned images/thumbs/medium/og. Without this a deleted post would linger at its old URL and on stale day/album pages; this also cleans up after a hand-removed sidecar. - Sidecars are written atomically (temp + rename) so a concurrent build never observes a half-written file. - Slugs and trash ids are validated as bare filenames, closing path traversal and dotfile access. - Every state-changing POST passes a sameOrigin (CSRF) check, since Basic-auth credentials are replayed by the browser on every request to the host. It honours Sec-Fetch-Site, falls back to Origin vs base-url/Host, and allows header-less non-browser clients. Ported from the independent /manage on plex/trunk-ng. module.nix proxies /manage and /api/ to the app (nginx otherwise serves them as missing static files). The apps are unchanged; the API is there for them to adopt edit/delete later.
photogallery
A minimal static photo gallery with an Atom feed and an upload API.
How it works
- Drop
photo.jpg+photo.tomlinto the content directory (or upload via the web UI at/upload). - The binary watches the directory and rebuilds the static site on any change.
- nginx serves
public/and proxies/uploadto the local API, secured with htpasswd.
For each photo the generator produces a 600×600 square thumbnail for the grid and an aspect-preserved medium JPEG (longest side 1600) for the detail page. The original file stays reachable from a download link on the detail page. Regeneration is skipped when the destination is at least as new as the source, so editing one sidecar doesn't reprocess every photo.
The upload page reads DateTimeOriginal from a JPEG's EXIF (or falls back to
the file's modification time) and prefills the date field, which stays
editable before submitting.
Sidecar format
caption = "Tempelhof at sunrise"
date = "2026-06-14"
If no .toml is present the filename is used as the caption and today's date as the date.
Using as a NixOS flake module
1. Add to your flake inputs
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
photogallery.url = "github:youruser/photogallery";
};
2. Import the module
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
modules = [
inputs.photogallery.nixosModules.default
./configuration.nix
];
};
3. Configure
services.photogallery = {
enable = true;
baseURL = "https://photos.yourdomain.tld";
# Optional: identity shown on the header and copyright footer
author = "Ada Lovelace";
handle = "@ada@example.com";
bio = "Photos from the field.";
bioAlt = ""; # optional secondary line in the alt-script font
# Optional: swap fonts
fontsURL = "https://fonts.googleapis.com/css2?family=EB+Garamond:wght@400;600;700&family=Manjari:wght@400;700&display=swap";
serifFamily = "EB Garamond";
mlFamily = "Manjari";
# Optional: built-in nginx vhost
nginx = {
enable = true;
domain = "photos.yourdomain.tld";
htpasswdFile = "/etc/nginx/.htpasswd-gallery";
};
};
Create the htpasswd file:
nix-shell -p apacheHttpd --run \
"htpasswd -c /etc/nginx/.htpasswd-gallery youruser"
Available options
| Option | Default | Description |
|---|---|---|
enable |
false |
Enable the service |
baseURL |
— | Public URL, no trailing slash |
contentDir |
/var/lib/photogallery/content |
Images and .toml sidecars |
outputDir |
/var/lib/photogallery/public |
Generated site output |
port |
8080 |
Upload API port (localhost only) |
author |
Ashik Salahudeen |
Name shown on the header, photo pages and copyright footer |
handle |
@puttaalu@inflo.ws |
Handle line under the author name |
bio |
Personal photo journal. |
Primary bio line |
bioAlt |
Malayalam default | Secondary bio line in the alt-script font ("" to omit) |
fontsURL |
Crimson Pro + Manjari | Stylesheet URL loading both web fonts |
serifFamily |
Crimson Pro |
CSS font-family for the primary serif |
mlFamily |
Manjari |
CSS font-family for the alt-script bio |
nginx.enable |
false |
Configure an nginx vhost |
nginx.domain |
— | Domain for the vhost |
nginx.htpasswdFile |
— | htpasswd file for /upload |
nginx.enableACME |
true |
Enable Let's Encrypt |
Bootstrap (first build)
buildGoModule needs a go.sum and the correct vendorHash.
# 1. Generate go.sum
go mod tidy
# 2. Commit go.sum
git add go.sum && git commit -m "add go.sum"
# 3. Get the vendorHash — this will fail and print the correct hash
nix build
# error: hash mismatch ... got: sha256-XXXXX...
# 4. Paste the hash into flake.nix, then build again
nix build
Uploading via curl
curl -u user:password \
-F "image=@photo.jpg" \
-F "caption=Tempelhof at sunrise" \
-F "date=2026-06-14" \
https://photos.yourdomain.tld/upload
Or open https://photos.yourdomain.tld/upload in a browser.