diff options
| author | grm <grm@eyesin.space> | 2026-09-17 21:42:02 +0300 |
|---|---|---|
| committer | grm <grm@eyesin.space> | 2026-09-17 21:42:02 +0300 |
| commit | 2a347ced1b4ea8c24440f646df35207c3a3597ac (patch) | |
| tree | 2bdfdbb32173172975e576b56bf0636af7462817 | |
| parent | b233445a73168994087356d45b5dc7dd6a07aa8e (diff) | |
| download | blogspace-2a347ced1b4ea8c24440f646df35207c3a3597ac.tar.gz blogspace-2a347ced1b4ea8c24440f646df35207c3a3597ac.tar.bz2 blogspace-2a347ced1b4ea8c24440f646df35207c3a3597ac.zip | |
Let a post hide its date
A checkbox on the post form (posts.hide_date) overriding the theme's
"show the date on posts" for that post alone: listings, the post page and
search results leave the date out; the feed keeps its pubDate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
| -rw-r--r-- | AGENTS.md | 7 | ||||
| -rw-r--r-- | internal/db/migrations/blog/00011_post_hide_date.sql | 7 | ||||
| -rw-r--r-- | internal/i18n/el.go | 21 | ||||
| -rw-r--r-- | internal/store/posts.go | 13 | ||||
| -rw-r--r-- | internal/web/handlers_posts.go | 1 | ||||
| -rw-r--r-- | internal/web/templates/blog/post.html | 2 | ||||
| -rw-r--r-- | internal/web/templates/blog/search.html | 2 | ||||
| -rw-r--r-- | internal/web/templates/dashboard/post_form.html | 1 | ||||
| -rw-r--r-- | internal/web/templates/partials/postlist.html | 2 |
9 files changed, 35 insertions, 21 deletions
@@ -398,7 +398,10 @@ internal/web/ server.go (host router, middleware, render helpers) accepts `YYYY-MM-DD[THH:MM]`), for new and existing posts alike, and it drives ordering, the archive, the feed and the displayed dates. Blank keeps the current value; a new post starts at now. Times are in the server's - zone, which is also the one dates are rendered in. + zone, which is also the one dates are rendered in. `posts.hide_date` + (`Post.HideDate`, the form's "Hide the date on this post") is a per-post + override of the theme's `ShowDates`: the listing, post page and search + templates check `and ShowDates (not .HideDate)`; the feed keeps its `pubDate`. - **Tags** (`tags` + `post_tags` tables, `store/tags.go`, `web/tags.go`): a tag is `name` + unique `slug` (`slug.Make(name)`), so the slug is both the URL (`/tag/<slug>`, `handleBlogTag`, published posts of every page, @@ -466,7 +469,7 @@ superadmin password to `admin`. Production refuses both. `-- +goose Up/Down` sections; they run automatically at startup. Never edit an applied migration. Blog chain so far: `00001_init`, `00002_language`, `00003_files`, `00004_tags`, `00005_search`, `00006_format`, `00007_post_image`, - `00008_page_outro`, `00009_gizmo_style`, `00010_font_kind`; control: `00001_init`, `00002_upload_limit`. Both chains were re-baselined at 00001 after + `00008_page_outro`, `00009_gizmo_style`, `00010_font_kind`, `00011_post_hide_date`; control: `00001_init`, `00002_upload_limit`. Both chains were re-baselined at 00001 after the move to per-blog databases; deployments from before it have `goose_db_version` rows 2–7 in the control DB that must be deleted once (README "Upgrading from a single database") or the next control migration diff --git a/internal/db/migrations/blog/00011_post_hide_date.sql b/internal/db/migrations/blog/00011_post_hide_date.sql new file mode 100644 index 0000000..5af11ed --- /dev/null +++ b/internal/db/migrations/blog/00011_post_hide_date.sql @@ -0,0 +1,7 @@ +-- +goose Up +-- A per-post override of the theme's "show the date" switch, for evergreen +-- posts on a blog that otherwise dates everything. +ALTER TABLE posts ADD COLUMN hide_date boolean NOT NULL DEFAULT false; + +-- +goose Down +ALTER TABLE posts DROP COLUMN hide_date; diff --git a/internal/i18n/el.go b/internal/i18n/el.go index 5603659..a8a1bde 100644 --- a/internal/i18n/el.go +++ b/internal/i18n/el.go @@ -71,16 +71,17 @@ var el = map[string]string{ "Write the first one.": "Γράψτε την πρώτη.", // ---- posts ---- - "New post": "Νέα δημοσίευση", - "Edit post": "Επεξεργασία δημοσίευσης", - "All pages": "Όλες οι σελίδες", - "No posts here yet.": "Δεν υπάρχουν δημοσιεύσεις εδώ ακόμα.", - "optional, made from the title": "προαιρετικό, φτιάχνεται από τον τίτλο", - "Visible on the blog": "Ορατή στο ιστολόγιο", - "Back to posts": "Πίσω στις δημοσιεύσεις", - "Post deleted.": "Η δημοσίευση διαγράφηκε.", - "the post “%s”": "τη δημοσίευση «%s»", - "Title is required (max 200 characters).": "Ο τίτλος είναι υποχρεωτικός (έως 200 χαρακτήρες).", + "New post": "Νέα δημοσίευση", + "Edit post": "Επεξεργασία δημοσίευσης", + "All pages": "Όλες οι σελίδες", + "No posts here yet.": "Δεν υπάρχουν δημοσιεύσεις εδώ ακόμα.", + "optional, made from the title": "προαιρετικό, φτιάχνεται από τον τίτλο", + "Visible on the blog": "Ορατή στο ιστολόγιο", + "Hide the date on this post": "Απόκρυψη της ημερομηνίας σε αυτή τη δημοσίευση", + "Back to posts": "Πίσω στις δημοσιεύσεις", + "Post deleted.": "Η δημοσίευση διαγράφηκε.", + "the post “%s”": "τη δημοσίευση «%s»", + "Title is required (max 200 characters).": "Ο τίτλος είναι υποχρεωτικός (έως 200 χαρακτήρες).", "Slug may only contain lowercase letters, digits and dashes.": "Η διεύθυνση επιτρέπει μόνο πεζά λατινικά γράμματα, ψηφία και παύλες.", "Pick a page for this post.": "Επιλέξτε σελίδα για τη δημοσίευση.", "Post is too long (200 KB max).": "Η δημοσίευση είναι πολύ μεγάλη (έως 200 KB).", diff --git a/internal/store/posts.go b/internal/store/posts.go index 368a4de..e2b7ed5 100644 --- a/internal/store/posts.go +++ b/internal/store/posts.go @@ -15,6 +15,7 @@ type Post struct { Format string // FormatMarkdown | FormatHTML: how BodyMD is read Image string // featured image: a file id, or "" Published bool + HideDate bool // this post shows no date even when the theme shows them CreatedAt time.Time UpdatedAt time.Time // joined @@ -29,7 +30,7 @@ type Post struct { // The tags come along as two parallel arrays (names and slugs, both by name) // so every post query stays a single round trip; the alias p is the posts row. -const postCols = `p.id, p.page_id, p.slug, p.title, p.body_md, p.body_html, p.format, p.image, p.published, p.created_at, p.updated_at, g.slug, g.title, +const postCols = `p.id, p.page_id, p.slug, p.title, p.body_md, p.body_html, p.format, p.image, p.published, p.hide_date, p.created_at, p.updated_at, g.slug, g.title, coalesce((SELECT array_agg(t.name ORDER BY t.name) FROM post_tags pt JOIN tags t ON t.id=pt.tag_id WHERE pt.post_id=p.id), '{}'), coalesce((SELECT array_agg(t.slug ORDER BY t.name) FROM post_tags pt JOIN tags t ON t.id=pt.tag_id WHERE pt.post_id=p.id), '{}')` @@ -37,7 +38,7 @@ func scanPost(row interface{ Scan(...any) error }) (*Post, error) { var p Post var names, slugs []string var image *string - err := row.Scan(&p.ID, &p.PageID, &p.Slug, &p.Title, &p.BodyMD, &p.BodyHTML, &p.Format, &image, &p.Published, &p.CreatedAt, &p.UpdatedAt, &p.PageSlug, &p.PageTitle, &names, &slugs) + err := row.Scan(&p.ID, &p.PageID, &p.Slug, &p.Title, &p.BodyMD, &p.BodyHTML, &p.Format, &image, &p.Published, &p.HideDate, &p.CreatedAt, &p.UpdatedAt, &p.PageSlug, &p.PageTitle, &names, &slugs) if err != nil { return nil, wrap(err) } @@ -149,8 +150,8 @@ func (bs *BlogStore) CreatePost(ctx context.Context, p *Post) (*Post, error) { p.CreatedAt = time.Now() } var id int64 - err := bs.db.QueryRow(ctx, `INSERT INTO posts (page_id, slug, title, body_md, body_html, format, image, published, created_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING id`, - p.PageID, p.Slug, p.Title, p.BodyMD, p.BodyHTML, formatOrMD(p.Format), nullIfEmpty(p.Image), p.Published, p.CreatedAt).Scan(&id) + err := bs.db.QueryRow(ctx, `INSERT INTO posts (page_id, slug, title, body_md, body_html, format, image, published, hide_date, created_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING id`, + p.PageID, p.Slug, p.Title, p.BodyMD, p.BodyHTML, formatOrMD(p.Format), nullIfEmpty(p.Image), p.Published, p.HideDate, p.CreatedAt).Scan(&id) if err != nil { return nil, wrap(err) } @@ -158,8 +159,8 @@ func (bs *BlogStore) CreatePost(ctx context.Context, p *Post) (*Post, error) { } func (bs *BlogStore) UpdatePost(ctx context.Context, p *Post) error { - _, err := bs.db.Exec(ctx, `UPDATE posts SET page_id=$2, slug=$3, title=$4, body_md=$5, body_html=$6, format=$7, image=$8, published=$9, created_at=$10, updated_at=now() WHERE id=$1`, - p.ID, p.PageID, p.Slug, p.Title, p.BodyMD, p.BodyHTML, formatOrMD(p.Format), nullIfEmpty(p.Image), p.Published, p.CreatedAt) + _, err := bs.db.Exec(ctx, `UPDATE posts SET page_id=$2, slug=$3, title=$4, body_md=$5, body_html=$6, format=$7, image=$8, published=$9, hide_date=$10, created_at=$11, updated_at=now() WHERE id=$1`, + p.ID, p.PageID, p.Slug, p.Title, p.BodyMD, p.BodyHTML, formatOrMD(p.Format), nullIfEmpty(p.Image), p.Published, p.HideDate, p.CreatedAt) return wrap(err) } diff --git a/internal/web/handlers_posts.go b/internal/web/handlers_posts.go index ac0dd90..8a54c31 100644 --- a/internal/web/handlers_posts.go +++ b/internal/web/handlers_posts.go @@ -113,6 +113,7 @@ func (s *Server) handlePostSave(w http.ResponseWriter, r *http.Request) { p.BodyMD = strings.ReplaceAll(r.FormValue("body"), "\r\n", "\n") p.Format = pick(r.FormValue("format"), store.FormatMarkdown, store.FormatHTML) p.Published = r.FormValue("published") == "on" + p.HideDate = r.FormValue("hide_date") == "on" p.PageID, _ = strconv.ParseInt(r.FormValue("page_id"), 10, 64) p.Image = fileID(pickFile(p.Image, r.FormValue("image"))) tags, tagsOK := parseTags(strings.Split(r.FormValue("tags"), ",")) diff --git a/internal/web/templates/blog/post.html b/internal/web/templates/blog/post.html index d16e842..33a1723 100644 --- a/internal/web/templates/blog/post.html +++ b/internal/web/templates/blog/post.html @@ -2,7 +2,7 @@ {{define "content"}} <article class="post single"> <h1 class="post-title">{{.Data.post.Title}}</h1> - <p class="post-date">{{if .Data.theme.ShowDates}}{{postdate .Data.theme.DateFormat .Data.post.CreatedAt}} · {{end}}<a href="{{if .Data.page.IsHome}}/{{else}}/{{.Data.page.Slug}}{{end}}">{{.Data.page.Title}}</a></p> + <p class="post-date">{{if and .Data.theme.ShowDates (not .Data.post.HideDate)}}{{postdate .Data.theme.DateFormat .Data.post.CreatedAt}} · {{end}}<a href="{{if .Data.page.IsHome}}/{{else}}/{{.Data.page.Slug}}{{end}}">{{.Data.page.Title}}</a></p> {{template "posttags" (dict "tags" .Data.post.Tags "hash" .Data.theme.TagHash)}} {{$img := .Data.post.Image}}{{$place := .Data.theme.PostImage}} {{if and $img (eq $place "top")}}<p class="post-image"><img src="/media/{{$img}}" alt=""></p>{{end}} diff --git a/internal/web/templates/blog/search.html b/internal/web/templates/blog/search.html index 7ee05fc..9a00c9d 100644 --- a/internal/web/templates/blog/search.html +++ b/internal/web/templates/blog/search.html @@ -8,7 +8,7 @@ <p class="muted small">{{if eq .Data.total 1}}{{t "One post matches."}}{{else}}{{tf "%d posts match." .Data.total}}{{end}}</p> <ul class="search-results"> {{range .Data.hits}}<li> - <a href="/{{.PageSlug}}/{{.Slug}}">{{.Title}}</a>{{if $.Data.theme.ShowDates}} <span class="post-date">{{postdate $.Data.theme.DateFormat .CreatedAt}}</span>{{end}} + <a href="/{{.PageSlug}}/{{.Slug}}">{{.Title}}</a>{{if and $.Data.theme.ShowDates (not .HideDate)}} <span class="post-date">{{postdate $.Data.theme.DateFormat .CreatedAt}}</span>{{end}} <p class="snippet">{{.Snippet}}</p> </li> {{end}}</ul> diff --git a/internal/web/templates/dashboard/post_form.html b/internal/web/templates/dashboard/post_form.html index 9bd7624..e64a331 100644 --- a/internal/web/templates/dashboard/post_form.html +++ b/internal/web/templates/dashboard/post_form.html @@ -41,6 +41,7 @@ {{template "editor" (dict "name" "body" "value" .Data.post.BodyMD "format" .Data.post.Format "rows" 24 "tall" true "upload" (print "/b/" .Blog.Subdomain "/files/upload") "preview" (print "/b/" .Blog.Subdomain "/preview") "csrf" .CSRF)}} {{template "imagepick" (dict "name" "image" "current" .Data.post.Image "sub" .Blog.Subdomain "images" .Data.images "names" .Data.imageNames "label" (t "Featured image") "hint" (t "Optional. A thumbnail on listings, and on the post page where the Design tab puts it."))}} <label class="check"><input type="checkbox" name="published"{{if .Data.post.Published}} checked{{end}}> {{t "Visible on the blog"}}</label> + <label class="check"><input type="checkbox" name="hide_date"{{if .Data.post.HideDate}} checked{{end}}> {{t "Hide the date on this post"}}</label> <p class="actionrow"> <button type="submit">{{t "Save"}}</button> {{if .Data.post.ID}}<a class="btn secondary" href="{{.BlogURL}}/{{.Data.post.PageSlug}}/{{.Data.post.Slug}}" target="_blank">{{t "View post"}} ↗</a>{{end}} diff --git a/internal/web/templates/partials/postlist.html b/internal/web/templates/partials/postlist.html index 91e248c..fce6291 100644 --- a/internal/web/templates/partials/postlist.html +++ b/internal/web/templates/partials/postlist.html @@ -6,7 +6,7 @@ <article class="post{{if .Image}} has-thumb{{end}}"> {{if .Image}}<a class="post-thumb" href="/{{.PageSlug}}/{{.Slug}}"><img src="/media/{{.Image}}" alt=""></a>{{end}} <h2 class="post-title"><a href="/{{.PageSlug}}/{{.Slug}}">{{.Title}}</a></h2> - {{if $.Data.theme.ShowDates}}<p class="post-date">{{postdate $.Data.theme.DateFormat .CreatedAt}}</p>{{end}} + {{if and $.Data.theme.ShowDates (not .HideDate)}}<p class="post-date">{{postdate $.Data.theme.DateFormat .CreatedAt}}</p>{{end}} {{template "posttags" (dict "tags" .Tags "hash" $.Data.theme.TagHash)}} <div class="post-body">{{html .Excerpt}}</div> {{if .HasMore}}<p class="read-more"><a href="/{{.PageSlug}}/{{.Slug}}">{{t "Read more"}} →</a></p>{{end}} |
