2
0

feat(blog): add view count tracking to blog posts
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m22s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m10s
Build and Release / Lint (push) Successful in 4m54s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m0s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m43s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 6m39s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 7m46s
Build and Release / Build Binary (linux/arm64) (push) Successful in 9m6s

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.
This commit is contained in:
2026-02-04 17:11:52 -05:00
parent f6e54207dc
commit a10dbda7ac
8 changed files with 54 additions and 0 deletions

View File

@@ -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))

View File

@@ -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
}

View File

@@ -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))
}

View File

@@ -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.",

View File

@@ -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

View File

@@ -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

View File

@@ -115,6 +115,10 @@
{{if .IsWriter}}
<span class="blog-reaction-hint">{{ctx.Locale.Tr "repo.blog.reactions.admin_hint"}}</span>
{{end}}
<span class="blog-view-count">
{{svg "octicon-eye" 16}}
<span>{{.BlogPost.ViewCount}}</span>
</span>
</div>
<!-- Comments Section -->
@@ -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;

View File

@@ -108,6 +108,10 @@
{{if .IsWriter}}
<span class="blog-reaction-hint">{{ctx.Locale.Tr "repo.blog.reactions.admin_hint"}}</span>
{{end}}
<span class="blog-view-count">
{{svg "octicon-eye" 16}}
<span>{{.BlogPost.ViewCount}}</span>
</span>
</div>
<!-- Comments Section -->
@@ -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;