package handlers import ( "net/http" "time" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "go_blog/models" "gorm.io/gorm" ) // publishedArticleOrder is the ordering used to list published articles: // pinned first, then by most recent publish/creation time. const publishedArticleOrder = "is_top DESC, published_at DESC, created_at DESC" // HomePage renders the public home page with the latest published articles. func HomePage(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) data := DefaultData(c) data["Title"] = tr["page_home"] var articles []models.Article db.Where("status = ?", models.ArticlePublished). Order(publishedArticleOrder). Limit(10). Find(&articles) data["Articles"] = articles c.HTML(http.StatusOK, "home", data) } } // ArticleDetail renders a single published article by its slug. func ArticleDetail(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) slug := c.Param("slug") var article models.Article err := db.Where("slug = ? AND status = ?", slug, models.ArticlePublished). Preload("Author"). First(&article).Error if err != nil { data := DefaultData(c) data["Title"] = tr["article_not_found"] c.HTML(http.StatusNotFound, "article_not_found", data) return } // Increment view count; ignore errors so a view never breaks the page. db.Model(&models.Article{}).Where("id = ?", article.ID). UpdateColumn("view_count", gorm.Expr("view_count + 1")) // One-time flash notice (set by PostComment on success/pending). Reading // consumes the flash, so refreshing the page no longer re-shows it. notice := readCommentFlash(c) renderArticleDetail(c, db, &article, commentForm{}, "", notice) } } // renderArticleDetail renders the article page, including the comment section. // formErr refills the form with an error banner; notice is a one-time // success/pending banner (already consumed from the session by the caller). func renderArticleDetail(c *gin.Context, db *gorm.DB, article *models.Article, form commentForm, formErr, notice string) { tr := getTr(c) authorName := article.Author.Username if article.Author.DisplayName != "" { authorName = article.Author.DisplayName } viewer := viewerFromContext(c) var comments []models.Comment db.Where("article_id = ?", article.ID).Order("created_at ASC").Find(&comments) cfg := models.GetCommentConfig() useGravatar := cfg != nil && cfg.UseGravatar tree := buildCommentTree(comments, viewer, tr, useGravatar) data := DefaultData(c) data["Title"] = article.Title data["Article"] = article data["AuthorName"] = authorName data["PublishedAt"] = formatPublishTime(article.PublishedAt) data["Comments"] = tree data["CommentConfig"] = models.GetCommentConfig() data["CommentForm"] = form data["CommentError"] = formErr data["CommentNotice"] = notice data["MaxCommentLength"] = MaxCommentLength c.HTML(http.StatusOK, "article", data) } // formatPublishTime returns the publication time as a readable string, falling // back to the creation time when the publish timestamp is unset. func formatPublishTime(publishedAt *time.Time) string { if publishedAt != nil { return publishedAt.Format("2006-01-02 15:04") } return "" } // commentFlashKey is the session key for the one-time comment notice. const commentFlashKey = "comment_flash" // setCommentFlash stores a one-time comment notice in the session so the // following GET /article/:slug (after the POST/redirect) can show it once and // never again on refresh. func setCommentFlash(c *gin.Context, value string) { session := sessions.Default(c) session.Set(commentFlashKey, value) session.Save() } // readCommentFlash returns and clears the one-time comment notice, if any. func readCommentFlash(c *gin.Context) string { session := sessions.Default(c) v, ok := session.Get(commentFlashKey).(string) if ok && v != "" { session.Delete(commentFlashKey) session.Save() return v } return "" }