Add albums: group photos by free-text tags

An album is a tag. Photos carry lowercase free-text tags in their .toml
sidecar (`tags = [...]`), parallel to the existing day grouping — no
database, all derived at build time.

- albums.go: parseTags (lowercase/trim/dedupe, slug via sanitiseSlug),
  groupByTag mirroring groupByDay, and rendering for album pages, the
  index, and the JSON API. Slug collisions between distinct names are
  suffixed and logged.
- Routes: /album/<slug>/ (paginated square grid per tag), /albums/ (index
  with counts), /tags.json (machine-readable tag list, served statically
  like feed.xml).
- POST /upload gains a `tags` field written into the sidecar; the web
  upload form gains a tags input.
- Tags shown on each photo page linking to their album; an Albums link in
  the timeline/grid header.
This commit is contained in:
puttaalu committed 2026-10-08 16:14:09 +02:00
1 parent f33bdd55b1
commit a42abfab48
11 files changed
+469 -6

No files matched your search

+16 -3
View File
@@ -41,8 +41,9 @@ var ogQualityLadder = []int{85, 78, 70, 62}
// PhotoMeta is the structure of a .toml sidecar file.
type PhotoMeta struct {
Caption string `toml:"caption"`
Date string `toml:"date"` // YYYY-MM-DD
Caption string `toml:"caption"`
Date string `toml:"date"` // YYYY-MM-DD
Tags []string `toml:"tags,omitempty"` // free-text, lowercased; each tag is an album
}
// Photo is the in-memory representation of one image.
@@ -56,6 +57,7 @@ type Photo struct {
OGHeight int
Caption string
Date time.Time
Tags []Tag // albums this photo belongs to
}
// DayGroup is a run of photos sharing the same calendar day, for the timeline.
@@ -121,7 +123,7 @@ func (g *Generator) Build() error {
return err
}
for _, d := range []string{"images", "thumbs", "medium", "og", "photo", "grid", "day", "static"} {
for _, d := range []string{"images", "thumbs", "medium", "og", "photo", "grid", "day", "album", "albums", "static"} {
if err := os.MkdirAll(filepath.Join(g.outputDir, d), 0755); err != nil {
return err
}
@@ -142,6 +144,16 @@ func (g *Generator) Build() error {
if err := g.renderDayPages(photos); err != nil {
return err
}
albums := groupByTag(photos)
if err := g.renderAlbumPages(albums, len(photos)); err != nil {
return err
}
if err := g.renderAlbumsIndex(albums, len(photos)); err != nil {
return err
}
if err := g.renderTagsJSON(albums); err != nil {
return err
}
for i := range photos {
var newer, older *Photo
if i > 0 {
@@ -209,6 +221,7 @@ func (g *Generator) loadPhotos() ([]Photo, error) {
OG: "og/" + slug + ".jpg",
Caption: meta.Caption,
Date: date,
Tags: parseTags(meta.Tags...),
})
}