- 开发工具
- 前端
- CLI
【免费下载链接】hugo
The world’s fastest framework for building websites.
本篇技术指南围绕 Hugo 中Taxonomy对象上的ByCount方法展开,讲解如何把无序的Taxonomy映射(map)转换为按每个分类项(term)关联页面数量降序排列的有序分类(OrderedTaxonomy),并处理数量相同时的平局排序。文章将从"捕获 Taxonomy 对象"开始,逐步演示ByCount、Reverse的用法,结合源码剖析排序算法与数据结构,并给出可直接复制的模板示例,读完即可在自己的 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]WeightedPagesGo 的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对象
虽然Alphabetical和ByCount为 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 }}Reverse是OrderedTaxonomy上的方法,其实现就在 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)。每个元素提供以下方法:
| 方法 | 返回类型 | 说明 |
|---|---|---|
Count | int | 返回该 term 被分配到的页面数量 |
Page | page.Page | 返回该 term 的Page对象,常用于链接到 term 页面 |
Pages | page.Pages | 返回包含该 term 下所有Page对象的Pages对象,按分类权重(taxonomic weight)排序;可以使用Pages对象提供的任意方法(如按最后修改日期排序)进行排序或分组 |
Term | string | 返回 term 名称 |
WeightedPages | page.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 }关键实现细节
TaxonomyArray()把 map 转为切片:遍历 map 的每个键值对,构造OrderedTaxonomyEntry{Name: k, WeightedPages: v},得到切片ia(resources/page/taxonomy.go)。- 比较闭包(closure):
count闭包先比较两个 entry 的WeightedPages长度;长度相等时退回比较 term 名称。 - 稳定排序:排序通过
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:a、B:b、c: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对象。 - 升序需求用
Reverse:ByCount.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.
相关推荐
Hugo 模板中的 Pages.GroupByParam 方法:按页面参数分组与排序的完整实战指南
Hugo 模板中的 Pages.GroupByParam 方法:按页面参数分组与排序的完整实战指南 本篇技术指南聚焦 Hugo 模板系统中 Pages.Grou
开发工具前端CLIHugo 页面集合按发布日期排序:ByPublishDate 方法完整指南
Hugo 页面集合按发布日期排序:ByPublishDate 方法完整指南 导读 ByPublishDate 是 Hugo 为 Pages 页面集合提供的一组排
开发工具前端CLIHugo 页面集合排序方法 ByDate 完全指南:按日期升序排序与 Reverse 降序实战
Hugo 页面集合排序方法 ByDate 完全指南:按日期升序排序与 Reverse 降序实战 ByDate 是 Hugo 模板系统中 Pages 集合类型的一
开发工具前端CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考