diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..5df727e --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,23 @@ +{ + "permissions": { + "allow": [ + "Bash(curl -s -c /tmp/cookies.txt -L -X POST http://localhost:8099/login -d \"username=admin&password=admin\" -o /dev/null -w \"login final HTTP %{http_code}\\\\n\")", + "Bash(curl -s -b /tmp/cookies.txt http://localhost:8099/admin -o /tmp/dash.html -w \"HTTP %{http_code}\\\\n\")", + "Read(//tmp/**)", + "Bash(sed -n 's/.*\\\\\\(text-3xl font-bold text-gray-900 mt-1\">1\\\\n\\\\\\).*//p' /tmp/dash.html)", + "Bash(perl -0777 -ne 'while\\(/dash_posts.*?text-3xl[^>]*>\\(.*?\\)<\\\\/p>/sg\\){print \"POSTS BLOCK: $1\\\\n\"}' /tmp/dash.html)", + "Bash(python -c ' *)", + "Bash(curl -s -b /tmp/cookies.txt http://localhost:8099/admin -o dash.html)", + "Bash(rm -f dash.html)", + "Bash(curl -s http://localhost:8099/article/123 -o detail.html -w \"detail HTTP %{http_code}\\\\n\")", + "Bash(rm -f detail.html)", + "Bash(cp /tmp/cfg_backup.yaml win/etc/blog_go/config.yaml)", + "Bash(pkill -f blogtest.exe)", + "Bash(pkill -f \"blog_go.exe\")", + "Bash(cd c:/Users/wuwen/Documents/project/go_blog && rm -f /tmp/blogtest.exe && git status --short && echo \"--- diff stat ---\" && git diff --stat)" + ], + "additionalDirectories": [ + "\\tmp" + ] + } +} diff --git a/handlers/admin.go b/handlers/admin.go index b9910c4..3e632c4 100644 --- a/handlers/admin.go +++ b/handlers/admin.go @@ -4,16 +4,27 @@ import ( "net/http" "github.com/gin-gonic/gin" + "go_blog/models" + + "gorm.io/gorm" ) // AdminDashboard renders the protected admin dashboard. -func AdminDashboard() gin.HandlerFunc { +func AdminDashboard(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) username, _ := c.Get("username") data := DefaultData(c) data["Title"] = tr["dash_page_title"] data["Username"] = username + + // Article counts: total and published. + var postCount, publishedCount int64 + db.Model(&models.Article{}).Count(&postCount) + db.Model(&models.Article{}).Where("status = ?", models.ArticlePublished).Count(&publishedCount) + data["PostCount"] = postCount + data["PublishedCount"] = publishedCount + c.HTML(http.StatusOK, "dashboard", data) } } diff --git a/handlers/home.go b/handlers/home.go index f173b9f..b258134 100644 --- a/handlers/home.go +++ b/handlers/home.go @@ -2,16 +2,76 @@ package handlers import ( "net/http" + "time" "github.com/gin-gonic/gin" + "go_blog/models" + + "gorm.io/gorm" ) -// HomePage renders the public home page. -func HomePage() gin.HandlerFunc { +// 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")) + + authorName := article.Author.Username + if article.Author.DisplayName != "" { + authorName = article.Author.DisplayName + } + + data := DefaultData(c) + data["Title"] = article.Title + data["Article"] = article + data["AuthorName"] = authorName + data["PublishedAt"] = formatPublishTime(article.PublishedAt) + 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 "" +} diff --git a/i18n/i18n.go b/i18n/i18n.go index d8e175f..5505502 100644 --- a/i18n/i18n.go +++ b/i18n/i18n.go @@ -28,6 +28,7 @@ var translations = map[Lang]map[string]string{ "home_sign_in": "Sign In", "home_to_dash": "Dashboard", "home_latest": "Latest Posts", + "home_read_more": "Read more", "home_post1_tit": "Getting Started", "home_post1_dsc": "Welcome to your new blog! This is a placeholder post. Start writing to see your content here.", "home_post2_tit": "Hello World", @@ -52,6 +53,7 @@ var translations = map[Lang]map[string]string{ "dash_posts": "Posts", "dash_pages": "Pages", "dash_users": "Users", + "dash_published": "Published", "dash_mgmt_title": "Blog Management", "dash_mgmt_desc": "Post management, categories, and settings will appear here in future updates.", "dash_new_article": "New Article", @@ -114,6 +116,8 @@ var translations = map[Lang]map[string]string{ "article_new": "New Article", "article_title_required": "Title is required.", "article_content_required": "Content is required.", + "article_back_home": "Back to Home", + "article_not_found": "Article not found.", }, ZH: { // 导航 @@ -129,6 +133,7 @@ var translations = map[Lang]map[string]string{ "home_sign_in": "登录", "home_to_dash": "后台管理", "home_latest": "最新文章", + "home_read_more": "阅读全文", "home_post1_tit": "入门指南", "home_post1_dsc": "欢迎来到你的新博客!这是一篇占位文章。开始写作后,你的内容将显示在这里。", "home_post2_tit": "你好,世界", @@ -153,6 +158,7 @@ var translations = map[Lang]map[string]string{ "dash_posts": "文章", "dash_pages": "页面", "dash_users": "用户", + "dash_published": "已发布", "dash_mgmt_title": "博客管理", "dash_mgmt_desc": "文章管理、分类和设置将在后续版本中添加。", "dash_new_article": "新建文章", @@ -213,6 +219,8 @@ var translations = map[Lang]map[string]string{ "article_new": "新建文章", "article_title_required": "标题不能为空。", "article_content_required": "正文不能为空。", + "article_back_home": "返回首页", + "article_not_found": "文章不存在。", }, } diff --git a/main.go b/main.go index b5323f0..4ed578b 100644 --- a/main.go +++ b/main.go @@ -46,16 +46,17 @@ func main() { router.Use(middleware.SetUserContext(db)) // 8. Register routes. - router.GET("/", handlers.HomePage()) + router.GET("/", handlers.HomePage(db)) router.GET("/login", handlers.LoginPage()) router.POST("/login", handlers.Login(db)) router.POST("/logout", handlers.Logout()) + router.GET("/article/:slug", handlers.ArticleDetail(db)) // Protected admin routes. admin := router.Group("/admin") admin.Use(middleware.AuthRequired()) { - admin.GET("", handlers.AdminDashboard()) + admin.GET("", handlers.AdminDashboard(db)) admin.GET("/articles/new", handlers.ArticleCreatePage(db)) admin.POST("/articles/new", handlers.ArticleCreate(db)) } diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html index f06fba4..74fbacf 100644 --- a/templates/admin/dashboard.html +++ b/templates/admin/dashboard.html @@ -20,7 +20,9 @@

{{index .Tr "dash_posts"}}

-

0

+

{{.PostCount}} + ({{.PublishedCount}} {{index .Tr "dash_published"}}) +

{{index .Tr "dash_pages"}}

diff --git a/templates/pages/404.html b/templates/pages/404.html new file mode 100644 index 0000000..5cf5e3a --- /dev/null +++ b/templates/pages/404.html @@ -0,0 +1,10 @@ +{{define "article_not_found"}} +{{template "header" .}} +
+

{{index .Tr "article_not_found"}}

+ + {{index .Tr "article_back_home"}} + +
+{{template "footer" .}} +{{end}} diff --git a/templates/pages/article.html b/templates/pages/article.html new file mode 100644 index 0000000..662744a --- /dev/null +++ b/templates/pages/article.html @@ -0,0 +1,39 @@ +{{define "article"}} +{{template "header" .}} + + + +
+ + ← {{index .Tr "article_back_home"}} + + +
+

{{.Article.Title}}

+
+ {{if .Article.Author.Avatar}} + avatar + {{end}} + {{.AuthorName}} + {{if .PublishedAt}}·{{.PublishedAt}}{{end}} +
+ + {{if .Article.Cover}} +
+ {{.Article.Title}} +
+ {{end}} + +
+
+
+ + + +{{template "footer" .}} +{{end}} diff --git a/templates/pages/home.html b/templates/pages/home.html index 10faa5a..f06a691 100644 --- a/templates/pages/home.html +++ b/templates/pages/home.html @@ -20,18 +20,25 @@

{{index .Tr "home_latest"}}

-
-

{{index .Tr "home_post1_tit"}}

-

{{index .Tr "home_post1_dsc"}}

-
-
-

{{index .Tr "home_post2_tit"}}

-

{{index .Tr "home_post2_dsc"}}

-
-
-

{{index .Tr "home_post3_tit"}}

-

{{index .Tr "home_post3_dsc"}}

-
+ {{range .Articles}} + + {{if .Cover}} +
+ {{.Title}} +
+ {{end}} +
+

{{.Title}} + {{if .IsTop}}{{index $.Tr "article_is_top"}}{{end}} +

+ {{if .Summary}}

{{.Summary}}

{{end}} + {{index $.Tr "home_read_more"}} → +
+
+ {{else}} +
{{index .Tr "home_no_posts"}}
+ {{end}}
{{template "footer" .}}