feat: add notes field to favorites and enhance OG meta tags

Add an optional long-form "notes" text field to each favorite for
reviews, thoughts, or extended descriptions. The field is stored in
SQLite via a new migration (002_add_fave_notes.sql) and propagated
through the entire stack:

- Model: Notes field on Fave struct
- Store: All SQL queries (Create, GetByID, Update, list methods,
  scanFaves) updated with notes column
- Web handlers: Read/write notes in create, edit, update forms
- API handlers: Notes in create, update, get, import request/response
- Export: Notes included in both JSON and CSV exports
- Import: Notes parsed from both JSON and CSV imports
- Feed: Notes used as Atom feed item summary when present
- Form template: New textarea between URL and image fields
- Detail template: Display notes, enhanced og:description with
  cascade: notes (truncated) → URL → generic fallback text

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-04-04 00:40:08 +02:00
commit 485d01ce45
14 changed files with 151 additions and 71 deletions

View file

@ -135,6 +135,7 @@ func (h *Handler) handleCreateFave(w http.ResponseWriter, r *http.Request) {
var req struct {
Description string `json:"description"`
URL string `json:"url"`
Notes string `json:"notes"`
Privacy string `json:"privacy"`
Tags []string `json:"tags"`
}
@ -151,7 +152,7 @@ func (h *Handler) handleCreateFave(w http.ResponseWriter, r *http.Request) {
req.Privacy = user.DefaultFavePrivacy
}
fave, err := h.deps.Faves.Create(user.ID, req.Description, req.URL, "", req.Privacy)
fave, err := h.deps.Faves.Create(user.ID, req.Description, req.URL, "", req.Notes, req.Privacy)
if err != nil {
slog.Error("api: create fave error", "error", err)
jsonError(w, "Internal error", http.StatusInternalServerError)
@ -222,6 +223,7 @@ func (h *Handler) handleUpdateFave(w http.ResponseWriter, r *http.Request) {
var req struct {
Description string `json:"description"`
URL string `json:"url"`
Notes string `json:"notes"`
Privacy string `json:"privacy"`
Tags []string `json:"tags"`
}
@ -237,7 +239,7 @@ func (h *Handler) handleUpdateFave(w http.ResponseWriter, r *http.Request) {
req.Privacy = fave.Privacy
}
if err := h.deps.Faves.Update(id, req.Description, req.URL, fave.ImagePath, req.Privacy); err != nil {
if err := h.deps.Faves.Update(id, req.Description, req.URL, fave.ImagePath, req.Notes, req.Privacy); err != nil {
slog.Error("api: update fave error", "error", err)
jsonError(w, "Internal error", http.StatusInternalServerError)
return
@ -378,6 +380,7 @@ func (h *Handler) handleImport(w http.ResponseWriter, r *http.Request) {
var faves []struct {
Description string `json:"description"`
URL string `json:"url"`
Notes string `json:"notes"`
Privacy string `json:"privacy"`
Tags []string `json:"tags"`
}
@ -395,7 +398,7 @@ func (h *Handler) handleImport(w http.ResponseWriter, r *http.Request) {
if privacy != "public" && privacy != "private" {
privacy = user.DefaultFavePrivacy
}
fave, err := h.deps.Faves.Create(user.ID, f.Description, f.URL, "", privacy)
fave, err := h.deps.Faves.Create(user.ID, f.Description, f.URL, "", f.Notes, privacy)
if err != nil {
continue
}
@ -446,6 +449,7 @@ func faveJSON(f *model.Fave) map[string]any {
"id": f.ID,
"description": f.Description,
"url": f.URL,
"notes": f.Notes,
"image_path": f.ImagePath,
"privacy": f.Privacy,
"tags": tags,