From 0961bbd1b7a4fa30ee41ff8fb292c2a76ffaeb79 Mon Sep 17 00:00:00 2001 From: Jiao77 Date: Thu, 5 Mar 2026 16:56:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BE=AE=E8=AF=AD?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增微语页面,类似 Twitter/QQ 空间的短内容发布平台 - 添加 GitHub 风格热力图组件展示发布活动 - 支持发布微语、图片上传、标签、Emoji - 支持点赞、评论功能 - 右侧栏显示统计数据和热门标签 - 支持按标签筛选微语 - 后端新增微语相关 API(CRUD、点赞、评论、标签) Co-authored-by: Qwen-Coder --- server/cmd/server/main.go | 18 + server/internal/database/database.go | 3 + server/internal/handlers/micro.go | 595 +++++++++++++++++++++++++ server/internal/models/models.go | 38 ++ src/components/HeatmapCalendar.vue | 230 ++++++++++ src/components/MicroCommentSection.vue | 285 ++++++++++++ src/components/MicroFeed.vue | 518 +++++++++++++++++++++ src/components/MicroPost.vue | 352 +++++++++++++++ src/components/MicroSidebar.vue | 191 ++++++++ src/layouts/BaseLayout.astro | 2 + src/pages/micro.astro | 44 ++ 11 files changed, 2276 insertions(+) create mode 100644 server/internal/handlers/micro.go create mode 100644 src/components/HeatmapCalendar.vue create mode 100644 src/components/MicroCommentSection.vue create mode 100644 src/components/MicroFeed.vue create mode 100644 src/components/MicroPost.vue create mode 100644 src/components/MicroSidebar.vue create mode 100644 src/pages/micro.astro diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go index 835b467..7701289 100644 --- a/server/cmd/server/main.go +++ b/server/cmd/server/main.go @@ -33,6 +33,7 @@ func main() { authHandler := handlers.NewAuthHandler(jwtManager) commentHandler := handlers.NewCommentHandler() likeHandler := handlers.NewLikeHandler() + microHandler := handlers.NewMicroHandler() // 创建路由 r := gin.New() @@ -70,6 +71,15 @@ func main() { api.GET("/likes", likeHandler.GetLikeStatus) api.POST("/likes", likeHandler.ToggleLike) // 允许访客点赞(基于 IP Hash) + // 微语公开接口 + api.GET("/micro", microHandler.GetMicros) + api.GET("/micro/stats", microHandler.GetMicroStats) + api.GET("/micro/heatmap", microHandler.GetMicroHeatmap) + api.GET("/micro/tags", microHandler.GetMicroTags) + api.GET("/micro/:id", microHandler.GetMicro) + api.POST("/micro/:id/like", microHandler.ToggleMicroLike) // 允许访客点赞 + api.GET("/micro-comments", microHandler.GetMicroComments) // 获取微语评论 + // 需要认证的接口 authGroup := api.Group("") authGroup.Use(middleware.AuthMiddleware(jwtManager)) @@ -81,6 +91,14 @@ func main() { // 评论相关(需要登录才能评论) authGroup.POST("/comments", commentHandler.CreateComment) authGroup.DELETE("/comments/:id", commentHandler.DeleteComment) + + // 微语相关(需要登录才能发布和删除) + authGroup.POST("/micro", microHandler.CreateMicro) + authGroup.DELETE("/micro/:id", microHandler.DeleteMicro) + + // 微语评论相关 + authGroup.POST("/micro-comments", microHandler.CreateMicroComment) + authGroup.DELETE("/micro-comments/:id", microHandler.DeleteMicroComment) } // 管理员接口 diff --git a/server/internal/database/database.go b/server/internal/database/database.go index c7da001..f0c853f 100644 --- a/server/internal/database/database.go +++ b/server/internal/database/database.go @@ -54,6 +54,9 @@ func autoMigrate() error { &models.Like{}, &models.LikeCount{}, &models.PostMeta{}, + &models.Micro{}, + &models.MicroLike{}, + &models.MicroComment{}, ) } diff --git a/server/internal/handlers/micro.go b/server/internal/handlers/micro.go new file mode 100644 index 0000000..acc1220 --- /dev/null +++ b/server/internal/handlers/micro.go @@ -0,0 +1,595 @@ +package handlers + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/novablog/server/internal/database" + "github.com/novablog/server/internal/middleware" + "github.com/novablog/server/internal/models" + "gorm.io/gorm" +) + +// MicroHandler 微语处理器 +type MicroHandler struct{} + +// NewMicroHandler 创建微语处理器 +func NewMicroHandler() *MicroHandler { + return &MicroHandler{} +} + +// CreateMicroRequest 创建微语请求 +type CreateMicroRequest struct { + Content string `json:"content" binding:"required,min=1,max=2000"` + Images []string `json:"images"` + Tags []string `json:"tags"` +} + +// CreateMicro 创建微语 +func (h *MicroHandler) CreateMicro(c *gin.Context) { + userID, isLoggedIn := middleware.GetUserID(c) + if !isLoggedIn || userID == 0 { + c.JSON(http.StatusUnauthorized, gin.H{"error": "请登录后再发布"}) + return + } + + var req CreateMicroRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 将图片数组转为 JSON 字符串 + var imagesJSON string + if len(req.Images) > 0 { + imagesBytes, _ := json.Marshal(req.Images) + imagesJSON = string(imagesBytes) + } + + // 将标签数组转为 JSON 字符串 + var tagsJSON string + if len(req.Tags) > 0 { + tagsBytes, _ := json.Marshal(req.Tags) + tagsJSON = string(tagsBytes) + } + + micro := models.Micro{ + UserID: userID, + Content: req.Content, + Images: imagesJSON, + Tags: tagsJSON, + } + + if err := database.DB.Create(µ).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "发布失败"}) + return + } + + // 加载用户信息 + database.DB.Preload("User").First(µ, micro.ID) + + c.JSON(http.StatusCreated, micro) +} + +// GetMicros 获取微语列表 +func (h *MicroHandler) GetMicros(c *gin.Context) { + // 分页参数 + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 50 { + pageSize = 20 + } + + // 标签过滤 + tag := c.Query("tag") + + var micros []models.Micro + var total int64 + + query := database.DB.Model(&models.Micro{}).Where("deleted_at IS NULL") + + // 如果有标签过滤 + if tag != "" { + query = query.Where("tags LIKE ?", "%\""+tag+"\"%") + } + + query.Count(&total) + + if err := query. + Preload("User"). + Order("created_at DESC"). + Offset((page - 1) * pageSize). + Limit(pageSize). + Find(µs).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取失败"}) + return + } + + // 获取当前用户的点赞状态 + userID, isLoggedIn := middleware.GetUserID(c) + userLikes := make(map[uint]bool) + if isLoggedIn && userID > 0 { + var likes []models.MicroLike + microIDs := make([]uint, len(micros)) + for i, m := range micros { + microIDs[i] = m.ID + } + database.DB.Where("micro_id IN ? AND user_id = ?", microIDs, userID).Find(&likes) + for _, like := range likes { + userLikes[like.MicroID] = true + } + } + + // 构建响应 + type MicroResponse struct { + models.Micro + IsLiked bool `json:"is_liked"` + } + + responses := make([]MicroResponse, len(micros)) + for i, m := range micros { + responses[i] = MicroResponse{ + Micro: m, + IsLiked: userLikes[m.ID], + } + } + + c.JSON(http.StatusOK, gin.H{ + "data": responses, + "pagination": gin.H{ + "page": page, + "page_size": pageSize, + "total": total, + "total_page": (total + int64(pageSize) - 1) / int64(pageSize), + }, + }) +} + +// GetMicro 获取单条微语 +func (h *MicroHandler) GetMicro(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"}) + return + } + + var micro models.Micro + if err := database.DB.Preload("User").First(µ, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "微语不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取失败"}) + return + } + + // 获取点赞状态 + userID, isLoggedIn := middleware.GetUserID(c) + isLiked := false + if isLoggedIn && userID > 0 { + var like models.MicroLike + if err := database.DB.Where("micro_id = ? AND user_id = ?", micro.ID, userID).First(&like).Error; err == nil { + isLiked = true + } + } + + c.JSON(http.StatusOK, gin.H{ + "data": micro, + "is_liked": isLiked, + }) +} + +// DeleteMicro 删除微语 +func (h *MicroHandler) DeleteMicro(c *gin.Context) { + userID, _ := middleware.GetUserID(c) + role, _ := c.Get("role") + + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"}) + return + } + + var micro models.Micro + if err := database.DB.First(µ, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "微语不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取失败"}) + return + } + + // 检查权限:本人或管理员可删除 + if micro.UserID != userID && role != "admin" { + c.JSON(http.StatusForbidden, gin.H{"error": "无权限删除"}) + return + } + + // 软删除 + if err := database.DB.Delete(µ).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) +} + +// ToggleMicroLike 切换点赞状态 +func (h *MicroHandler) ToggleMicroLike(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"}) + return + } + + // 检查微语是否存在 + var micro models.Micro + if err := database.DB.First(µ, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "微语不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取失败"}) + return + } + + userID, isLoggedIn := middleware.GetUserID(c) + + var existingLike models.MicroLike + var likeErr error + + if isLoggedIn && userID > 0 { + // 登录用户:按 user_id 查找 + likeErr = database.DB.Where("micro_id = ? AND user_id = ?", id, userID).First(&existingLike).Error + } else { + // 访客:按 IP Hash 查找 + ipHash := getIPHash(c) + likeErr = database.DB.Where("micro_id = ? AND ip_hash = ?", id, ipHash).First(&existingLike).Error + } + + if likeErr == gorm.ErrRecordNotFound { + // 创建点赞 + newLike := models.MicroLike{ + MicroID: uint(id), + } + if isLoggedIn && userID > 0 { + newLike.UserID = &userID + } else { + newLike.IPHash = getIPHash(c) + } + + if err := database.DB.Create(&newLike).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "点赞失败"}) + return + } + + // 更新点赞计数 + database.DB.Model(µ).Update("like_count", gorm.Expr("like_count + 1")) + + c.JSON(http.StatusOK, gin.H{ + "liked": true, + "like_count": micro.LikeCount + 1, + }) + } else if likeErr == nil { + // 取消点赞 + if err := database.DB.Delete(&existingLike).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "取消点赞失败"}) + return + } + + // 更新点赞计数 + newCount := micro.LikeCount - 1 + if newCount < 0 { + newCount = 0 + } + database.DB.Model(µ).Update("like_count", newCount) + + c.JSON(http.StatusOK, gin.H{ + "liked": false, + "like_count": newCount, + }) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"}) + return + } +} + +// GetMicroHeatmap 获取热力图数据 +func (h *MicroHandler) GetMicroHeatmap(c *gin.Context) { + yearStr := c.DefaultQuery("year", strconv.Itoa(time.Now().Year())) + year, err := strconv.Atoi(yearStr) + if err != nil { + year = time.Now().Year() + } + + // 计算年份的起止日期 + startDate := time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC) + endDate := time.Date(year, 12, 31, 23, 59, 59, 0, time.UTC) + + // 按日期分组统计 + type DayCount struct { + Date string `json:"date"` + Count int `json:"count"` + } + + var results []DayCount + + database.DB.Model(&models.Micro{}). + Select("DATE(created_at) as date, COUNT(*) as count"). + Where("created_at >= ? AND created_at <= ? AND deleted_at IS NULL", startDate, endDate). + Group("DATE(created_at)"). + Find(&results) + + // 转换为 map + data := make(map[string]int) + for _, r := range results { + data[r.Date] = r.Count + } + + c.JSON(http.StatusOK, gin.H{ + "year": year, + "data": data, + }) +} + +// GetMicroStats 获取统计数据 +func (h *MicroHandler) GetMicroStats(c *gin.Context) { + var totalMicros int64 + var totalLikes int64 + var totalComments int64 + var monthMicros int64 + + // 总微语数 + database.DB.Model(&models.Micro{}).Count(&totalMicros) + + // 总点赞数 + database.DB.Model(&models.MicroLike{}).Count(&totalLikes) + + // 总评论数 + database.DB.Model(&models.MicroComment{}).Count(&totalComments) + + // 本月发布数 + now := time.Now() + monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) + database.DB.Model(&models.Micro{}).Where("created_at >= ? AND deleted_at IS NULL", monthStart).Count(&monthMicros) + + c.JSON(http.StatusOK, gin.H{ + "total_micros": totalMicros, + "total_likes": totalLikes, + "total_comments": totalComments, + "month_micros": monthMicros, + }) +} + +// GetMicroTags 获取热门标签 +func (h *MicroHandler) GetMicroTags(c *gin.Context) { + // 获取所有微语的标签 + var micros []models.Micro + database.DB.Where("tags IS NOT NULL AND tags != '' AND deleted_at IS NULL").Select("tags").Find(µs) + + // 统计标签使用次数 + tagCount := make(map[string]int) + for _, micro := range micros { + if micro.Tags == "" { + continue + } + var tags []string + if err := json.Unmarshal([]byte(micro.Tags), &tags); err == nil { + for _, tag := range tags { + tagCount[tag]++ + } + } + } + + // 转换为排序后的列表 + type TagItem struct { + Name string `json:"name"` + Count int `json:"count"` + } + + var tags []TagItem + for name, count := range tagCount { + tags = append(tags, TagItem{Name: name, Count: count}) + } + + // 按使用次数排序 + for i := 0; i < len(tags); i++ { + for j := i + 1; j < len(tags); j++ { + if tags[j].Count > tags[i].Count { + tags[i], tags[j] = tags[j], tags[i] + } + } + } + + // 只返回前20个 + if len(tags) > 20 { + tags = tags[:20] + } + + c.JSON(http.StatusOK, gin.H{ + "tags": tags, + }) +} + +// getIPHash 获取 IP 的哈希值 +func getIPHash(c *gin.Context) string { + ip := c.ClientIP() + hash := sha256.Sum256([]byte(ip + "micro-salt")) + return hex.EncodeToString(hash[:]) +} + +// CreateMicroCommentRequest 创建微语评论请求 +type CreateMicroCommentRequest struct { + MicroID uint `json:"micro_id" binding:"required"` + ParentID *uint `json:"parent_id"` + Content string `json:"content" binding:"required,min=1,max=2000"` +} + +// CreateMicroComment 创建微语评论 +func (h *MicroHandler) CreateMicroComment(c *gin.Context) { + userID, isLoggedIn := middleware.GetUserID(c) + if !isLoggedIn || userID == 0 { + c.JSON(http.StatusUnauthorized, gin.H{"error": "请登录后再评论"}) + return + } + + var req CreateMicroCommentRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 检查微语是否存在 + var micro models.Micro + if err := database.DB.First(µ, req.MicroID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "微语不存在"}) + return + } + + comment := models.MicroComment{ + MicroID: req.MicroID, + UserID: userID, + ParentID: req.ParentID, + Content: req.Content, + } + + if err := database.DB.Create(&comment).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "评论失败"}) + return + } + + // 更新评论计数 + database.DB.Model(µ).Update("comment_count", gorm.Expr("comment_count + 1")) + + // 加载用户信息 + database.DB.Preload("User").First(&comment, comment.ID) + + c.JSON(http.StatusCreated, comment) +} + +// GetMicroComments 获取微语评论列表 +func (h *MicroHandler) GetMicroComments(c *gin.Context) { + microIDStr := c.Query("micro_id") + if microIDStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "micro_id is required"}) + return + } + + microID, err := strconv.ParseUint(microIDStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid micro_id"}) + return + } + + // 分页参数 + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + // 获取顶级评论(非回复) + var comments []models.MicroComment + var total int64 + + query := database.DB.Model(&models.MicroComment{}). + Where("micro_id = ? AND parent_id IS NULL", microID) + + query.Count(&total) + + if err := query. + Preload("User"). + Preload("Replies.User"). + Order("created_at DESC"). + Offset((page - 1) * pageSize). + Limit(pageSize). + Find(&comments).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取评论失败"}) + return + } + + // 手动加载每个评论的用户信息 + for i := range comments { + if comments[i].UserID > 0 { + var user models.User + if err := database.DB.First(&user, comments[i].UserID).Error; err == nil { + comments[i].User = user + } + } + // 加载回复的用户信息 + for j := range comments[i].Replies { + if comments[i].Replies[j].UserID > 0 { + var replyUser models.User + if err := database.DB.First(&replyUser, comments[i].Replies[j].UserID).Error; err == nil { + comments[i].Replies[j].User = replyUser + } + } + } + } + + c.JSON(http.StatusOK, gin.H{ + "data": comments, + "pagination": gin.H{ + "page": page, + "page_size": pageSize, + "total": total, + "total_page": (total + int64(pageSize) - 1) / int64(pageSize), + }, + }) +} + +// DeleteMicroComment 删除微语评论 +func (h *MicroHandler) DeleteMicroComment(c *gin.Context) { + userID, _ := middleware.GetUserID(c) + role, _ := c.Get("role") + + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid comment id"}) + return + } + + var comment models.MicroComment + if err := database.DB.First(&comment, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "评论不存在"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + return + } + + // 检查权限:本人或管理员可删除 + if comment.UserID != userID && role != "admin" { + c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"}) + return + } + + // 软删除 + if err := database.DB.Delete(&comment).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"}) + return + } + + // 更新评论计数 + database.DB.Model(&models.Micro{}).Where("id = ?", comment.MicroID). + Update("comment_count", gorm.Expr("comment_count - 1")) + + c.JSON(http.StatusOK, gin.H{"message": "评论已删除"}) +} \ No newline at end of file diff --git a/server/internal/models/models.go b/server/internal/models/models.go index 87829e1..8e0769c 100644 --- a/server/internal/models/models.go +++ b/server/internal/models/models.go @@ -62,3 +62,41 @@ type PostMeta struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } + +// Micro 微语模型 +type Micro struct { + ID uint `json:"id" gorm:"primaryKey"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + UserID uint `json:"user_id" gorm:"index;not null"` + Content string `json:"content" gorm:"type:text;not null"` + Images string `json:"images" gorm:"type:text"` // JSON array of image URLs + Tags string `json:"tags" gorm:"type:text"` // JSON array of tags + LikeCount int `json:"like_count" gorm:"default:0"` + CommentCount int `json:"comment_count" gorm:"default:0"` + User User `json:"user" gorm:"foreignKey:UserID"` +} + +// MicroLike 微语点赞模型 +type MicroLike struct { + ID uint `json:"id" gorm:"primaryKey"` + CreatedAt time.Time `json:"created_at"` + MicroID uint `json:"micro_id" gorm:"uniqueIndex:idx_micro_user;not null"` + UserID *uint `json:"user_id" gorm:"uniqueIndex:idx_micro_user;index"` + IPHash string `json:"-" gorm:"uniqueIndex:idx_micro_ip;size:64"` +} + +// MicroComment 微语评论模型 +type MicroComment struct { + ID uint `json:"id" gorm:"primaryKey"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + MicroID uint `json:"micro_id" gorm:"index;not null"` + UserID uint `json:"user_id" gorm:"index;not null"` + ParentID *uint `json:"parent_id" gorm:"index"` + Content string `json:"content" gorm:"type:text;not null"` + User User `json:"user" gorm:"foreignKey:UserID"` + Replies []MicroComment `json:"replies,omitempty" gorm:"foreignKey:ParentID"` +} diff --git a/src/components/HeatmapCalendar.vue b/src/components/HeatmapCalendar.vue new file mode 100644 index 0000000..9be9a28 --- /dev/null +++ b/src/components/HeatmapCalendar.vue @@ -0,0 +1,230 @@ + + + + + \ No newline at end of file diff --git a/src/components/MicroCommentSection.vue b/src/components/MicroCommentSection.vue new file mode 100644 index 0000000..92e34a0 --- /dev/null +++ b/src/components/MicroCommentSection.vue @@ -0,0 +1,285 @@ + + + + + \ No newline at end of file diff --git a/src/components/MicroFeed.vue b/src/components/MicroFeed.vue new file mode 100644 index 0000000..9ac891a --- /dev/null +++ b/src/components/MicroFeed.vue @@ -0,0 +1,518 @@ + + + \ No newline at end of file diff --git a/src/components/MicroPost.vue b/src/components/MicroPost.vue new file mode 100644 index 0000000..e00955b --- /dev/null +++ b/src/components/MicroPost.vue @@ -0,0 +1,352 @@ + + + + + \ No newline at end of file diff --git a/src/components/MicroSidebar.vue b/src/components/MicroSidebar.vue new file mode 100644 index 0000000..c6ea8f8 --- /dev/null +++ b/src/components/MicroSidebar.vue @@ -0,0 +1,191 @@ + + + \ No newline at end of file diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 3570113..bee5cfd 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -112,6 +112,7 @@ const socialImageURL = image.startsWith('http') ? image : new URL(image, site).h