去提问»
提问背景 Question:
下面是,baike完整的go程序,里面有渲染的方式和相关可以参考的设计方式。模板文件名就定为sitemap.html吧。访问路由也就/baike/sitemap这样了。 package ai import ( "bytes" "database/sql" "html/template" "net/http" "path/filepath" "strconv" "strings" "stv/fun" "time" "github.com/julienschmidt/httprouter" "github.com/yuin/goldmark" "github.com/yuin/goldmark/renderer/html" ) // 定义全局单例实例 var md goldmark.Markdown func init() { // 在 init 函数中一次性完成初始化,避免每次调用时重复创建 md = goldmark.New( goldmark.WithRendererOptions( // html.WithHardWraps() 对应渲染器中的硬换行配置(将渲染为 <br />) html.WithHardWraps(), // 如果需要支持 HTML 标签(默认是转义的),可以加上这一行: // html.WithUnsafe(), ), ) } // Article 对应 cms_article 表及关联标签 type Article struct { ID int Question string Title string Description template.HTML Content template.HTML // 使用 template.HTML 避免 HTML 标签被转义 ClickCount int LikeCount int CreatedAt string Tags []Tag } // Tag 对应 cms_tag 标签表 type Tag struct { ID int Name string Count int } // BaikeIndexData 首页模板渲染数据 type BaikeIndexData struct { Articles []Article HotTags []Tag Keyword string } // BaikeDetailData 详情页模板渲染数据 type BaikeDetailData struct { Article Article Tags []Tag } // 1. 百科首页 Handler(带搜索支持) // 路由配置:r.GET("/baike", ai.Baike_index) func Baike_index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { query := r.URL.Query() keyword := query.Get("q") tagName := query.Get("tag") // 获取热门标签 (前20个) var hotTags []Tag rows, err := fun.DB.Query("SELECT id, name, count FROM cms_tag WHERE count > 0 ORDER BY count DESC LIMIT 20") if err == nil { for rows.Next() { var t Tag if err := rows.Scan(&t.ID, &t.Name, &t.Count); err == nil { hotTags = append(hotTags, t) } } rows.Close() } // 查询文章列表 var articles []Article var sqlStr string var args []interface{} if keyword != "" { // 关键词搜索模式 sqlStr = `SELECT id, title, description, click_count, created_at FROM cms_article WHERE status = 1 AND (title LIKE ? OR content LIKE ? OR question LIKE ?) ORDER BY id DESC LIMIT 50` likeKey := "%" + keyword + "%" args = append(args, likeKey, likeKey, likeKey) } else if tagName != "" { // 标签过滤模式 sqlStr = `SELECT a.id, a.title, a.description, a.click_count, a.created_at FROM cms_article a INNER JOIN cms_article_tag at ON a.id = at.article_id INNER JOIN cms_tag t ON t.id = at.tag_id WHERE a.status = 1 AND t.name = ? ORDER BY a.id DESC LIMIT 50` args = append(args, tagName) } else { // 默认最新列表 sqlStr = `SELECT id, title, description, click_count, created_at FROM cms_article WHERE status = 1 and score>80 ORDER BY id DESC LIMIT 15` } rows, err = fun.DB.Query(sqlStr, args...) if err != nil { http.Error(w, "读取数据库失败: "+err.Error(), http.StatusInternalServerError) return } defer rows.Close() for rows.Next() { var a Article var createdAtInt int64 err := rows.Scan(&a.ID, &a.Title, &a.Description, &a.ClickCount, &createdAtInt) if err == nil { // 格式化时间戳 a.CreatedAt = time.Unix(createdAtInt, 0).Format("2006-01-02 15:04") articles = append(articles, a) } } // 拼装首页数据 data := BaikeIndexData{ Articles: articles, HotTags: hotTags, Keyword: keyword, } // 渲染模板 path := filepath.Join(fun.AppPath, "/templates/ai/baike_index.html") mb, err := fun.TManager.Get(path) if err != nil { http.Error(w, "模板加载失败: "+err.Error(), http.StatusInternalServerError) return } err = mb.Execute(w, data) if err != nil { fun.WriteLog("渲染百科首页模板报错: %s", err) } } // 2. 百科详情页 Handler // 路由配置:r.GET("/baike/article/:id", ai.Baike_detail) func Baike_detail(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { articleIDStr := ps.ByName("id") articleID, err := strconv.Atoi(articleIDStr) if err != nil { http.NotFound(w, r) return } // 1. 查询文章主体 var a Article var descriptionRaw string // 用临时变量接收数据库中的原始 String var contentRaw string var createdAtInt int64 row := fun.DB.QueryRow( "SELECT id, question, title, description, content, click_count, like_count, created_at FROM cms_article WHERE id = ? AND status = 1", articleID, ) err = row.Scan(&a.ID, &a.Question, &a.Title, &descriptionRaw, &contentRaw, &a.ClickCount, &a.LikeCount, &createdAtInt) if err != nil { if err == sql.ErrNoRows { http.NotFound(w, r) } else { http.Error(w, "数据库错误: "+err.Error(), http.StatusInternalServerError) } return } a.Description = ConvertMarkdown(descriptionRaw) // 解析提问背景的 Markdown a.Content = ConvertMarkdown(contentRaw) //解析Markdown并转义template.HTML a.CreatedAt = time.Unix(createdAtInt, 0).Format("2006-01-02 15:04") // 2. 异步增加浏览量(避免阻塞主渲染) go func(id int) { _, _ = fun.DB.Exec("UPDATE cms_article SET click_count = click_count + 1 WHERE id = ?", id) }(articleID) // 3. 查询当前文章的 Tags var tags []Tag tagRows, err := fun.DB.Query(` SELECT t.id, t.name FROM cms_tag t INNER JOIN cms_article_tag at ON t.id = at.tag_id WHERE at.article_id = ?`, articleID, ) if err == nil { for tagRows.Next() { var t Tag if err := tagRows.Scan(&t.ID, &t.Name); err == nil { tags = append(tags, t) } } tagRows.Close() } data := BaikeDetailData{ Article: a, Tags: tags, } // 4. 渲染模板 path := filepath.Join(fun.AppPath, "/templates/ai/baike_detail.html") mb, err := fun.TManager.Get(path) if err != nil { http.Error(w, "详情模板渲染失败: "+err.Error(), http.StatusInternalServerError) return } err = mb.Execute(w, data) if err != nil { fun.WriteLog("渲染百科详情页模板报错: %s", err) } } func ConvertMarkdown(mdContent string) template.HTML { // 1. 将字面量的 "" (反斜杠+n) 替换为真正的换行符 "" mdContent = strings.ReplaceAll(mdContent, "\\n", "\n") // 2. 兼容性处理:将字面量的 "\\r" 替换为真正的 "\r" mdContent = strings.ReplaceAll(mdContent, "\\r", "\n") // 3. 兼容性处理:将 Windows 换行符统一替换为 Linux 换行符(可选,防患未然) mdContent = strings.ReplaceAll(mdContent, "\r", "\n") var buf bytes.Buffer if err := md.Convert([]byte(mdContent), &buf); err != nil { return template.HTML(mdContent) // 如果解析失败,回退返回原始文本 } return template.HTML(buf.String()) }

Go 模板网站地图实现方案与完整代码

发布时间:2026-08-19 21:03 👁️ 阅读:2

为了完美契合您之前设计的网站地图模板 (sitemap.html),并保证其在生产环境下的极高加载性能和 SEO 蜘蛛爬行效率,我们需要在 baike.go 中建立按标签(Tag)作为虚拟分类的数据分组聚合逻辑。

在搜索引擎蜘蛛(如 Googlebot、Baiduspider)的视角中,将文章按「标签目录」分类归档,并限制单页渲染的链接数(防止因单页超链接过多而被搜索引擎惩罚),是最标准的做法。

1. 新增的数据结构定义

请在您的 baike.go 文件中的 BaikeDetailData 下方添加以下供 sitemap.html 渲染使用的数据结构:

// CategoryGroup 对应 sitemap 模板中的分类节点
type CategoryGroup struct {
	CategoryName string
	Articles     []Article
}

// BaikeSitemapData 对应 sitemap 模板的渲染数据
type BaikeSitemapData struct {
	CategoryGroups []CategoryGroup
	HasPagination  bool
	CurrentPage    int
	TotalPages     int
	PageNums       []int
}

2. 编写 Baike_sitemap 处理函数

baike.go 的末尾添加该处理函数。此函数支持流式分页,每页默认展示 10 个标签分类,每个分类下聚合展示最新及权重较高的 30 篇百科文章

// 3. 百科网站地图/目录树 Handler
// 路由配置:r.GET("/baike/sitemap", ai.Baike_sitemap)
func Baike_sitemap(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	query := r.URL.Query()
	pageStr := query.Get("page")
	currentPage := 1
	if pageStr != "" {
		if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
			currentPage = p
		}
	}

	// 定义分页配置:每页显示 10 个分类标签
	pageSize := 10
	offset := (currentPage - 1) * pageSize

	// 1. 查询有文章的标签总数(用于分页计算)
	var totalTags int
	err := fun.DB.QueryRow("SELECT COUNT(1) FROM cms_tag WHERE count > 0").Scan(&totalTags)
	if err != nil {
		http.Error(w, "计算标签失败: "+err.Error(), http.StatusInternalServerError)
		return
	}

	// 计算总页数
	totalPages := (totalTags + pageSize - 1) / pageSize
	if totalPages < 1 {
		totalPages = 1
	}
	if currentPage > totalPages {
		currentPage = totalPages
		offset = (currentPage - 1) * pageSize
	}

	// 2. 分页获取热门/非空标签列表
	tagRows, err := fun.DB.Query("SELECT id, name, count FROM cms_tag WHERE count > 0 ORDER BY count DESC, id DESC LIMIT ? OFFSET ?", pageSize, offset)
	if err != nil {
		http.Error(w, "查询标签失败: "+err.Error(), http.StatusInternalServerError)
		return
	}
	defer tagRows.Close()

	var groups []CategoryGroup

	// 3. 循环获取每个标签分类下的高权重公开文章 (每组显示最新 30 篇)
	for tagRows.Next() {
		var t Tag
		if err := tagRows.Scan(&t.ID, &t.Name, &t.Count); err == nil {
			var articles []Article

			// 查询关联文章列表,必须满足 status = 1 正常状态
			articleRows, err := fun.DB.Query(`
				SELECT a.id, a.title 
				FROM cms_article a
				INNER JOIN cms_article_tag at ON a.id = at.article_id
				WHERE a.status = 1 AND at.tag_id = ?
				ORDER BY a.id DESC LIMIT 30`, 
				t.ID,
			)

			if err == nil {
				for articleRows.Next() {
					var a Article
					if err := articleRows.Scan(&a.ID, &a.Title); err == nil {
						articles = append(articles, a)
					}
				}
				articleRows.Close()
			}

			// 仅当分类下有文章才展示到目录树中
			if len(articles) > 0 {
				groups = append(groups, CategoryGroup{
					CategoryName: t.Name,
					Articles:     articles,
				})
			}
		}
	}

	// 生成分页页码数组
	var pageNums []int
	for i := 1; i <= totalPages; i++ {
		pageNums = append(pageNums, i)
	}

	data := BaikeSitemapData{
		CategoryGroups: groups,
		HasPagination:  totalPages > 1,
		CurrentPage:    currentPage,
		TotalPages:     totalPages,
		PageNums:       pageNums,
		}

	// 4. 读取模板文件并渲染
	path := filepath.Join(fun.AppPath, "/templates/ai/sitemap.html")
	mb, err := fun.TManager.Get(path)
	if err != nil {
		http.Error(w, "加载地图模板失败: "+err.Error(), http.StatusInternalServerError)
		return
	}

	err = mb.Execute(w, data)
	if err != nil {
		fun.WriteLog("渲染百科地图页模板报错: %s", err)
	}
}

3. 需要同步修改的页面模板细节 (sitemap.html 分页部分)

您之前的模板中分页使用了固定的静态 A 标签链接。为了让它随 Go 后端数据动态输出,请将 sitemap.html 中的分页区修改为以下标准的 Go 循环渲染逻辑:

      <!-- 分页区域:完美动态生成 -->
      {{if .HasPagination}}
      <div class="pagination">
        {{$curr := .CurrentPage}}
        {{range .PageNums}}
        <a href="?page={{.}}" class="page-btn{{if eq . $curr}} active{{end}}">{{.}}</a>
        {{end}}
      </div>
      {{end}}

4. 路由配置指引

在您项目注册 HTTP 路由的地方(通常在 main.go 或路由配置文件中),添加对应的 GET 路由指向新创建的方法即可:

// 注册百科全站 HTML 目录树(网站地图)
router.GET("/baike/sitemap", ai.Baike_sitemap)

为什么这个设计对搜索引擎极具吸引力?

  1. 极佳的语义层级:使用标准的 nav > h2 > ul > li > a 语义树,搜索引擎蜘蛛解析起来毫无障碍。
  2. 物理分页控流:控制单页分类数与文章显示上限,避免页面体积过大(控制在 100KB 以内),有利于蜘蛛的高频轻量抓取。
  3. 标签关联聚合:打破了传统单一死板的物理分类,让高关联的文章多入口曝光,显著缩短了蜘蛛发现新文章、未收录文章的物理路径。