From e060faa39f28357b378ae895561c584813f12b65 Mon Sep 17 00:00:00 2001 From: luxick Date: Fri, 8 May 2026 18:15:09 +0200 Subject: [PATCH] Add persitent URLs for year/month/day in the diary --- README.md | 10 +++++++++ diary.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 27 +++++++++++++++++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 631eadc..a5771d8 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,13 @@ FolderName/ | Day (`DD/`) | Entry content and photo grid | Days with photos but no `index.md` still appear in the month view and can be created by clicking their heading link. + +#### Persistent date links + +Each diary root exposes three stable paths intended for browser bookmarks. They redirect to the current dated URL on every visit: + +| Path | Redirects to | +|------|-------------| +| `/today/` | `/YYYY/MM/DD/` (or `…/?edit` if the day folder does not exist yet) | +| `/this-month/` | `/YYYY/MM/` | +| `/this-year/` | `/YYYY/` | diff --git a/diary.go b/diary.go index 1377283..c88b5ef 100644 --- a/diary.go +++ b/diary.go @@ -21,6 +21,67 @@ func init() { type diaryHandler struct{} +// redirect resolves the persistent date links (today, this-month, this-year) +// inside any diary root to a dated URL. Returns ok=false otherwise. +func (d *diaryHandler) redirect(root, fsPath, urlPath string) (string, bool) { + base := path.Base(strings.TrimSuffix(urlPath, "/")) + switch base { + case "today", "this-month", "this-year": + default: + return "", false + } + + parentFS := filepath.Dir(fsPath) + parentURLPath := parentURL(urlPath) + _, diaryRootFS, diaryRootURL, ok := findDiaryContext(root, parentFS, parentURLPath) + if !ok { + return "", false + } + + now := time.Now() + year := fmt.Sprintf("%d", now.Year()) + month := fmt.Sprintf("%02d", int(now.Month())) + day := fmt.Sprintf("%02d", now.Day()) + + switch base { + case "today": + target := path.Join(diaryRootURL, year, month, day) + "/" + dayFS := filepath.Join(diaryRootFS, year, month, day) + if _, err := os.Stat(dayFS); err != nil { + target += "?edit" + } + return target, true + case "this-month": + return path.Join(diaryRootURL, year, month) + "/", true + case "this-year": + return path.Join(diaryRootURL, year) + "/", true + } + return "", false +} + +// defaultHeading returns the German long-form date as the editor pre-fill +// heading for a diary day folder (depth 3 inside a diary root). +func (d *diaryHandler) defaultHeading(root, fsPath, urlPath string) (string, bool) { + depth, _, _, ok := findDiaryContext(root, fsPath, urlPath) + if !ok || depth != 3 { + return "", false + } + day, err := strconv.Atoi(filepath.Base(fsPath)) + if err != nil { + return "", false + } + monthFS := filepath.Dir(fsPath) + month, err := strconv.Atoi(filepath.Base(monthFS)) + if err != nil || month < 1 || month > 12 { + return "", false + } + year, err := strconv.Atoi(filepath.Base(filepath.Dir(monthFS))) + if err != nil { + return "", false + } + return formatGermanDate(time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)), true +} + func (d *diaryHandler) handle(root, fsPath, urlPath string) *specialPage { depth, diaryRootFS, diaryRootURL, ok := findDiaryContext(root, fsPath, urlPath) if !ok { diff --git a/main.go b/main.go index 862e189..f3b5f34 100644 --- a/main.go +++ b/main.go @@ -37,8 +37,19 @@ type specialPage struct { // pageTypeHandler is implemented by each special folder type (diary, gallery, …). // handle returns nil when the handler does not apply to the given path. +// redirect returns ok=true with an absolute URL when the request should be +// short-circuited with a 302 redirect (e.g. persistent date links in a diary). +// defaultHeading returns ok=true with custom pre-fill heading text for the +// editor when no index.md exists yet (e.g. German long-form date for a diary +// day). Handlers that don't need a hook should return the zero value. +// +// When adding a new hook, prefer a sibling method here over folding logic +// into main.go or render.go. If this list grows much beyond three, consider +// collapsing into a single overrides struct returned per request. type pageTypeHandler interface { handle(root, fsPath, urlPath string) *specialPage + redirect(root, fsPath, urlPath string) (target string, ok bool) + defaultHeading(root, fsPath, urlPath string) (heading string, ok bool) } // pageTypeHandlers is the registry. Each type registers itself via init(). @@ -204,6 +215,13 @@ func (h *handler) serveDir(w http.ResponseWriter, r *http.Request, urlPath, fsPa return } + for _, ph := range pageTypeHandlers { + if target, ok := ph.redirect(h.root, fsPath, urlPath); ok { + http.Redirect(w, r, target, http.StatusFound) + return + } + } + indexPath := filepath.Join(fsPath, "index.md") rawMD, _ := os.ReadFile(indexPath) @@ -255,7 +273,14 @@ func (h *handler) serveDir(w http.ResponseWriter, r *http.Request, urlPath, fsPa rawContent = string(sections[sectionIndex]) } } else if editMode && rawContent == "" && urlPath != "/" { - rawContent = "# " + pageTitle(urlPath) + "\n\n" + heading := pageTitle(urlPath) + for _, ph := range pageTypeHandlers { + if custom, ok := ph.defaultHeading(h.root, fsPath, urlPath); ok { + heading = custom + break + } + } + rawContent = "# " + heading + "\n\n" } data := pageData{