惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tailwind CSS Blog
C
CERT Recently Published Vulnerability Notes
P
Proofpoint News Feed
Vercel News
Vercel News
博客园 - 三生石上(FineUI控件)
IT之家
IT之家
Help Net Security
Help Net Security
月光博客
月光博客
N
News and Events Feed by Topic
Cloudbric
Cloudbric
博客园 - 司徒正美
L
LangChain Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Tenable Blog
The Register - Security
The Register - Security
The Hacker News
The Hacker News
I
InfoQ
The Last Watchdog
The Last Watchdog
MyScale Blog
MyScale Blog
Schneier on Security
Schneier on Security
WordPress大学
WordPress大学
小众软件
小众软件
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
宝玉的分享
宝玉的分享
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
K
Kaspersky official blog
L
LINUX DO - 热门话题
N
News | PayPal Newsroom
F
Fortinet All Blogs
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Security @ Cisco Blogs
Recorded Future
Recorded Future
大猫的无限游戏
大猫的无限游戏
H
Help Net Security
Google Online Security Blog
Google Online Security Blog
S
Schneier on Security
C
Cisco Blogs
N
News and Events Feed by Topic
V2EX - 技术
V2EX - 技术
Latest news
Latest news
PCI Perspectives
PCI Perspectives
T
The Blog of Author Tim Ferriss
P
Palo Alto Networks Blog
T
Tor Project blog
Project Zero
Project Zero
云风的 BLOG
云风的 BLOG
Webroot Blog
Webroot Blog
Attack and Defense Labs
Attack and Defense Labs
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org

皓子的小站

网站字体应用之坑——font-family 篇 糟糕的 meta name="theme-color" How to Automatically Track Newly Supported ESLint Rules in Oxlint How to Gradually Migrate from ESLint to Oxlint (Without Breaking Everything) 静态资源预压缩:零运行时开销,极致节省带宽 CVE-2025-70886 Proof of Concept (PoC) | A Script to Crash Halo CMS Comment Backend 自定义网页鼠标指针——一段曲折的旅程 CVE-2025-70886 漏洞复现/PoC | 一个脚本让 Halo CMS 评论后台瘫痪 2025 年终总结 & 博客两周年 老用户专享!已有 Halo 专业版授权可免费升级商业版 自动追踪 Oxlint 对 ESLint 规则的新增支持 Halo 贡献者证书与实体周边盲盒开箱 博客评论系统指南 CDN 回源跟随配置导致登录异常问题排查 SCDN 免费赞助计划:助力高质量博客&博客圈 锐捷校园网:网络共享与带宽叠加方案(哈理工案例) ESLint 到 Oxlint 渐进式迁移快速上手指南 Fixing Vite Breaking Inline JS & CSS in Thymeleaf Templates 解决 Vite 破坏 Thymeleaf 模板内联 JS & CSS 的方法 我的博客,为什么是月更? 博客俱乐部一周年纪念品开箱 网站字体加载之坑——format 篇 经历 1000000000 次 DDoS 请求攻击后,我总结了三条经验 题解分享:[AtCoder Beginner Contest 414 E] Count A%B=C 题解分享:[蓝桥杯 2025 国 Python A] 杨辉三角 P12876 FiF口语训练破解刷分教程(适用于 Windows) 不要与蠢人辩论 小心网络地雷·续篇 当心网络组织“开往”·续篇 当心网络组织“开往” 小心网络地雷 回应某博客上有关“开往”组织在内部会议上对我的污蔑(上) 再遭 DDoS 攻击:CDN 流量限额拦不住请求计费飙升 2025年团体程序设计天梯赛 - Python 个人赛时暴力题解 网站流量分析工具 Umami 中国大陆合规版发布 把握当下,享受生活 Folo 认证功能反成漏洞?警惕 RSS 订阅源所有权被恶意盗取风险 在 Halo CMS 中通过模板实现随机文章跳转功能 从沉默展柜到时代回响:731部队罪证陈列馆参访手记 脚本分享:CDN 流量触顶后自动切换至 Cloudflare 官方 BUG 快薅!Halo 永久/买断用户可额外获得一份买断许可 老用户专享!已有 1Panel 永久授权/买断许可用户可再领免费买断许可证! 限时特惠!1¥ 立即解锁 1Panel 专业版全功能 61 天 混合云架构下的动态 IPv6 智能加速实践:从家庭网络到全球分发的低成本部署指南 如何用 AI 全方面辅助博客创作 - 从博文之内到博文之外 Random Number Generation and Formatting in Thymeleaf Thymeleaf 随机数生成与格式化详解 列举我维护的项目和发布的软件包 快速部署教程 - 通过 Webhook 触发邮件通知的开源自托管服务 Going Against the Grain - Four Websites That Make Your URL Longer 反其道而行 - 四个让你网址变得更长的网站 2024 年终总结 & 博客一周年 基于 OneBot API 的简易异步 QQ 群批量发消息脚本 如何快速完成中文排版优化 如何给文章分类和打标签 群规之敏感违禁词对抗 一图展示​ ​RSS3 世界 - The Open(RSS3) Universe 开源项目发到 bilibili 后可能获得的 PR C/C++:为什么 a[i][j] 等价于 *(*(a+i)+j) Fast Methods to Convert Uppercase Letters to Lowercase in C++ C++ 中快速将字符串大写字母转为小写的方法 Fixing GitHub Actions push to ghcr '403 Forbidden' error Fixing docker pull image "ghcr.io..." Unauthorized error 本站“算法题练习/题解分享”相关文章汇总 不怕流氓多,就怕流氓有文化——站长应该防范的几个问题及对应解决方法分享 题解分享:HRBUST 1132 水数 新手程序员必备的三项技能 如何快速排查插件(模组)的兼容性问题(基于二分法和分治思想) 通过 Umami 最新功能观察定时发布文章的最佳时段 踩坑分享:如何一个人搬运 126.7 斤的书坐动车 两篇文章分享:别像弱智一样提问 & 提问的智慧 论下落式音游/定轨音游中的优质粪铺(文末附铺面推荐) 为什么选择无纸化阅读?为什么买墨水屏电子书?为什么 Kindle“失败”了? 我维护的 Halo 主题,现已发布到 Halo 应用商店! OSU!Standard(osu!std)/OSU!Taiko 转 OSU!Mania 铺面转换器发布 我常用的 OSU!Mania 皮肤分享 电荷守恒辨析:为什么一个离子带多个电荷要乘上所带电荷数而不是除以所带电荷数 注意!align-items: flex-start 会导致 overflow-wrap: break-word 失效 惊人 98 元限定T恤!1Panel, Halo 开发商开启推广积分奖励活动 Minecraft Spigot Bukkit 服务器插件开发 教程收集 中文博客倡议 Windows 10 遇到“只能重装系统”但不想重装的解决方案 Windows 10 文件系统错误 -2144926975 -2147219196 修复 Windows 10 升级 Windows 11 简明教程 CSS 预处理器如何正确的复用样式(stylus 中使用 @extend 继承的坑) 请一定要回复博客下的匿名评论!对于博主和评论者有关匿名评论的倡议 一图解释单一质点运动波动图像和振动图像的关系 解决 Docker pull image "ghcr.io..." Unauthorized error 解决 GitHub Actions push to ghcr '403 Forbidden' error 解决 Umami API 更新延迟问题(如何修改开源项目代码并构建 Docker 镜像-以 Umami 为例) 在国家公祭日自动将网站变灰 中文博客圈列表 解决 Halo CMS 的 Umami Plugin CORS&CSP 跨域报错问题 Windows 创建存储池报错 无法创建池 不支持该请求 (0x00000032) Windows 显示器突然黑白 桌面突然黑白 鼠标和截图还是彩色的 通过指令过滤出正确的公网 IPv6 地址(记一次折腾 IPv6 DDNS) 我的世界免费科技地址 1.7-1.21 网站 Favicon 下载工具发布 键盘 Bios 内可用 系统内无法输入 设备管理器无黄色感叹号 设备管理器 网卡一堆黄色感叹号 例如 WAN, Miniport
在 Halo CMS 中通过模板实现随机推荐多篇文章
HowieHz · 2025-05-09 · via 皓子的小站

前言

仅需要随机获取一篇文章请看:在 Halo CMS 中通过模板实现随机文章跳转功能
本文着重于随机获取多篇文章,以及根据指定条件过滤结果。

本文实现了两完整示例:

  1. 获取多篇随机文章。
  2. 在第一个示例的基础上,按当前页面文章第一个分类过滤结果。

笔者将先分享完整示例,之后拆解示例,逐层讲解。

完整代码示例

模板代码使用了两配置项:

  • theme.config?.post_styles?.is_post_recommended_articles_show 控制是否启用推荐文章
  • theme.config?.post_styles?.post_recommended_articles_count 控制推荐文章数。

示例配置文件如下:

# settings.yaml
spec:
  forms:
	- group: post_styles
	  label: 文章页样式
	  formSchema:
		- $formkit: checkbox
		  name: is_post_recommended_articles_show
		  label: 文章底部的推荐文章
		  value: false
		  help: 开启后将在文章底部显示推荐文章列表
		- $formkit: number
		  name: post_recommended_articles_count
		  if: "$is_post_recommended_articles_show === true"
		  label: 推荐文章数量
		  value: 3
		  min: 1
		  max: 10
		  help: 设置文章底部显示的推荐文章数量

模板代码示例如下:
(这段代码是设计放置在文章页模板,即 /templates/post.html。如果这段模板代码不是放置在文章页模板,可以将 th:if="${#lists.size(firstPagePostList) > 1}" 中的 > 1 改为 > 0,并且要去除 <div th:if="${post.metadata.name != iterPost.metadata.name}"> .. </div>th:if 属性。具体含义会在下文解释。)

<th:block
  th:if="${theme.config?.post_styles?.is_post_recommended_articles_show}"
  th:with="n=${#conversions.convert(theme.config?.post_styles?.post_recommended_articles_count, 'java.lang.Integer')},
              postFinderResult=${postFinder.list({
                size: n,
                sort: {'spec.publishTime,desc', 'metadata.creationTimestamp,asc'}
              })},
              firstPagePostList=${postFinderResult.items}"
>
  <th:block
    th:if="${#lists.size(firstPagePostList) > 1}"
    th:with="randomPageNumber=${T(java.lang.Math).floor(T(java.lang.Math).random()*(postFinderResult.totalPages)+1)},
              targetPagePostFinderResult=${postFinder.list({
                page: randomPageNumber,
                size: n,
                sort: {'spec.publishTime,desc', 'metadata.creationTimestamp,asc'}
              })},
              targetPagePostList=${targetPagePostFinderResult.items},"
  >
    <th:block th:each="iterPost: ${targetPagePostList}">
      <div th:if="${post.metadata.name != iterPost.metadata.name}">
        <time
          th:text="${#temporals.format(iterPost.spec?.publishTime, 'yyyy-MM-dd')}"
        >文章发布时间替换位</time>
        <a
          th:href="@{${iterPost.status?.permalink}}" 
          th:text="${iterPost.spec?.title}"
        >文章超链接替换位(显示文字为标题/超链接为文章链接)</a>
      </div>
    </th:block>

    <th:block
      th:if="${targetPagePostFinderResult.last and not targetPagePostFinderResult.first}"
      th:with="itemsNeeded=${n-#lists.size(targetPagePostList)}"
    >
      <!--/* 缺项则补 */-->
      <th:block th:if="${itemsNeeded > 0}">
        <th:block th:each="index : ${#numbers.sequence(0,itemsNeeded-1)}">
          <th:block th:with="iterPost=${firstPagePostList[index]}">
            <div th:if="${post.metadata.name != iterPost.metadata.name}">
              <time
                th:text="${#temporals.format(iterPost.spec?.publishTime, 'yyyy-MM-dd')}"
              >文章发布时间替换位</time>
              <a
                th:href="@{${iterPost.status?.permalink}}"
                th:text="${iterPost.spec?.title}"
              >文章超链接替换位(显示文字为标题/超链接为文章链接)</a>
            </div>
          </th:block>
        </th:block>
      </th:block>
    </th:block>
  </th:block>
</th:block>

讲解代码示例

第一层

<th:block
  th:if="${theme.config?.post_styles?.is_post_recommended_articles_show}"
  th:with="n=${#conversions.convert(theme.config?.post_styles?.post_recommended_articles_count, 'java.lang.Integer')},
              postFinderResult=${postFinder.list({
                size: n,
                sort: {'spec.publishTime,desc', 'metadata.creationTimestamp,asc'}
              })},
              firstPagePostList=${postFinderResult.items}"
>
	<!-- ... -->
</th:block>

th:if="${theme.config?.post_styles?.is_post_recommended_articles_show}":读取 theme.config?.post_styles?.is_post_recommended_articles_show 控制是否启用推荐文章。
th:with 初始化了多个变量:

  • n 读取 theme.config?.post_styles?.post_recommended_articles_count 用于控制推荐文章数。
  • postFinderResult 使用 Halo CMS 提供的 Finder API 中的 list({...}) 获取文章列表数据(查询参数设置了分页条数和排序字段。排序字段无要求;分页条数必须为 n,保证随机出的文章数接近要求数。参数含义详情请参考官方文档),变量类型为 #ListResult<ListedPostVo>
  • firstPagePostList 保存 postFinderResult 的文章列表数据,变量类型为 List<ListedPostVo>。

第二层

<th:block
    th:if="${#lists.size(firstPagePostList) > 1}"
    th:with="randomPageNumber=${T(java.lang.Math).floor(T(java.lang.Math).random()*(postFinderResult.totalPages)+1)},
              targetPagePostFinderResult=${postFinder.list({
                page: randomPageNumber,
                size: n,
                sort: {'spec.publishTime,desc', 'metadata.creationTimestamp,asc'}
              })},
              targetPagePostList=${targetPagePostFinderResult.items},"
>
  	<!-- ... -->
</th:block>

使用 #lists.size 检查 firstPagePostList 变量保存的文章数据是否大于 1,如果大于 1 则进入下一层,否则不进行文章推荐。(这段代码原本是设计放置在文章页模板,即 /templates/post.html。如果等于 1,说明当前站点仅有一篇文章,无需进行重复推荐。如果这段模板代码不是放置在文章页模板,可以将 > 1 改为 > 0。)
th:with 初始化了多个变量:

  • randomPageNumber:为 Thymeleaf 随机数生成与格式化详解(整数/小数/浮点数) 的应用示例,根据一开始查询结果的总页码数,来随机生成一个页码。
  • targetPagePostFinderResult:使用 Halo CMS 提供的 Finder API 中的 list({...}) 获取文章列表数据(查询参数设置了目标页码、分页条数和排序字段。目标页码为随机出的页码 randomPageNumber;排序字段无要求;分页条数必须为 n,保证随机出的文章数接近要求数。参数含义详情请参考官方文档),变量类型为 #ListResult<ListedPostVo>
  • targetPagePostList:保存 targetPagePostFinderResult 的文章列表数据,变量类型为 List<ListedPostVo>。

第三层

第三层第一部分

<th:block th:each="iterPost: ${targetPagePostList}">
  <div th:if="${post.metadata.name != iterPost.metadata.name}">
	<time
	  th:text="${#temporals.format(iterPost.spec?.publishTime, 'yyyy-MM-dd')}"
    >文章发布时间替换位</time>
	<a
	  th:href="@{${iterPost.status?.permalink}}" 
	  th:text="${iterPost.spec?.title}"
    >文章超链接替换位(显示文字为标题/超链接为文章链接)</a>
  </div>
</th:block>

使用 th:each 遍历 targetPagePostList
使用 th:if="${post.metadata.name != iterPost.metadata.name}" 避免推荐列表中出现当前文章(这段代码原本是设计放置在文章页模板,即 /templates/post.html。如果这段模板代码不是放置在文章页模板,请去除这个 th:if 属性)。
最内层使用一个 <time> 标签和一个 <a> 标签展示文章信息。

第三层第二部分

<th:block
  th:if="${targetPagePostFinderResult.last and not targetPagePostFinderResult.first}"
  th:with="itemsNeeded=${n-#lists.size(targetPagePostList)}"
>
  <!-- ... -->
</th:block>

使用 th:if 检查 targetPagePostFinderResult 属性:如果是最后一页,而且不是第一页,就进行补偿检查。

  • 为何需要进行补偿检查:如果总文章数不能被 n 整除导致最后一页查询结果会小于 n
  • 为何是 targetPagePostFinderResult.last and not targetPagePostFinderResult.first 为才进行补偿检查:
    • 如果查询结果不是最后一页,不进入补偿检查。
      • 不会出现不能整除导致缺少的情况。
      • 最多因为查询结果中有当前文章,然后被 th:if="${post.metadata.name != iterPost.metadata.name}" 过滤,导致最后展示数为 n-1
      • 由于内层变量无法传递到外层,所以解决 n-1 会使得代码比较复杂:判断如果 post.metadata.name == iterPost.metadata.name 成立,就多补偿一篇。补偿的时候也要进行检查,防止多补偿的一篇文章依然为当前文章。
      • 如果这段模板代码不是放置在文章页模板,去除了 th:if=${post.metadata.name != iterPost.metadata.name} 则不会出现展示数为 n-1 的问题。
    • 如果查询结果是最后一页,也是第一页,不进入补偿检查。
      • 说明查询结果只有一页,总文章数小于 n,无需进行补偿
    • 如果是最后一页,而且不是第一页,就进行补偿检查。
      th:with 初始化一个变量:
    • itemsNeeded:保存需要补偿的文章数,通过计算 n 减去实际查询结果
第三层第二部分内层 - 补偿显示部分
<th:block th:if="${itemsNeeded > 0}">
	<th:block th:each="index : ${#numbers.sequence(0,itemsNeeded-1)}">
	  <th:block th:with="iterPost=${firstPagePostList[index]}">
		<div th:if="${post.metadata.name != iterPost.metadata.name}">
		  <time
			th:text="${#temporals.format(iterPost.spec?.publishTime, 'yyyy-MM-dd')}"
          >文章发布时间替换位</time>
		  <a
			th:href="@{${iterPost.status?.permalink}}"
			th:text="${iterPost.spec?.title}"
          >文章超链接替换位(显示文字为标题/超链接为文章链接)</a>
		</div>
	  </th:block>
	</th:block>
  </th:block>

如果 itemsNeeded 大于 0,才进行之后的补偿。
使用 #numbers.sequence 创建索引序列,遍历从 0 到 itemsNeeded-1
复用 firstPagePostList 节约查询次数(这就是为什么笔者将两次查询填写了相同的 sort 参数)。展示 firstPagePostList 中索引数从 0 到 itemsNeeded-1 的文章数据。
使用 th:if="${post.metadata.name != iterPost.metadata.name}" 避免推荐列表中出现当前文章(这段代码原本是设计放置在文章页模板,即 /templates/post.html。如果这段模板代码不是放置在文章页模板,请去除这个 th:if 属性)。
最内层展示方法同第三层第一部分,使用一个 <time> 标签和一个 <a> 标签展示文章信息。

完整模板代码示例(按当前文章第一个分类过滤结果)

此处对上述代码进行了增强,仅选取当前文章第一个分类的文章。
需放置于模板 /templates/post.html
后文详细讲解仅讲解新增代码。

<!--/* 根据文章的第一个类别,找相同类别的文章 */-->
<!--/* 文章无分类则不进行推荐 */-->
<th:block
  th:if="${theme.config?.post_styles?.is_post_recommended_articles_show
          and not #lists.isEmpty(post.categories)}"
  th:with="firstCategoryName=${post.categories[0].metadata.name},
            n=${#conversions.convert(theme.config?.post_styles?.post_recommended_articles_count, 'java.lang.Integer')},
            postFinderResult=${postFinder.list({
              size: n,
              categoryName: firstCategoryName,
              sort: {'spec.publishTime,desc', 'metadata.creationTimestamp,asc'}
            })},
            firstPagePostList=${postFinderResult.items}"
>
  <th:block
    th:if="${#lists.size(firstPagePostList) > 1}"
    th:with="randomPageNumber=${T(java.lang.Math).floor(T(java.lang.Math).random()*(postFinderResult.totalPages)+1)},
              targetPagePostFinderResult=${postFinder.list({
                page: randomPageNumber,
                size: n,
                categoryName: firstCategoryName,
                sort: {'spec.publishTime,desc', 'metadata.creationTimestamp,asc'}
              })},
              targetPagePostList=${targetPagePostFinderResult.items},"
  >
    <th:block th:each="iterPost: ${targetPagePostList}">
      <div th:if="${post.metadata.name != iterPost.metadata.name}">
        <time
          th:text="${#temporals.format(iterPost.spec?.publishTime, 'yyyy-MM-dd')}"
        >文章发布时间替换位</time>
        <a
          th:href="@{${iterPost.status?.permalink}}" 
          th:text="${iterPost.spec?.title}"
        >文章超链接替换位(显示文字为标题/超链接为文章链接)</a>
      </div>
    </th:block>

    <th:block
      th:if="${targetPagePostFinderResult.last and not targetPagePostFinderResult.first}"
      th:with="itemsNeeded=${n-#lists.size(targetPagePostList)}"
    >
      <!--/* 缺项则补 */-->
      <th:block th:if="${itemsNeeded > 0}">
        <th:block th:each="index : ${#numbers.sequence(0,itemsNeeded-1)}">
          <th:block th:with="iterPost=${firstPagePostList[index]}">
            <div th:if="${post.metadata.name != iterPost.metadata.name}">
              <time
                th:text="${#temporals.format(iterPost.spec?.publishTime, 'yyyy-MM-dd')}"
              >文章发布时间替换位</time>
              <a
                th:href="@{${iterPost.status?.permalink}}" 
                th:text="${iterPost.spec?.title}"
              >文章超链接替换位(显示文字为标题/超链接为文章链接)</a>
            </div>
          </th:block>
        </th:block>
      </th:block>
    </th:block>
  </th:block>
</th:block>

讲解代码示例(按当前文章第一个分类过滤结果)

第一层(按当前文章第一个分类过滤结果)

<th:block
  th:if="${theme.config?.post_styles?.is_post_recommended_articles_show
          and not #lists.isEmpty(post.categories)}"
  th:with="firstCategoryName=${post.categories[0].metadata.name},
            n=${#conversions.convert(theme.config?.post_styles?.post_recommended_articles_count, 'java.lang.Integer')},
            postFinderResult=${postFinder.list({
              size: n,
              categoryName: firstCategoryName,
              sort: {'spec.publishTime,desc', 'metadata.creationTimestamp,asc'}
            })},
            firstPagePostList=${postFinderResult.items}"
>
	<!-- ... -->
</th:block>

th:if 中添加了一个判断项:not #lists.isEmpty(post.categories)

  • 解释:现在是按当前文章第一个分类过滤结果,因此文章无分类则不进行推荐。
    th:with 初始化了多个变量:
  • firstCategoryName:保存当前文章第一个分类的唯一标识。

第二层(按当前文章第一个分类过滤结果)

<th:block
    th:if="${#lists.size(firstPagePostList) > 1}"
    th:with="randomPageNumber=${T(java.lang.Math).floor(T(java.lang.Math).random()*(postFinderResult.totalPages)+1)},
              targetPagePostFinderResult=${postFinder.list({
                page: randomPageNumber,
                size: n,
                categoryName: firstCategoryName,
                sort: {'spec.publishTime,desc', 'metadata.creationTimestamp,asc'}
              })},
              targetPagePostList=${targetPagePostFinderResult.items},"
  >
  	<!-- ... -->
</th:block>

th:with 初始化了多个变量:

  • targetPagePostFinderResult:在原有的基础上新设置了分类标识,为当前文章第一个分类的唯一标识。参数含义详情请参考官方文档

第三层(按当前文章第一个分类过滤结果)

此层无变化。

后记

更好的解决方案可能是实现一个 Halo CMS 插件,提供 Finder API 来显示随机文章,仅需要在原有的 list({...}) 上进行拓展。

实现了此功能的主题有:halo-theme-higan-hz

欢迎在评论区分享您的看法。