feat(generator): add -default-view to choose timeline or grid as site root
The chosen view is published at /, the other under /timeline/ or /grid/. Page paths, pager links and the cross-links between the views are derived from the setting. The directory named after the default view is removed on build, since it can only be a leftover from before a switch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -55,9 +55,10 @@ Missing sidecar → filename as caption, today as date. Photos sort newest first
|
|||||||
- Crimson Pro (serif) for headings and captions — configurable via `-serif-family` / `-fonts-url`
|
- Crimson Pro (serif) for headings and captions — configurable via `-serif-family` / `-fonts-url`
|
||||||
- Manjari for Malayalam text (bio line), via `-ml-family`
|
- Manjari for Malayalam text (bio line), via `-ml-family`
|
||||||
- Two views of the same photos, both 3-column square grid → 2-column on mobile, both with infinite-scroll load-more. Page size is configurable via `-page-size` (default 60; NixOS `services.photogallery.pageSize`):
|
- Two views of the same photos, both 3-column square grid → 2-column on mobile, both with infinite-scroll load-more. Page size is configurable via `-page-size` (default 60; NixOS `services.photogallery.pageSize`):
|
||||||
- **Timeline is the site root** (`/`, `/page/2/`, …). Photos grouped by calendar day: one ISO date heading (`2026-06-14`) + count per day, then the square grid. `groupByDay` collapses the newest-first list into consecutive same-day runs; `chunkDayGroups` packs whole days into pages targeting `pageSize`, never splitting a day across a page boundary (so appended pages never duplicate a heading — the load-more JS relies on this).
|
- **Timeline is the site root by default** (`/`, `/page/2/`, …). Photos grouped by calendar day: one ISO date heading (`2026-06-14`) + count per day, then the square grid. `groupByDay` collapses the newest-first list into consecutive same-day runs; `chunkDayGroups` packs whole days into pages targeting `pageSize`, never splitting a day across a page boundary (so appended pages never duplicate a heading — the load-more JS relies on this).
|
||||||
- **Grid** (`/grid/`, `/grid/page/2/`, …) is the flat square grid, linked from the profile header.
|
- **Grid** (`/grid/`, `/grid/page/2/`, …) is the flat square grid, linked from the profile header.
|
||||||
- Each timeline date heading links to a **single-day page** (`/day/<YYYY-MM-DD>/`) holding just that day's photos. `renderDayPages` writes one per `groupByDay` run; the ISO label doubles as the URL slug.
|
- Each timeline date heading links to a **single-day page** (`/day/<YYYY-MM-DD>/`) holding just that day's photos. `renderDayPages` writes one per `groupByDay` run; the ISO label doubles as the URL slug.
|
||||||
|
- Which view owns the site root is set by `-default-view timeline|grid` (NixOS `services.photogallery.defaultView`). The other view moves under `/<name>/`: with `-default-view grid` the grid is `/` and the timeline is `/timeline/`. `viewBase` and `pageURL` in `generator.go` derive every page path and pager link from that, and the templates take the cross-links as `GridURL` / `TimelineURL` — never hardcode `/grid/` or `/`. On each build the directory named after the default view is removed, since it can only be a leftover from before a switch. Root-level `/page/N/` beyond the new page count is *not* pruned.
|
||||||
- Single photo page with sidebar and newer/older navigation
|
- Single photo page with sidebar and newer/older navigation
|
||||||
|
|
||||||
### Link previews
|
### Link previews
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ nix-shell -p apacheHttpd --run \
|
|||||||
| `fontsURL` | Crimson Pro + Manjari | Stylesheet URL loading both web fonts |
|
| `fontsURL` | Crimson Pro + Manjari | Stylesheet URL loading both web fonts |
|
||||||
| `serifFamily` | `Crimson Pro` | CSS font-family for the primary serif |
|
| `serifFamily` | `Crimson Pro` | CSS font-family for the primary serif |
|
||||||
| `mlFamily` | `Manjari` | CSS font-family for the alt-script bio |
|
| `mlFamily` | `Manjari` | CSS font-family for the alt-script bio |
|
||||||
|
| `pageSize` | `60` | Photos per page on the timeline and grid views |
|
||||||
|
| `defaultView` | `timeline` | View at the site root, `timeline` or `grid`; the other is served under `/timeline/` or `/grid/` |
|
||||||
| `nginx.enable` | `false` | Configure an nginx vhost |
|
| `nginx.enable` | `false` | Configure an nginx vhost |
|
||||||
| `nginx.domain` | — | Domain for the vhost |
|
| `nginx.domain` | — | Domain for the vhost |
|
||||||
| `nginx.htpasswdFile` | — | htpasswd file for `/upload` and `/manage` |
|
| `nginx.htpasswdFile` | — | htpasswd file for `/upload` and `/manage` |
|
||||||
|
|||||||
+56
-33
@@ -27,6 +27,11 @@ const (
|
|||||||
mediumMaxSize = 1600
|
mediumMaxSize = 1600
|
||||||
defaultPageSize = 60
|
defaultPageSize = 60
|
||||||
|
|
||||||
|
// The two paginated views. The one chosen as -default-view is published
|
||||||
|
// at the site root, the other under /<name>/.
|
||||||
|
viewTimeline = "timeline"
|
||||||
|
viewGrid = "grid"
|
||||||
|
|
||||||
// Open Graph preview image, shared to WhatsApp/Telegram/etc. Kept to a
|
// Open Graph preview image, shared to WhatsApp/Telegram/etc. Kept to a
|
||||||
// 1200px longest side and squeezed under ogTargetBytes: WhatsApp is widely
|
// 1200px longest side and squeezed under ogTargetBytes: WhatsApp is widely
|
||||||
// reported to drop the preview entirely once the image passes ~300 KB, so
|
// reported to drop the preview entirely once the image passes ~300 KB, so
|
||||||
@@ -78,15 +83,19 @@ type Generator struct {
|
|||||||
serifFamily string
|
serifFamily string
|
||||||
mlFamily string
|
mlFamily string
|
||||||
pageSize int
|
pageSize int
|
||||||
|
defaultView string
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
tmpl *template.Template
|
tmpl *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGenerator(contentDir, outputDir, baseURL, author, handle, bio, bioAlt, fontsURL, serifFamily, mlFamily string, pageSize int) *Generator {
|
func NewGenerator(contentDir, outputDir, baseURL, author, handle, bio, bioAlt, fontsURL, serifFamily, mlFamily string, pageSize int, defaultView string) *Generator {
|
||||||
tmpl := template.Must(template.ParseFS(assets, "templates/*.html"))
|
tmpl := template.Must(template.ParseFS(assets, "templates/*.html"))
|
||||||
if pageSize < 1 {
|
if pageSize < 1 {
|
||||||
pageSize = defaultPageSize
|
pageSize = defaultPageSize
|
||||||
}
|
}
|
||||||
|
if defaultView != viewGrid {
|
||||||
|
defaultView = viewTimeline
|
||||||
|
}
|
||||||
return &Generator{
|
return &Generator{
|
||||||
contentDir: contentDir,
|
contentDir: contentDir,
|
||||||
outputDir: outputDir,
|
outputDir: outputDir,
|
||||||
@@ -99,6 +108,7 @@ func NewGenerator(contentDir, outputDir, baseURL, author, handle, bio, bioAlt, f
|
|||||||
serifFamily: serifFamily,
|
serifFamily: serifFamily,
|
||||||
mlFamily: mlFamily,
|
mlFamily: mlFamily,
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
|
defaultView: defaultView,
|
||||||
tmpl: tmpl,
|
tmpl: tmpl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,6 +120,23 @@ func (g *Generator) initial() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// viewBase returns the URL prefix a view is published under: "" for the
|
||||||
|
// default view, which owns the site root, and "/<view>" for the other one.
|
||||||
|
func (g *Generator) viewBase(view string) string {
|
||||||
|
if view == g.defaultView {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "/" + view
|
||||||
|
}
|
||||||
|
|
||||||
|
// pageURL returns the URL of the 1-based page n of a view rooted at base.
|
||||||
|
func pageURL(base string, n int) string {
|
||||||
|
if n == 1 {
|
||||||
|
return base + "/"
|
||||||
|
}
|
||||||
|
return base + "/page/" + strconv.Itoa(n) + "/"
|
||||||
|
}
|
||||||
|
|
||||||
// Build performs a full rebuild of the site. It is safe to call concurrently;
|
// Build performs a full rebuild of the site. It is safe to call concurrently;
|
||||||
// concurrent calls are serialised by a mutex.
|
// concurrent calls are serialised by a mutex.
|
||||||
func (g *Generator) Build() error {
|
func (g *Generator) Build() error {
|
||||||
@@ -121,7 +148,7 @@ func (g *Generator) Build() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range []string{"images", "thumbs", "medium", "og", "photo", "grid", "day", "static"} {
|
for _, d := range []string{"images", "thumbs", "medium", "og", "photo", "day", "static"} {
|
||||||
if err := os.MkdirAll(filepath.Join(g.outputDir, d), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Join(g.outputDir, d), 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -133,6 +160,13 @@ func (g *Generator) Build() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The default view lives at the site root, so a directory named after it
|
||||||
|
// can only be left over from a run with the other -default-view. Drop it,
|
||||||
|
// or the old pages would stay reachable and go stale.
|
||||||
|
if err := os.RemoveAll(filepath.Join(g.outputDir, g.defaultView)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := g.renderTimelinePages(photos); err != nil {
|
if err := g.renderTimelinePages(photos); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -417,7 +451,7 @@ func (g *Generator) renderGridPages(photos []Photo) error {
|
|||||||
if totalPages == 0 {
|
if totalPages == 0 {
|
||||||
totalPages = 1
|
totalPages = 1
|
||||||
}
|
}
|
||||||
gridDir := filepath.Join(g.outputDir, "grid")
|
base := g.viewBase(viewGrid)
|
||||||
|
|
||||||
for i := 0; i < totalPages; i++ {
|
for i := 0; i < totalPages; i++ {
|
||||||
start := i * g.pageSize
|
start := i * g.pageSize
|
||||||
@@ -427,31 +461,24 @@ func (g *Generator) renderGridPages(photos []Photo) error {
|
|||||||
}
|
}
|
||||||
slice := photos[start:end]
|
slice := photos[start:end]
|
||||||
|
|
||||||
dst := filepath.Join(gridDir, "index.html")
|
dir := filepath.Join(g.outputDir, filepath.FromSlash(pageURL(base, i+1)))
|
||||||
if i > 0 {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
dir := filepath.Join(gridDir, "page", strconv.Itoa(i+1))
|
return err
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dst = filepath.Join(dir, "index.html")
|
|
||||||
}
|
}
|
||||||
|
dst := filepath.Join(dir, "index.html")
|
||||||
|
|
||||||
prev := ""
|
prev := ""
|
||||||
next := ""
|
next := ""
|
||||||
switch i {
|
if i > 0 {
|
||||||
case 0:
|
prev = pageURL(base, i)
|
||||||
// no prev
|
|
||||||
case 1:
|
|
||||||
prev = "/grid/"
|
|
||||||
default:
|
|
||||||
prev = "/grid/page/" + strconv.Itoa(i) + "/"
|
|
||||||
}
|
}
|
||||||
if i+1 < totalPages {
|
if i+1 < totalPages {
|
||||||
next = "/grid/page/" + strconv.Itoa(i+2) + "/"
|
next = pageURL(base, i+2)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"Photos": slice,
|
"Photos": slice,
|
||||||
|
"TimelineURL": pageURL(g.viewBase(viewTimeline), 1),
|
||||||
"Total": total,
|
"Total": total,
|
||||||
"Page": i + 1,
|
"Page": i + 1,
|
||||||
"TotalPages": totalPages,
|
"TotalPages": totalPages,
|
||||||
@@ -551,32 +578,27 @@ func (g *Generator) renderTimelinePages(photos []Photo) error {
|
|||||||
pages := chunkDayGroups(groupByDay(photos), g.pageSize)
|
pages := chunkDayGroups(groupByDay(photos), g.pageSize)
|
||||||
totalPages := len(pages)
|
totalPages := len(pages)
|
||||||
|
|
||||||
|
base := g.viewBase(viewTimeline)
|
||||||
|
|
||||||
for i, groups := range pages {
|
for i, groups := range pages {
|
||||||
dst := filepath.Join(g.outputDir, "index.html")
|
dir := filepath.Join(g.outputDir, filepath.FromSlash(pageURL(base, i+1)))
|
||||||
if i > 0 {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
dir := filepath.Join(g.outputDir, "page", strconv.Itoa(i+1))
|
return err
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dst = filepath.Join(dir, "index.html")
|
|
||||||
}
|
}
|
||||||
|
dst := filepath.Join(dir, "index.html")
|
||||||
|
|
||||||
prev := ""
|
prev := ""
|
||||||
next := ""
|
next := ""
|
||||||
switch i {
|
if i > 0 {
|
||||||
case 0:
|
prev = pageURL(base, i)
|
||||||
// no prev
|
|
||||||
case 1:
|
|
||||||
prev = "/"
|
|
||||||
default:
|
|
||||||
prev = "/page/" + strconv.Itoa(i) + "/"
|
|
||||||
}
|
}
|
||||||
if i+1 < totalPages {
|
if i+1 < totalPages {
|
||||||
next = "/page/" + strconv.Itoa(i+2) + "/"
|
next = pageURL(base, i+2)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"Groups": groups,
|
"Groups": groups,
|
||||||
|
"GridURL": pageURL(g.viewBase(viewGrid), 1),
|
||||||
"Total": len(photos),
|
"Total": len(photos),
|
||||||
"Page": i + 1,
|
"Page": i + 1,
|
||||||
"TotalPages": totalPages,
|
"TotalPages": totalPages,
|
||||||
@@ -610,6 +632,7 @@ func (g *Generator) renderDayPages(photos []Photo) error {
|
|||||||
}
|
}
|
||||||
err := g.render("day.html", filepath.Join(dir, "index.html"), map[string]any{
|
err := g.render("day.html", filepath.Join(dir, "index.html"), map[string]any{
|
||||||
"Date": grp.Label,
|
"Date": grp.Label,
|
||||||
|
"TimelineURL": pageURL(g.viewBase(viewTimeline), 1),
|
||||||
"Count": grp.Count,
|
"Count": grp.Count,
|
||||||
"Photos": grp.Photos,
|
"Photos": grp.Photos,
|
||||||
"Total": len(photos),
|
"Total": len(photos),
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// buildSite builds a site of three photos spread over two days, with a page
|
||||||
|
// size of two so both views need a second page, and returns the output dir.
|
||||||
|
func buildSite(t *testing.T, output, defaultView string) {
|
||||||
|
t.Helper()
|
||||||
|
content := t.TempDir()
|
||||||
|
|
||||||
|
var img bytes.Buffer
|
||||||
|
if err := jpeg.Encode(&img, image.NewRGBA(image.Rect(0, 0, 8, 8)), nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for slug, date := range map[string]string{
|
||||||
|
"one": "2026-06-14",
|
||||||
|
"two": "2026-06-14",
|
||||||
|
"three": "2026-06-10",
|
||||||
|
} {
|
||||||
|
writeFile(t, filepath.Join(content, slug+".jpg"), img.Bytes())
|
||||||
|
writeFile(t, filepath.Join(content, slug+".toml"),
|
||||||
|
[]byte("caption = \""+slug+"\"\ndate = \""+date+"\"\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
g := NewGenerator(content, output, "https://photos.example.com",
|
||||||
|
"Ada", "", "", "", "", "Crimson Pro", "Manjari", 2, defaultView)
|
||||||
|
if err := g.Build(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPage(t *testing.T, output, rel string) string {
|
||||||
|
t.Helper()
|
||||||
|
data, err := os.ReadFile(filepath.Join(output, filepath.FromSlash(rel), "index.html"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeline pages carry day sections, grid pages do not.
|
||||||
|
func isTimeline(page string) bool {
|
||||||
|
return strings.Contains(page, `class="tl-day"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultViewOwnsSiteRoot(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
defaultView string
|
||||||
|
gridAt string // URL of the grid's first page
|
||||||
|
timelineAt string // URL of the timeline's first page
|
||||||
|
}{
|
||||||
|
{viewTimeline, "/grid/", "/"},
|
||||||
|
{viewGrid, "/", "/timeline/"},
|
||||||
|
{"nonsense", "/grid/", "/"}, // unknown values fall back to the timeline
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.defaultView, func(t *testing.T) {
|
||||||
|
output := t.TempDir()
|
||||||
|
buildSite(t, output, tt.defaultView)
|
||||||
|
|
||||||
|
timeline := readPage(t, output, tt.timelineAt)
|
||||||
|
grid := readPage(t, output, tt.gridAt)
|
||||||
|
if !isTimeline(timeline) {
|
||||||
|
t.Errorf("%s is not the timeline", tt.timelineAt)
|
||||||
|
}
|
||||||
|
if isTimeline(grid) {
|
||||||
|
t.Errorf("%s is not the grid", tt.gridAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The views link to each other, and to their own second page.
|
||||||
|
for page, want := range map[string]string{
|
||||||
|
timeline: `href="` + tt.gridAt + `" class="rss-a">Grid`,
|
||||||
|
grid: `href="` + tt.timelineAt + `" class="rss-a">Timeline`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(page, want) {
|
||||||
|
t.Errorf("missing link %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for first, base := range map[string]string{
|
||||||
|
timeline: strings.TrimSuffix(tt.timelineAt, "/"),
|
||||||
|
grid: strings.TrimSuffix(tt.gridAt, "/"),
|
||||||
|
} {
|
||||||
|
next := base + "/page/2/"
|
||||||
|
if !strings.Contains(first, `href="`+next+`" data-load-more`) {
|
||||||
|
t.Errorf("first page does not link to %s", next)
|
||||||
|
}
|
||||||
|
second := readPage(t, output, next)
|
||||||
|
if !strings.Contains(second, `class="pager-newer" href="`+base+`/"`) {
|
||||||
|
t.Errorf("%s does not link back to %s/", next, base)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Day pages lead back to the timeline, wherever it lives.
|
||||||
|
day := readPage(t, output, "/day/2026-06-14/")
|
||||||
|
if !strings.Contains(day, `href="`+tt.timelineAt+`" class="rss-a">← Timeline`) {
|
||||||
|
t.Errorf("day page does not link to the timeline at %s", tt.timelineAt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchingDefaultViewDropsOldViewDir(t *testing.T) {
|
||||||
|
output := t.TempDir()
|
||||||
|
|
||||||
|
buildSite(t, output, viewTimeline)
|
||||||
|
if _, err := os.Stat(filepath.Join(output, "grid", "index.html")); err != nil {
|
||||||
|
t.Fatalf("grid view missing before the switch: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSite(t, output, viewGrid)
|
||||||
|
if _, err := os.Stat(filepath.Join(output, "grid")); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("stale /grid/ survived the switch to -default-view grid")
|
||||||
|
}
|
||||||
|
if isTimeline(readPage(t, output, "/")) {
|
||||||
|
t.Errorf("site root is still the timeline")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,15 +20,20 @@ func main() {
|
|||||||
serifFamily := flag.String("serif-family", "Crimson Pro", "CSS font-family for the primary serif text")
|
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")
|
mlFamily := flag.String("ml-family", "Manjari", "CSS font-family for the alt-script (Malayalam) bio line")
|
||||||
pageSize := flag.Int("page-size", 60, "photos per page on the timeline and grid views (timeline packs whole days, so pages vary slightly around this)")
|
pageSize := flag.Int("page-size", 60, "photos per page on the timeline and grid views (timeline packs whole days, so pages vary slightly around this)")
|
||||||
|
defaultView := flag.String("default-view", viewTimeline, "view published at the site root: \"timeline\" or \"grid\"; the other one is published under /<name>/")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if *defaultView != viewTimeline && *defaultView != viewGrid {
|
||||||
|
log.Fatalf("-default-view must be %q or %q, got %q", viewTimeline, viewGrid, *defaultView)
|
||||||
|
}
|
||||||
|
|
||||||
for _, d := range []string{*contentDir, *outputDir} {
|
for _, d := range []string{*contentDir, *outputDir} {
|
||||||
if err := os.MkdirAll(d, 0755); err != nil {
|
if err := os.MkdirAll(d, 0755); err != nil {
|
||||||
log.Fatalf("mkdir %s: %v", d, err)
|
log.Fatalf("mkdir %s: %v", d, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
g := NewGenerator(*contentDir, *outputDir, *baseURL, *author, *handle, *bio, *bioAlt, *fontsURL, *serifFamily, *mlFamily, *pageSize)
|
g := NewGenerator(*contentDir, *outputDir, *baseURL, *author, *handle, *bio, *bioAlt, *fontsURL, *serifFamily, *mlFamily, *pageSize, *defaultView)
|
||||||
|
|
||||||
if err := g.Build(); err != nil {
|
if err := g.Build(); err != nil {
|
||||||
log.Printf("initial build: %v", err)
|
log.Printf("initial build: %v", err)
|
||||||
|
|||||||
+1
-1
@@ -46,7 +46,7 @@ func newTestServer(t *testing.T) *Server {
|
|||||||
[]byte("caption = \"Tempelhof at sunrise\"\ndate = \"2026-06-14\"\n"))
|
[]byte("caption = \"Tempelhof at sunrise\"\ndate = \"2026-06-14\"\n"))
|
||||||
|
|
||||||
g := NewGenerator(content, output, "https://photos.example.com",
|
g := NewGenerator(content, output, "https://photos.example.com",
|
||||||
"Ada", "", "", "", "", "Crimson Pro", "Manjari", 60)
|
"Ada", "", "", "", "", "Crimson Pro", "Manjari", 60, viewTimeline)
|
||||||
return NewServer("0", content, output, false, g)
|
return NewServer("0", content, output, false, g)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ in {
|
|||||||
description = "Photos per page on the timeline and grid views. The timeline packs whole days, so pages vary slightly around this.";
|
description = "Photos per page on the timeline and grid views. The timeline packs whole days, so pages vary slightly around this.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
defaultView = lib.mkOption {
|
||||||
|
type = lib.types.enum [ "timeline" "grid" ];
|
||||||
|
default = "timeline";
|
||||||
|
description = "View published at the site root. The other one is published under /timeline/ or /grid/.";
|
||||||
|
};
|
||||||
|
|
||||||
nginx = {
|
nginx = {
|
||||||
enable = lib.mkEnableOption "nginx virtual host for photogallery";
|
enable = lib.mkEnableOption "nginx virtual host for photogallery";
|
||||||
|
|
||||||
@@ -148,6 +154,7 @@ in {
|
|||||||
"--serif-family" cfg.serifFamily
|
"--serif-family" cfg.serifFamily
|
||||||
"--ml-family" cfg.mlFamily
|
"--ml-family" cfg.mlFamily
|
||||||
"--page-size" (toString cfg.pageSize)
|
"--page-size" (toString cfg.pageSize)
|
||||||
|
"--default-view" cfg.defaultView
|
||||||
];
|
];
|
||||||
|
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@
|
|||||||
<p class="handle">{{.Handle}}</p>
|
<p class="handle">{{.Handle}}</p>
|
||||||
<p class="pcount"><strong>{{.Total}}</strong> posts</p>
|
<p class="pcount"><strong>{{.Total}}</strong> posts</p>
|
||||||
<p class="pbio">{{.Bio}}{{if .BioAlt}}<br><span class="ml">{{.BioAlt}}</span>{{end}}</p>
|
<p class="pbio">{{.Bio}}{{if .BioAlt}}<br><span class="ml">{{.BioAlt}}</span>{{end}}</p>
|
||||||
<a href="/" class="rss-a">← Timeline</a>
|
<a href="{{.TimelineURL}}" class="rss-a">← Timeline</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@
|
|||||||
<p class="handle">{{.Handle}}</p>
|
<p class="handle">{{.Handle}}</p>
|
||||||
<p class="pcount"><strong>{{.Total}}</strong> posts</p>
|
<p class="pcount"><strong>{{.Total}}</strong> posts</p>
|
||||||
<p class="pbio">{{.Bio}}{{if .BioAlt}}<br><span class="ml">{{.BioAlt}}</span>{{end}}</p>
|
<p class="pbio">{{.Bio}}{{if .BioAlt}}<br><span class="ml">{{.BioAlt}}</span>{{end}}</p>
|
||||||
<a href="/" class="rss-a">Timeline</a>
|
<a href="{{.TimelineURL}}" class="rss-a">Timeline</a>
|
||||||
<a href="/feed.xml" class="rss-a">
|
<a href="/feed.xml" class="rss-a">
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor" aria-hidden="true">
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor" aria-hidden="true">
|
||||||
<circle cx="2.5" cy="11.5" r="1.5"/>
|
<circle cx="2.5" cy="11.5" r="1.5"/>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<p class="handle">{{.Handle}}</p>
|
<p class="handle">{{.Handle}}</p>
|
||||||
<p class="pcount"><strong>{{.Total}}</strong> posts</p>
|
<p class="pcount"><strong>{{.Total}}</strong> posts</p>
|
||||||
<p class="pbio">{{.Bio}}{{if .BioAlt}}<br><span class="ml">{{.BioAlt}}</span>{{end}}</p>
|
<p class="pbio">{{.Bio}}{{if .BioAlt}}<br><span class="ml">{{.BioAlt}}</span>{{end}}</p>
|
||||||
<a href="/grid/" class="rss-a">Grid</a>
|
<a href="{{.GridURL}}" class="rss-a">Grid</a>
|
||||||
<a href="/feed.xml" class="rss-a">
|
<a href="/feed.xml" class="rss-a">
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor" aria-hidden="true">
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor" aria-hidden="true">
|
||||||
<circle cx="2.5" cy="11.5" r="1.5"/>
|
<circle cx="2.5" cy="11.5" r="1.5"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user