Files
photog/module.nix
T
puttaalu 51b082ccf4 Make profile identity, bio and fonts configurable
The profile header, photo page byline and copyright footer previously
hardcoded the author name, handle, bio, and font choices. Move all of
these to CLI flags on the binary and matching options on the NixOS
module so the same build can serve any single-user gallery.

New surface, all with defaults matching the previous hardcoded values:
  --author       / services.photogallery.author
  --handle       / services.photogallery.handle
  --bio          / services.photogallery.bio
  --bio-alt      / services.photogallery.bioAlt   ("" to omit)
  --fonts-url    / services.photogallery.fontsURL
  --serif-family / services.photogallery.serifFamily
  --ml-family    / services.photogallery.mlFamily

Templates read the two font families from a small inline <style> that
overrides the :root variables, so the stylesheet stays fully static and
the font URL is loaded via the configured stylesheet link.

README updated to describe the new options, the medium/full-size photo
detail flow, and the EXIF-based date prefill on the upload page.
2026-07-02 02:30:35 +02:00

186 lines
5.9 KiB
Nix

self:
{ config, lib, pkgs, ... }:
let
cfg = config.services.photogallery;
pkg = self.packages.${pkgs.system}.default;
in {
options.services.photogallery = {
enable = lib.mkEnableOption "photogallery static photo gallery";
contentDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/photogallery/content";
description = "Directory containing images and .toml sidecars.";
};
outputDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/photogallery/public";
description = "Directory for the generated static site.";
};
baseURL = lib.mkOption {
type = lib.types.str;
example = "https://photos.example.com";
description = "Public base URL, no trailing slash.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Upload API port. Always bound to 127.0.0.1.";
};
author = lib.mkOption {
type = lib.types.str;
default = "Ashik Salahudeen";
description = "Author name shown on the profile header, photo pages and in the copyright footer.";
};
handle = lib.mkOption {
type = lib.types.str;
default = "@puttaalu@inflo.ws";
description = "Handle line shown under the author name on the profile header.";
};
bio = lib.mkOption {
type = lib.types.str;
default = "Personal photo journal.";
description = "Primary bio line shown on the profile header.";
};
bioAlt = lib.mkOption {
type = lib.types.str;
default = "ി ി ി ";
description = "Secondary bio line rendered on a new line in the alt-script font. Set to \"\" to omit.";
};
fontsURL = lib.mkOption {
type = lib.types.str;
default = "https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,400;0,600;0,700;1,400&family=Manjari:wght@100;400;700&display=swap";
description = "Stylesheet URL that loads the primary serif font and the alt-script font.";
};
serifFamily = lib.mkOption {
type = lib.types.str;
default = "Crimson Pro";
description = "CSS font-family for the primary serif text. Must match a family loaded by fontsURL.";
};
mlFamily = lib.mkOption {
type = lib.types.str;
default = "Manjari";
description = "CSS font-family for the alt-script (Malayalam) bio line. Must match a family loaded by fontsURL.";
};
nginx = {
enable = lib.mkEnableOption "nginx virtual host for photogallery";
domain = lib.mkOption {
type = lib.types.str;
example = "photos.example.com";
description = "Domain for the nginx virtual host.";
};
htpasswdFile = lib.mkOption {
type = lib.types.path;
example = "/etc/nginx/.htpasswd-gallery";
description = "Path to htpasswd file protecting the /upload endpoint.";
};
enableACME = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable ACME/Let's Encrypt for TLS.";
};
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [
# ── Core service ──────────────────────────────────────────────────────
{
users.users.photogallery = {
isSystemUser = true;
group = "photogallery";
home = "/var/lib/photogallery";
createHome = true;
description = "photogallery service user";
};
users.groups.photogallery = {};
systemd.tmpfiles.rules = [
"d ${cfg.contentDir} 0755 photogallery photogallery -"
"d ${cfg.outputDir} 0755 photogallery photogallery -"
];
systemd.services.photogallery = {
description = "photogallery static site generator";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "photogallery";
Group = "photogallery";
ExecStart = lib.escapeShellArgs [
"${pkg}/bin/photogallery"
"--content" cfg.contentDir
"--output" cfg.outputDir
"--base-url" cfg.baseURL
"--port" (toString cfg.port)
"--author" cfg.author
"--handle" cfg.handle
"--bio" cfg.bio
"--bio-alt" cfg.bioAlt
"--fonts-url" cfg.fontsURL
"--serif-family" cfg.serifFamily
"--ml-family" cfg.mlFamily
];
Restart = "on-failure";
RestartSec = "5s";
# Hardening
NoNewPrivileges = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ cfg.contentDir cfg.outputDir ];
};
};
}
# ── Optional nginx vhost ──────────────────────────────────────────────
(lib.mkIf cfg.nginx.enable {
services.nginx = {
enable = true;
virtualHosts.${cfg.nginx.domain} = {
enableACME = cfg.nginx.enableACME;
forceSSL = cfg.nginx.enableACME;
root = cfg.outputDir;
locations."/" = {
tryFiles = "$uri $uri/ =404";
};
locations."/upload" = {
proxyPass = "http://127.0.0.1:${toString cfg.port}";
basicAuth = "gallery";
basicAuthFile = cfg.nginx.htpasswdFile;
extraConfig = "client_max_body_size 20M;";
};
};
};
})
]);
}