diff --git a/README.md b/README.md index be96289..b10bcab 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,16 @@ A minimal static photo gallery with an Atom feed and an upload API. 2. The binary watches the directory and rebuilds the static site on any change. 3. nginx serves `public/` and proxies `/upload` to 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 ```toml @@ -48,6 +58,17 @@ 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; @@ -73,6 +94,13 @@ nix-shell -p apacheHttpd --run \ | `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` | diff --git a/generator.go b/generator.go index bc02bc8..cb63186 100644 --- a/generator.go +++ b/generator.go @@ -3,8 +3,8 @@ package main import ( "encoding/xml" "html/template" - imagedraw "image/draw" "image" + imagedraw "image/draw" "image/jpeg" _ "image/png" "io" @@ -42,23 +42,44 @@ type Photo struct { } type Generator struct { - contentDir string - outputDir string - baseURL string - mu sync.Mutex - tmpl *template.Template + contentDir string + outputDir string + baseURL string + author string + handle string + bio string + bioAlt string + fontsURL string + serifFamily string + mlFamily string + mu sync.Mutex + tmpl *template.Template } -func NewGenerator(contentDir, outputDir, baseURL string) *Generator { +func NewGenerator(contentDir, outputDir, baseURL, author, handle, bio, bioAlt, fontsURL, serifFamily, mlFamily string) *Generator { tmpl := template.Must(template.ParseFS(assets, "templates/*.html")) return &Generator{ - contentDir: contentDir, - outputDir: outputDir, - baseURL: baseURL, - tmpl: tmpl, + contentDir: contentDir, + outputDir: outputDir, + baseURL: baseURL, + author: author, + handle: handle, + bio: bio, + bioAlt: bioAlt, + fontsURL: fontsURL, + serifFamily: serifFamily, + mlFamily: mlFamily, + tmpl: tmpl, } } +func (g *Generator) initial() string { + for _, r := range g.author { + return strings.ToUpper(string(r)) + } + return "" +} + // Build performs a full rebuild of the site. It is safe to call concurrently; // concurrent calls are serialised by a mutex. func (g *Generator) Build() error { @@ -157,9 +178,9 @@ func (g *Generator) loadPhotos() ([]Photo, error) { // thumbnail to public/thumbs/ plus an aspect-preserved medium image to // public/medium/. Outputs are skipped when already newer than the source. func (g *Generator) processImage(p *Photo) error { - src := filepath.Join(g.contentDir, filepath.Base(p.File)) - dstImg := filepath.Join(g.outputDir, p.File) - dstThumb := filepath.Join(g.outputDir, p.Thumb) + src := filepath.Join(g.contentDir, filepath.Base(p.File)) + dstImg := filepath.Join(g.outputDir, p.File) + dstThumb := filepath.Join(g.outputDir, p.Thumb) dstMedium := filepath.Join(g.outputDir, p.Medium) if !upToDate(src, dstImg) { @@ -272,9 +293,17 @@ func generateMedium(src, dst string, maxDim int) error { // renderIndex writes public/index.html. func (g *Generator) renderIndex(photos []Photo) error { return g.render("index.html", filepath.Join(g.outputDir, "index.html"), map[string]any{ - "Photos": photos, - "BaseURL": g.baseURL, - "Year": time.Now().Year(), + "Photos": photos, + "BaseURL": g.baseURL, + "Year": time.Now().Year(), + "Author": g.author, + "Handle": g.handle, + "Bio": g.bio, + "BioAlt": g.bioAlt, + "Initial": g.initial(), + "FontsURL": g.fontsURL, + "SerifFamily": g.serifFamily, + "MlFamily": g.mlFamily, }) } @@ -285,9 +314,15 @@ func (g *Generator) renderPhotoPage(p Photo) error { return err } return g.render("photo.html", filepath.Join(dir, "index.html"), map[string]any{ - "Photo": p, - "BaseURL": g.baseURL, - "Year": time.Now().Year(), + "Photo": p, + "BaseURL": g.baseURL, + "Year": time.Now().Year(), + "Author": g.author, + "Handle": g.handle, + "Initial": g.initial(), + "FontsURL": g.fontsURL, + "SerifFamily": g.serifFamily, + "MlFamily": g.mlFamily, }) } diff --git a/main.go b/main.go index c967c5a..c2480d2 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,13 @@ func main() { baseURL := flag.String("base-url", "http://localhost:8080", "public base URL, no trailing slash") port := flag.String("port", "8080", "port to listen on") serve := flag.Bool("serve", false, "serve the generated site on the same port (for local development)") + author := flag.String("author", "Ashik Salahudeen", "author name shown on the profile header, photo pages and in the copyright footer") + handle := flag.String("handle", "@puttaalu@inflo.ws", "handle line shown under the author name on the profile header") + bio := flag.String("bio", "Personal photo journal.", "primary bio line shown on the profile header") + bioAlt := flag.String("bio-alt", "എന്തിനോ വേണ്ടി തിളയ്ക്കുന്ന സാമ്പാർ", "secondary bio line rendered on a new line in the alt-script font") + fontsURL := flag.String("fonts-url", "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", "stylesheet URL loading the two web fonts") + serifFamily := flag.String("serif-family", "Crimson Pro", "CSS font-family for the primary serif text") + mlFamily := flag.String("ml-family", "Manjari", "CSS font-family for the alt-script (Malayalam) bio line") flag.Parse() for _, d := range []string{*contentDir, *outputDir} { @@ -20,7 +27,7 @@ func main() { } } - g := NewGenerator(*contentDir, *outputDir, *baseURL) + g := NewGenerator(*contentDir, *outputDir, *baseURL, *author, *handle, *bio, *bioAlt, *fontsURL, *serifFamily, *mlFamily) if err := g.Build(); err != nil { log.Printf("initial build: %v", err) diff --git a/module.nix b/module.nix index 84283dd..b43989f 100644 --- a/module.nix +++ b/module.nix @@ -34,6 +34,48 @@ in { 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"; @@ -92,6 +134,13 @@ in { "--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"; diff --git a/templates/index.html b/templates/index.html index 40a3363..9af3214 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,21 +4,22 @@ Photo Gallery - + +
- A + {{.Initial}}
-
A
- Ashik Salahudeen +
{{.Initial}}
+ {{.Author}}

{{.Photo.Caption}}

{{.Photo.Date.Format "Jan 02, 2006"}}

@@ -30,7 +31,7 @@