在 Halo CMS 中通过模板实现随机推荐多篇文章
前言
仅需要随机获取一篇文章请看:在 Halo CMS 中通过模板实现随机文章跳转功能。
本文着重于随机获取多篇文章,以及根据指定条件过滤结果。
本文实现了两完整示例:
- 获取多篇随机文章。
- 在第一个示例的基础上,按当前页面文章第一个分类过滤结果。
笔者将先分享完整示例,之后拆解示例,逐层讲解。
完整代码示例
模板代码使用了两配置项:
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](%5B#ListedPostVo%5D(https://docs.halo.run/developer-guide/theme/finder-apis/post/#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](%5B#ListedPostVo%5D(https://docs.halo.run/developer-guide/theme/finder-apis/post/#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。
欢迎在评论区分享您的看法。
0