Add -default-view to choose timeline or grid as the site root
The view named by -default-view (timeline, the default, or grid) is published at the site root; the other is served under /timeline/ or /grid/. viewBase and pageURL derive every page path, pager link and the Timeline<->Grid cross-link from the single setting, and Build removes both timeline/ and grid/ on each run so flipping the default never leaves a stale root behind. Reimplemented on trunk (which has albums and the /manage feature) rather than cherry-picked, since the original was written against an earlier base. Credit for the edit/delete and default-view ideas goes to Santhosh Raju, now noted in a README Contributors section. - main.go: -default-view flag, validated (fatal on anything but timeline/grid) - module.nix: services.photogallery.defaultView enum option - templates: timeline/grid/day/album/albums cross-links use TimelineURL/GridURL - tests: timeline-root, grid-root, and switch-cleanup
This commit is contained in:
1 parent
a7f2afc3ad
commit
8fa1aa4cd4
13 files changed
+209
-56
No files matched your search
@@ -113,9 +113,9 @@ contain spaces (`"berlin trip"`), and its URL slug is derived with `sanitiseSlug
|
|||||||
- Gruvbox dark hard colour scheme
|
- Gruvbox dark hard colour scheme
|
||||||
- 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`). Which view sits at the site root is chosen by `-default-view` (`timeline` or `grid`, default `timeline`; NixOS `services.photogallery.defaultView`): the chosen one owns `/` (`/`, `/page/2/`, …) and the other is published under `/<name>/`. `viewBase`/`pageURL` (`generator.go`) derive every page path, pager link and the Timeline↔Grid cross-links (`TimelineURL`/`GridURL` in the template data) from that one setting; `Build` removes both `timeline/` and `grid/` each run so a flipped default leaves no stale root behind.
|
||||||
- **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** (the default root): 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** is the flat square grid. At `/grid/` by default, or at the root when `-default-view grid`.
|
||||||
- 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.
|
||||||
- Single photo page with sidebar and newer/older navigation
|
- Single photo page with sidebar and newer/older navigation
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
A minimal static photo gallery with an Atom feed and an upload API.
|
A minimal static photo gallery with an Atom feed and an upload API.
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
- **[Santhosh Raju](https://git.planet-express.in/fox)** — originated the ideas for the edit/delete ("manage")
|
||||||
|
capability and the configurable default view (`-default-view`, timeline or
|
||||||
|
grid as the site root). Both landed here as independent implementations
|
||||||
|
building on that work.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
1. Drop `photo.jpg` + `photo.toml` into the content directory (or upload via the web UI at `/upload`).
|
1. Drop `photo.jpg` + `photo.toml` into the content directory (or upload via the web UI at `/upload`).
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ func (g *Generator) baseData() map[string]any {
|
|||||||
"FontsURL": g.fontsURL,
|
"FontsURL": g.fontsURL,
|
||||||
"SerifFamily": g.serifFamily,
|
"SerifFamily": g.serifFamily,
|
||||||
"MlFamily": g.mlFamily,
|
"MlFamily": g.mlFamily,
|
||||||
|
"TimelineURL": g.timelineURL(),
|
||||||
|
"GridURL": g.gridURL(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// buildWithView builds a two-photo site with the given default view and returns
|
||||||
|
// the output dir.
|
||||||
|
func buildWithView(t *testing.T, view string) string {
|
||||||
|
t.Helper()
|
||||||
|
content := t.TempDir()
|
||||||
|
output := t.TempDir()
|
||||||
|
writePNG(t, filepath.Join(content, "a.png"))
|
||||||
|
writePNG(t, filepath.Join(content, "b.png"))
|
||||||
|
|
||||||
|
g := NewGenerator(content, output, "http://x", "A", "@a", "bio", "", "f", "s", "m", 60, view)
|
||||||
|
if err := g.Build(); err != nil {
|
||||||
|
t.Fatalf("build (%s): %v", view, err)
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func exists(p string) bool { _, err := os.Stat(p); return err == nil }
|
||||||
|
|
||||||
|
func TestDefaultViewTimeline(t *testing.T) {
|
||||||
|
out := buildWithView(t, viewTimeline)
|
||||||
|
|
||||||
|
// Timeline owns the root; grid is the secondary under /grid/.
|
||||||
|
if !exists(filepath.Join(out, "index.html")) {
|
||||||
|
t.Error("root index.html missing")
|
||||||
|
}
|
||||||
|
if !exists(filepath.Join(out, "grid", "index.html")) {
|
||||||
|
t.Error("/grid/ should exist as the secondary view")
|
||||||
|
}
|
||||||
|
// No /timeline/ dir — timeline is at the root, not under its own name.
|
||||||
|
if exists(filepath.Join(out, "timeline")) {
|
||||||
|
t.Error("/timeline/ should not exist when timeline is the default")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The root (timeline) links to grid at /grid/.
|
||||||
|
root := readFile(t, filepath.Join(out, "index.html"))
|
||||||
|
if !strings.Contains(root, `href="/grid/"`) {
|
||||||
|
t.Error("timeline root should link to /grid/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultViewGrid(t *testing.T) {
|
||||||
|
out := buildWithView(t, viewGrid)
|
||||||
|
|
||||||
|
// Grid owns the root; timeline is the secondary under /timeline/.
|
||||||
|
if !exists(filepath.Join(out, "index.html")) {
|
||||||
|
t.Error("root index.html missing")
|
||||||
|
}
|
||||||
|
if !exists(filepath.Join(out, "timeline", "index.html")) {
|
||||||
|
t.Error("/timeline/ should exist as the secondary view")
|
||||||
|
}
|
||||||
|
// No /grid/ dir — grid is at the root now.
|
||||||
|
if exists(filepath.Join(out, "grid")) {
|
||||||
|
t.Error("/grid/ should not exist when grid is the default")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The root (grid) links back to the timeline at /timeline/.
|
||||||
|
root := readFile(t, filepath.Join(out, "index.html"))
|
||||||
|
if !strings.Contains(root, `href="/timeline/"`) {
|
||||||
|
t.Error("grid root should link to /timeline/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDefaultViewSwitchCleansUp confirms that flipping the default view does not
|
||||||
|
// leave the previous root's view directory behind.
|
||||||
|
func TestDefaultViewSwitchCleansUp(t *testing.T) {
|
||||||
|
content := t.TempDir()
|
||||||
|
output := t.TempDir()
|
||||||
|
writePNG(t, filepath.Join(content, "a.png"))
|
||||||
|
|
||||||
|
// First build with grid as root → creates /timeline/.
|
||||||
|
g1 := NewGenerator(content, output, "http://x", "A", "@a", "b", "", "f", "s", "m", 60, viewGrid)
|
||||||
|
if err := g1.Build(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !exists(filepath.Join(output, "timeline")) {
|
||||||
|
t.Fatal("expected /timeline/ after grid-default build")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild with timeline as root → /timeline/ must be gone, /grid/ present.
|
||||||
|
g2 := NewGenerator(content, output, "http://x", "A", "@a", "b", "", "f", "s", "m", 60, viewTimeline)
|
||||||
|
if err := g2.Build(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if exists(filepath.Join(output, "timeline")) {
|
||||||
|
t.Error("/timeline/ left over after switching default to timeline")
|
||||||
|
}
|
||||||
|
if !exists(filepath.Join(output, "grid", "index.html")) {
|
||||||
|
t.Error("/grid/ should exist after switching default to timeline")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFile(t *testing.T, p string) string {
|
||||||
|
t.Helper()
|
||||||
|
b, err := os.ReadFile(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
+70
-46
@@ -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
|
||||||
@@ -80,15 +85,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,
|
||||||
@@ -101,10 +110,33 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// timelineURL and gridURL are the roots of each view, for cross-links in the
|
||||||
|
// page headers.
|
||||||
|
func (g *Generator) timelineURL() string { return pageURL(g.viewBase(viewTimeline), 1) }
|
||||||
|
func (g *Generator) gridURL() string { return pageURL(g.viewBase(viewGrid), 1) }
|
||||||
|
|
||||||
func (g *Generator) initial() string {
|
func (g *Generator) initial() string {
|
||||||
for _, r := range g.author {
|
for _, r := range g.author {
|
||||||
return strings.ToUpper(string(r))
|
return strings.ToUpper(string(r))
|
||||||
@@ -124,19 +156,19 @@ func (g *Generator) Build() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Drop fully-derived HTML trees before regenerating them, so a deleted
|
// Drop fully-derived HTML trees before regenerating them, so a deleted
|
||||||
// post, a retag or a changed date leaves no orphaned page behind. These are
|
// post, a retag, a changed date or a flipped -default-view leaves no
|
||||||
// cheap to rewrite (no up-to-date skipping applies to HTML), unlike the
|
// orphaned page behind. These are cheap to rewrite (no up-to-date skipping
|
||||||
// image renditions, which are pruned by slug just below.
|
// applies to HTML), unlike the image renditions, which are pruned by slug
|
||||||
for _, d := range []string{"photo", "day", "album", "page"} {
|
// just below. Both "timeline" and "grid" are removed: whichever is the
|
||||||
|
// default owns the site root and has no dir of its own, so a dir by that
|
||||||
|
// name can only be a leftover from the opposite -default-view.
|
||||||
|
for _, d := range []string{"photo", "day", "album", "page", "timeline", "grid"} {
|
||||||
if err := os.RemoveAll(filepath.Join(g.outputDir, d)); err != nil {
|
if err := os.RemoveAll(filepath.Join(g.outputDir, d)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := os.RemoveAll(filepath.Join(g.outputDir, "grid", "page")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, d := range []string{"images", "thumbs", "medium", "og", "photo", "grid", "day", "album", "albums", "static"} {
|
for _, d := range []string{"images", "thumbs", "medium", "og", "photo", "day", "album", "albums", "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
|
||||||
}
|
}
|
||||||
@@ -467,17 +499,17 @@ func generateOG(src, dst string, maxDim int) error {
|
|||||||
return os.WriteFile(dst, buf.Bytes(), 0644)
|
return os.WriteFile(dst, buf.Bytes(), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderGridPages writes the paginated square-grid view under public/grid/:
|
// renderGridPages writes the paginated square-grid view. When grid is the
|
||||||
// grid/index.html and grid/page/N/index.html, chunking photos into pageSize
|
// -default-view it is the site root (index.html + page/N/); otherwise it lives
|
||||||
// slices for progressive pagination. The grid is the secondary view; the
|
// under /grid/. Photos are chunked into pageSize slices for progressive
|
||||||
// timeline is the site root.
|
// pagination.
|
||||||
func (g *Generator) renderGridPages(photos []Photo) error {
|
func (g *Generator) renderGridPages(photos []Photo) error {
|
||||||
total := len(photos)
|
total := len(photos)
|
||||||
totalPages := (total + g.pageSize - 1) / g.pageSize
|
totalPages := (total + g.pageSize - 1) / g.pageSize
|
||||||
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
|
||||||
@@ -487,27 +519,19 @@ 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{
|
||||||
@@ -517,6 +541,8 @@ func (g *Generator) renderGridPages(photos []Photo) error {
|
|||||||
"TotalPages": totalPages,
|
"TotalPages": totalPages,
|
||||||
"PrevURL": prev,
|
"PrevURL": prev,
|
||||||
"NextURL": next,
|
"NextURL": next,
|
||||||
|
"TimelineURL": g.timelineURL(),
|
||||||
|
"GridURL": g.gridURL(),
|
||||||
"BaseURL": g.baseURL,
|
"BaseURL": g.baseURL,
|
||||||
"Year": time.Now().Year(),
|
"Year": time.Now().Year(),
|
||||||
"Author": g.author,
|
"Author": g.author,
|
||||||
@@ -604,35 +630,29 @@ func chunkDayGroups(groups []DayGroup, target int) [][]DayGroup {
|
|||||||
return pages
|
return pages
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderTimelinePages writes the day-grouped timeline as the site root:
|
// renderTimelinePages writes the day-grouped timeline. When timeline is the
|
||||||
// index.html and page/N/index.html. Photos are grouped by day, then packed
|
// -default-view (the default) it is the site root (index.html + page/N/);
|
||||||
|
// otherwise it lives under /timeline/. Photos are grouped by day, then packed
|
||||||
// into pages of roughly pageSize without splitting a day across pages.
|
// into pages of roughly pageSize without splitting a day across pages.
|
||||||
func (g *Generator) renderTimelinePages(photos []Photo) error {
|
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{
|
||||||
@@ -642,6 +662,8 @@ func (g *Generator) renderTimelinePages(photos []Photo) error {
|
|||||||
"TotalPages": totalPages,
|
"TotalPages": totalPages,
|
||||||
"PrevURL": prev,
|
"PrevURL": prev,
|
||||||
"NextURL": next,
|
"NextURL": next,
|
||||||
|
"TimelineURL": g.timelineURL(),
|
||||||
|
"GridURL": g.gridURL(),
|
||||||
"BaseURL": g.baseURL,
|
"BaseURL": g.baseURL,
|
||||||
"Year": time.Now().Year(),
|
"Year": time.Now().Year(),
|
||||||
"Author": g.author,
|
"Author": g.author,
|
||||||
@@ -673,6 +695,8 @@ func (g *Generator) renderDayPages(photos []Photo) error {
|
|||||||
"Count": grp.Count,
|
"Count": grp.Count,
|
||||||
"Photos": grp.Photos,
|
"Photos": grp.Photos,
|
||||||
"Total": len(photos),
|
"Total": len(photos),
|
||||||
|
"TimelineURL": g.timelineURL(),
|
||||||
|
"GridURL": g.gridURL(),
|
||||||
"BaseURL": g.baseURL,
|
"BaseURL": g.baseURL,
|
||||||
"Year": time.Now().Year(),
|
"Year": time.Now().Year(),
|
||||||
"Author": g.author,
|
"Author": g.author,
|
||||||
|
|||||||
@@ -21,15 +21,20 @@ func main() {
|
|||||||
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)")
|
||||||
htpasswd := flag.String("htpasswd", "", "path to an htpasswd file (bcrypt, `htpasswd -B`) guarding /upload; empty disables auth")
|
htpasswd := flag.String("htpasswd", "", "path to an htpasswd file (bcrypt, `htpasswd -B`) guarding /upload; empty disables auth")
|
||||||
|
defaultView := flag.String("default-view", viewTimeline, "view published at the site root: \"timeline\" or \"grid\"; the other 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
@@ -29,7 +29,7 @@ func newTestServer(t *testing.T) (*Server, string, string) {
|
|||||||
}
|
}
|
||||||
writePNG(t, filepath.Join(content, "bar.png")) // no sidecar
|
writePNG(t, filepath.Join(content, "bar.png")) // no sidecar
|
||||||
|
|
||||||
g := NewGenerator(content, output, "http://x", "A", "@a", "bio", "", "f", "s", "m", 60)
|
g := NewGenerator(content, output, "http://x", "A", "@a", "bio", "", "f", "s", "m", 60, viewTimeline)
|
||||||
if err := g.Build(); err != nil {
|
if err := g.Build(); err != nil {
|
||||||
t.Fatalf("initial build: %v", err)
|
t.Fatalf("initial build: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 is published under /timeline/ or /grid/.";
|
||||||
|
};
|
||||||
|
|
||||||
htpasswdFile = lib.mkOption {
|
htpasswdFile = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = lib.types.nullOr lib.types.path;
|
||||||
default = null;
|
default = null;
|
||||||
@@ -156,6 +162,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
|
||||||
] ++ lib.optionals (cfg.htpasswdFile != null) [
|
] ++ lib.optionals (cfg.htpasswdFile != null) [
|
||||||
# %d is systemd's credentials directory; LoadCredential below drops
|
# %d is systemd's credentials directory; LoadCredential below drops
|
||||||
# the htpasswd file there, readable only by this service.
|
# the htpasswd file there, readable only by this service.
|
||||||
|
|||||||
@@ -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="/albums/" class="rss-a">← Albums</a>
|
<a href="/albums/" class="rss-a">← Albums</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
</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="/albums/" class="rss-a">Albums</a>
|
<a href="/albums/" class="rss-a">Albums</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">
|
||||||
|
|||||||
@@ -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="/albums/" class="rss-a">Albums</a>
|
<a href="/albums/" class="rss-a">Albums</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">
|
||||||
|
|||||||
Reference in new issue
Block a user