aboutsummaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-17 21:42:02 +0300
committergrm <grm@eyesin.space>2026-09-17 21:42:02 +0300
commit2a347ced1b4ea8c24440f646df35207c3a3597ac (patch)
tree2bdfdbb32173172975e576b56bf0636af7462817 /internal
parentb233445a73168994087356d45b5dc7dd6a07aa8e (diff)
downloadblogspace-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
Diffstat (limited to 'internal')
-rw-r--r--internal/db/migrations/blog/00011_post_hide_date.sql7
-rw-r--r--internal/i18n/el.go21
-rw-r--r--internal/store/posts.go13
-rw-r--r--internal/web/handlers_posts.go1
-rw-r--r--internal/web/templates/blog/post.html2
-rw-r--r--internal/web/templates/blog/search.html2
-rw-r--r--internal/web/templates/dashboard/post_form.html1
-rw-r--r--internal/web/templates/partials/postlist.html2
8 files changed, 30 insertions, 19 deletions
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}} &middot; {{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}} &middot; {{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"}} &#8599;</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"}} &rarr;</a></p>{{end}}