Hugo 分类按页面数量排序:`Taxonomy.ByCount` 方法完整实战指南
2026/9/20 7:04:06 网站建设 项目流程
  • 开发工具
  • 前端
  • CLI

【免费下载链接】hugo

The world’s fastest framework for building websites.

项目地址:https://gitcode.com/gh_mirrors/hu/hugo
点击查看免费下载

本篇技术指南围绕 Hugo 中Taxonomy对象上的ByCount方法展开,讲解如何把无序的Taxonomy映射(map)转换为按每个分类项(term)关联页面数量降序排列的有序分类(OrderedTaxonomy),并处理数量相同时的平局排序。文章将从"捕获 Taxonomy 对象"开始,逐步演示ByCountReverse的用法,结合源码剖析排序算法与数据结构,并给出可直接复制的模板示例,读完即可在自己的 Hugo 站点中实现"页面最多的分类排在最前"的分类导航、标签云等功能。

ByCount是什么:从无序 Map 到有序切片

在 Hugo 中,一个Taxonomy对象本质上是一个 Go 的map——term 名到加权页面列表(WeightedPages)的映射。正如源码 resources/page/taxonomy.go 中定义的:

// A Taxonomy is a map of keywords to a list of pages. // For example // // TagTaxonomy['technology'] = WeightedPages // TagTaxonomy['go'] = WeightedPages type Taxonomy map[string]WeightedPages

Go 的map无序的,每次遍历的顺序都不固定,因此无法直接用于需要稳定顺序的模板渲染。ByCount方法正是为此而生:它返回一个OrderedTaxonomy——一个切片(slice),其中每个元素都包含 term 名称和它关联的加权页面切片:

// OrderedTaxonomy is another representation of an Taxonomy using an array rather than a map. // Important because you can't order a map. type OrderedTaxonomy []OrderedTaxonomyEntry // OrderedTaxonomyEntry is similar to an element of a Taxonomy, but with the key embedded (as name) // e.g: {Name: Technology, WeightedPages: TaxonomyPages} type OrderedTaxonomyEntry struct { Name string WeightedPages }

ByCount的排序规则非常明确(见 resources/page/taxonomy.go 的注释与实现):

  • 首先按每个 term 关联的页面数量从多到少(降序)排序;
  • 如果两个 term 关联的页面数量相同,则按 term 名称**字母序(升序)**排列。

其签名与返回类型如下:

项目说明
方法签名TAXONOMY.ByCount(作用于Taxonomy对象)
返回类型page.OrderedTaxonomy
排序主键term 关联的页面数量,降序
平局规则term 名称,字母升序

准备工作:捕获一个Taxonomy对象

要调用ByCount,首先需要在模板中拿到一个Taxonomy对象。以下两种方式都可以(内容来自 get-a-taxonomy-object.md)。

项目配置与内容结构

假设hugo.toml中配置了分类:

[taxonomies] genre = 'genres' author = 'authors'

内容目录结构如下:

content/ ├── books/ │ ├── and-then-there-were-none.md --> genres: suspense │ ├── death-on-the-nile.md --> genres: suspense │ └── jamaica-inn.md --> genres: suspense, romance │ └── pride-and-prejudice.md --> genres: romance └── _index.md

可以看到suspense关联 3 个页面,romance关联 2 个页面(其中jamaica-inn.md同时属于两个分类)。

方式一:通过Site对象获取

在任意模板中,使用Site对象上的Taxonomies方法捕获 "genres" 分类:

{{ $taxonomyObject := .Site.Taxonomies.genres }}

方式二:在 taxonomy 模板中通过页面Data获取

当渲染分类页(taxonomy模板)时,页面Data对象的Terms方法同样能拿到该分类的Taxonomy对象:

{{ $taxonomyObject := .Data.Terms }}

检查数据结构

想确认拿到的对象长什么样,可以用debug.Dump输出其完整结构:

<pre>{{ debug.Dump $taxonomyObject }}</pre>

对比:直接遍历Taxonomy对象

虽然AlphabeticalByCount为 range 遍历提供了更友好的数据结构,但也可以直接从Taxonomy对象渲染每个 term 的加权页面:

{{ range $term, $weightedPages := $taxonomyObject }} <h2><a href="{{ .Page.RelPermalink }}">{{ .Page.LinkTitle }}</a></h2> <ul> {{ range $weightedPages }} <li><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></li> {{ end }} </ul> {{ end }}

上面的示例中,第一个锚点元素链接到 term 页面。注意此方式无法保证顺序,这正是ByCount的用武之地。

获取按页面数量排序的有序分类

拿到 "genres" 的Taxonomy对象后,只需一行代码即可得到按关联页面数量排序的有序分类:

{{ $taxonomyObject.ByCount }}

反转排序顺序

ByCount默认是数量降序(多 → 少)。若需要升序,链式调用Reverse方法即可:

{{ $taxonomyObject.ByCount.Reverse }}

ReverseOrderedTaxonomy上的方法,其实现就在 resources/page/taxonomy.go 中,直接对切片元素做首尾交换:

// Reverse reverses the order of the entries in this taxonomy. func (t OrderedTaxonomy) Reverse() OrderedTaxonomy { for i, j := 0, len(t)-1; i < j; i, j = i+1, j-1 { t[i], t[j] = t[j], t[i] } return t }

检查排序后的结构

同样可以用debug.Dump检查ByCount返回的数据结构:

<pre>{{ debug.Dump $taxonomyObject.ByCount }}</pre>

有序分类的元素结构:每个元素提供哪些方法

OrderedTaxonomy是切片,其中每个元素都是一个对象,包含 term 名称以及该 term 的加权页面切片(详见 ordered-taxonomy-element-methods.md)。每个元素提供以下方法:

方法返回类型说明
Countint返回该 term 被分配到的页面数量
Pagepage.Page返回该 term 的Page对象,常用于链接到 term 页面
Pagespage.Pages返回包含该 term 下所有Page对象的Pages对象,按分类权重(taxonomic weight)排序;可以使用Pages对象提供的任意方法(如按最后修改日期排序)进行排序或分组
Termstring返回 term 名称
WeightedPagespage.WeightedPages返回该 term 下按分类权重排序的加权页面切片;Pages方法更灵活,支持任意排序与分组

从源码看,这些方法的实现非常直接(resources/page/taxonomy.go):

// Pages returns the Pages for this taxonomy. func (ie OrderedTaxonomyEntry) Pages() Pages { return ie.WeightedPages.Pages() } // Count returns the count the pages in this taxonomy. func (ie OrderedTaxonomyEntry) Count() int { return len(ie.WeightedPages) } // Term returns the name given to this taxonomy. func (ie OrderedTaxonomyEntry) Term() string { return ie.Name }

其中Count就是len(ie.WeightedPages)——这也再次印证了ByCount的排序键本质上是每个 term 的加权页面切片长度。

完整示例:渲染"页面最多的分类在前"的分类列表

以下模板(示例来自 ByCount.md)遍历按页面数量降序排列的分类,先渲染 term 链接与数量,再渲染该 term 下的页面列表(按标题排序):

{{ range $taxonomyObject.ByCount }} <h2><a href="{{ .Page.RelPermalink }}">{{ .Page.LinkTitle }}</a> ({{ .Count }})</h2> <ul> {{ range .Pages.ByTitle }} <li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li> {{ end }} </ul> {{ end }}

针对前文的内容结构,Hugo 渲染结果如下:

<h2><a href="/genres/suspense/">suspense</a> (3)</h2> <ul> <li><a href="/books/and-then-there-were-none/">And then there were none</a></li> <li><a href="/books/death-on-the-nile/">Death on the nile</a></li> <li><a href="/books/jamaica-inn/">Jamaica inn</a></li> </ul> <h2><a href="/genres/romance/">romance</a> (2)</h2> <ul> <li><a href="/books/jamaica-inn/">Jamaica inn</a></li> <li><a href="/books/pride-and-prejudice/">Pride and prejudice</a></li> </ul>

注意观察几个细节:

  • suspense有 3 个页面,排在romance(2 个页面)之前,符合降序规则;
  • {{ .Count }}直接输出数字用于展示,{{ .Page.RelPermalink }}{{ .Page.LinkTitle }}用于链接到 term 页面;
  • 子列表通过.Pages.ByTitle按标题排序,不受分类权重影响。

源码深挖:ByCount的排序算法是如何工作的

ByCount的完整实现位于 resources/page/taxonomy.go:

// ByCount returns an ordered taxonomy sorted by # of pages per key. // If taxonomies have the same # of pages, sort them alphabetical func (i Taxonomy) ByCount() OrderedTaxonomy { count := func(i1, i2 *OrderedTaxonomyEntry) bool { li1 := len(i1.WeightedPages) li2 := len(i2.WeightedPages) if li1 == li2 { return compare.LessStrings(i1.Name, i2.Name) } return li1 > li2 } ia := i.TaxonomyArray() oiBy(count).Sort(ia) return ia }

关键实现细节

  1. TaxonomyArray()把 map 转为切片:遍历 map 的每个键值对,构造OrderedTaxonomyEntry{Name: k, WeightedPages: v},得到切片ia(resources/page/taxonomy.go)。
  2. 比较闭包(closure)count闭包先比较两个 entry 的WeightedPages长度;长度相等时退回比较 term 名称。
  3. 稳定排序:排序通过oiBy(count).Sort(ia)完成,最终调用sort.Stable(resources/page/taxonomy.go)。稳定排序保证在比较器认为相等(数量相同且名称相同)的情况下,元素的原始相对顺序得以保留。

平局时的字符串比较:compare.LessStrings

平局时的名称比较调用compare.LessStrings,其实现位于 compare/compare_strings.go:

// LessStrings returns whether s is less than t lexicographically. func LessStrings(s, t string) bool { return Strings(s, t) < 0 }

Strings的排序语义值得注意(compare/compare_strings.go):

// Strings returns an integer comparing two strings lexicographically. func Strings(s, t string) int { c := compareFold(s, t) if c == 0 { // "B" and "b" would be the same so we need a tiebreaker. return strings.Compare(s, t) } return c }

也就是说,名称比较是大小写不敏感的(compareFold借鉴了 Go 标准库strings.EqualFold的思路,逐 rune 比较并处理 Unicode 折叠),当两个名称仅大小写不同(如"B""b")时,再退回到区分大小写的strings.Compare作为决胜规则。这意味着ByCount的名称平局排序也是 Unicode 友好的,可以正确处理非 ASCII 字符。

一个值得对比的差异:Alphabetical

ByCount的平局排序用的是compare.LessStrings(大小写不敏感 + tiebreaker),而分类目录中的另一个方法Alphabetical(见 docs/content/en/methods/taxonomy/Alphabetical.md)使用的是语言感知的 collator(langs.GetCollator1,见 resources/page/taxonomy.go),支持按当前站点语言的排序规则比较。因此:

  • 需要"数量优先、名称兜底"的场景,用ByCount
  • 需要纯语言化字典序的场景,用Alphabetical

测试用例印证

仓库的测试 hugolib/taxonomy_test.go 直接验证了ByCount的行为:在只有一页、且 tag 为['a', 'B', 'c']的情况下,遍历s.Taxonomies()["tags"].ByCount()得到的顺序是a:aB:bc:c——由于三个 term 页面数量相同(均为 1),排序完全退化为名称比较,结果与compare.LessStrings的大小写不敏感规则一致('B''b'视作相等后再按原始字节序决出先后)。

此外,tpl/collections/collections_integration_test.go 与 hugolib/taxonomy_test.go 也展示了在模板中直接使用{{ range .Data.Terms.ByCount }}的集成测试场景,可作为实践参考。

实战建议与注意事项

  • map 不可排序,先转换再遍历:任何需要确定顺序的分类渲染都应先调用ByCount(或Alphabetical),不要直接rangeTaxonomy对象。
  • 升序需求用ReverseByCount.Reverse会反转整个切片,实现数量从少到多;若同时对子列表也需要反转,请在Pages对象上单独处理。
  • 展示数量:元素上的Count方法返回int,可直接输出或参与条件判断(如只显示页面数大于 1 的分类)。
  • 子页面排序独立ByCount只决定 term 的排列顺序;term 下的页面排序默认按分类权重,如需按标题、日期等排序,请在Pages上继续链式调用(如.Pages.ByTitle.Pages.ByLastmod)。
  • 多语言站点:平局时的名称排序对 Unicode 友好;若需要完全遵循站点语言的排序规则,请使用Alphabetical

通过ByCount,你可以快速实现"热门分类优先"的标签云、分类导航或站点地图等常见功能——只需在Taxonomy对象上调用这一个方法,剩下的排序逻辑由 Hugo 在底层完成。

  • 开发工具
  • 前端
  • CLI

【免费下载链接】hugo

The world’s fastest framework for building websites.

项目地址:https://gitcode.com/gh_mirrors/hu/hugo
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询