From a10dbda7acb92efac5ee5bc5b9fe22652daa3e59 Mon Sep 17 00:00:00 2001 From: logikonline Date: Wed, 4 Feb 2026 17:11:52 -0500 Subject: [PATCH] feat(blog): add view count tracking to blog posts Adds view_count field to blog_post table with database migration. Implements atomic increment on post views in both standalone and repo blog routes. Displays view count with eye icon in post templates. --- models/blog/blog_post.go | 7 +++++++ models/migrations/migrations.go | 1 + models/migrations/v1_26/v364.go | 13 +++++++++++++ options/locale/locale_en-US.json | 1 + routers/web/explore/blog.go | 4 ++++ routers/web/repo/blog.go | 4 ++++ templates/blog/standalone_view.tmpl | 12 ++++++++++++ templates/repo/blog/view.tmpl | 12 ++++++++++++ 8 files changed, 54 insertions(+) create mode 100644 models/migrations/v1_26/v364.go diff --git a/models/blog/blog_post.go b/models/blog/blog_post.go index 9a8cfa84e6..3362d67106 100644 --- a/models/blog/blog_post.go +++ b/models/blog/blog_post.go @@ -56,6 +56,7 @@ type BlogPost struct { //revive:disable-line:exported Status BlogPostStatus `xorm:"SMALLINT NOT NULL DEFAULT 0"` AllowComments bool `xorm:"NOT NULL DEFAULT true"` SubscriptionOnly bool `xorm:"NOT NULL DEFAULT false"` + ViewCount int64 `xorm:"NOT NULL DEFAULT 0"` PublishedUnix timeutil.TimeStamp `xorm:"INDEX"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` @@ -437,6 +438,12 @@ func DeleteBlogPost(ctx context.Context, id int64) error { return err } +// IncrementBlogPostViewCount atomically increments the view count for a blog post. +func IncrementBlogPostViewCount(ctx context.Context, id int64) error { + _, err := db.GetEngine(ctx).Exec("UPDATE blog_post SET view_count = view_count + 1 WHERE id = ?", id) + return err +} + // CountPublishedBlogPostsByAuthorID returns the number of published/public blog posts by a user. func CountPublishedBlogPostsByAuthorID(ctx context.Context, authorID int64) (int64, error) { return db.GetEngine(ctx).Where("author_id = ? AND status >= ?", authorID, BlogPostPublic).Count(new(BlogPost)) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index c4ff7f6a44..69739ca531 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -438,6 +438,7 @@ func prepareMigrationTasks() []*migration { newMigration(361, "Create wishlist_comment table", v1_26.CreateWishlistCommentTable), newMigration(362, "Create wishlist_comment_reaction table", v1_26.CreateWishlistCommentReactionTable), newMigration(363, "Add keep_packages_private to user", v1_26.AddKeepPackagesPrivateToUser), + newMigration(364, "Add view_count to blog_post", v1_26.AddViewCountToBlogPost), } return preparedMigrations } diff --git a/models/migrations/v1_26/v364.go b/models/migrations/v1_26/v364.go new file mode 100644 index 0000000000..e7ff544ce4 --- /dev/null +++ b/models/migrations/v1_26/v364.go @@ -0,0 +1,13 @@ +// Copyright 2026 MarketAlly. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import "xorm.io/xorm" + +func AddViewCountToBlogPost(x *xorm.Engine) error { + type BlogPost struct { + ViewCount int64 `xorm:"NOT NULL DEFAULT 0"` + } + return x.Sync(new(BlogPost)) +} diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 0f80f01695..328f56407b 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2040,6 +2040,7 @@ "repo.blog.subscription_required_desc": "This post is available exclusively to subscribers. Subscribe to unlock the full content.", "repo.blog.subscribe_to_read": "Subscribe to Read", "repo.blog.reactions.admin_hint": "Thumbs down counts are only visible to repo admins.", + "repo.blog.views": "views", "repo.blog.comments": "Comments", "repo.blog.comments.empty": "No comments yet. Be the first to share your thoughts!", "repo.blog.comments.disabled": "Comments have been disabled for this post.", diff --git a/routers/web/explore/blog.go b/routers/web/explore/blog.go index 3d1202adb2..17aaa74aec 100644 --- a/routers/web/explore/blog.go +++ b/routers/web/explore/blog.go @@ -168,6 +168,10 @@ func StandaloneBlogView(ctx *context.Context) { } } + // Increment view count + _ = blog_model.IncrementBlogPostViewCount(ctx, post.ID) + post.ViewCount++ + if err := post.LoadAuthor(ctx); err != nil { ctx.ServerError("LoadAuthor", err) return diff --git a/routers/web/repo/blog.go b/routers/web/repo/blog.go index 87eb0047d8..91cf587a15 100644 --- a/routers/web/repo/blog.go +++ b/routers/web/repo/blog.go @@ -229,6 +229,10 @@ func BlogView(ctx *context.Context) { } } + // Increment view count + _ = blog_model.IncrementBlogPostViewCount(ctx, post.ID) + post.ViewCount++ + if err := post.LoadAuthor(ctx); err != nil { ctx.ServerError("LoadAuthor", err) return diff --git a/templates/blog/standalone_view.tmpl b/templates/blog/standalone_view.tmpl index c0f9584de8..4cfe57cf41 100644 --- a/templates/blog/standalone_view.tmpl +++ b/templates/blog/standalone_view.tmpl @@ -115,6 +115,10 @@ {{if .IsWriter}} {{ctx.Locale.Tr "repo.blog.reactions.admin_hint"}} {{end}} + + {{svg "octicon-eye" 16}} + {{.BlogPost.ViewCount}} + @@ -510,6 +514,14 @@ font-size: 12px; color: var(--color-text-light-3); } +.blog-view-count { + display: inline-flex; + align-items: center; + gap: 6px; + margin-left: auto; + font-size: 14px; + color: var(--color-text-light-3); +} /* Comments */ .blog-comments { margin-bottom: 32px; diff --git a/templates/repo/blog/view.tmpl b/templates/repo/blog/view.tmpl index a5a33c97a7..dd63d7656f 100644 --- a/templates/repo/blog/view.tmpl +++ b/templates/repo/blog/view.tmpl @@ -108,6 +108,10 @@ {{if .IsWriter}} {{ctx.Locale.Tr "repo.blog.reactions.admin_hint"}} {{end}} + + {{svg "octicon-eye" 16}} + {{.BlogPost.ViewCount}} + @@ -477,6 +481,14 @@ font-size: 12px; color: var(--color-text-light-3); } +.blog-view-count { + display: inline-flex; + align-items: center; + gap: 6px; + margin-left: auto; + font-size: 14px; + color: var(--color-text-light-3); +} /* Comments */ .blog-comments { margin-bottom: 32px;