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;