博客装修记:『每日推荐』
系列内文章:
博客的 RSS 如何导致 SEO 异常
网易云音乐有个每日推荐功能,感觉还不错,能够听一些以前没听过的歌。能不能在博客上模仿一个呢?事实证明,虽然没办法按访客喜好来推荐,但是每天随机抽取一些文章,并展示到侧边栏,还是很简单的事情。
前言
经常看我博客的朋友都知道,目前使用的是Waxy主题。当初挑选主题的时候,笔者主要还是看中了他加载迅速,样式简洁(我不是很喜欢花里胡哨的东西,比如说跟随着鼠标点击的气泡之类的)之类的优点,于是就这么用下来了。
不过,目前有个问题:Waxy主题确实简洁,但也意味着部分个性化的功能是缺失的(当然这里没有贬低开发者的意思,主题本身还是很好用的),举例就是:对于一般的个人博客,访客一般都是从Google之类的搜索引擎,或者其他引荐源点进来的,其访问目标就是博文页面而已。如果访客在看完文章之后还想看看本站其他文章,他要么点开文章归档页面去看,要么回到首页一页页地翻看,从访客的角度来说,不利于访问,从功利一点的角度来说,『不利于其他内容的曝光』。
想让很久以前发布的,已经『沉底』的内容定期浮上来,最简单的办法就是每天随机抽取一些,将他们展示出来,说不准就碰上了访客喜欢的内容呢?
数据获取
Typecho作为一个博客CMS,他内部确实有封装一些用于访问数据库的方法,但是这些方法得在特定的上下文中使用。笔者一时间懒得去研究怎么去进入这个上下文,反正自己也有全站的访问权,而且考虑到是只读操作,于是还是采用最直接的办法:在PHP中直接访问数据库,去获取内容。
SELECT cid
FROM `typecho_contents`
WHERE `type`="post" AND `status`!="hidden";
拿到所有公开状态的文章ID之后,就可以随机抽取了。笔者一开始的想法是,把获取到的cid存入数组,再通过PHP中的shuffle()
打乱,然后获取前面的若干个元素作为今日推荐。但是经过实际测试发现,这个随机打乱函数其实并不是那么随机,连续执行数次能发现同一个数字反复出现,显然这不是一件太好的事情。
如果这个不够随机的话,什么东西最够随机呢,那当然是random_int()
了,根据官方文档,这个函数满足密码学意义上的安全(Cryptographically Secure),在Linux上其背后实现是getrandom()
这个系统调用。虽然说『密码学安全』对于我们的应用来说有点大材小用了,搞不好还有性能损失,但是考虑到这个程序一天也就执行一次,因此也就无所谓了。
此外,本站的博文篇数已经有一百多篇,总体来说算不算多笔者不敢妄下定论,但是对于我们的目的(抽取10篇)而言,可以认为足够大,因此可以采用一些取巧的方式去生成一个不重复的ID列表。下面这个函数是ChatGPT给出来的,笔者稍作改造(确保不会误用,并不代表有数学意义上的合理性——笔者的数学不是很好,但硬要说的话,可能需要参考生日悖论相关内容)并检查无误后,将其用于本次改造中:
function getUniIDList(int $max, int $count): array {
if ($count / $max > 0.2) {
throw new InvalidArgumentException('给定范围过小');
}
$pool = range(0, $max);
$result = [];
for ($i = 0; $i < $count; $i++) {
$index = random_int(0, count($pool) - 1);
$result[] = $pool[$index];
array_splice($pool, $index, 1);
}
return $result;
}
此函数给出的就是我们需要抽取的ID在暂存数组中的index,因此只需要套进去就可以获得真实的文章ID,最后继续通过SQL语句查询数据库,便可以直接获取对应的标题供展示。
数据存储
获取到数据,自然需要找地方去存储。比较反直觉的是,笔者选择使用Redis来储存。虽然说把数据写入MariaDB没有太大问题,但是考虑到某些问题(例如数据库账号权限调整之类的)之后,笔者还是做出了这样的选择。如果你要做类似改造,那自然不必拘泥于笔者的想法。
//准备redis,$config变量在另外的文件中定义并引入
$redis = new Redis();
$redis -> connect($config['redisaddr'], $config['redisport']);
$redis -> auth($config['redispass']);
//清空前一日的推荐
$iterator = null;
while (true) {
$keys = $redis->scan($iterator, $config['redisprefix'].':*', 10);
if ($keys === false) {
break;
}
if (!empty($keys)) {
$redis->del($keys);
}
}
$j = 0;
foreach($dailyList as $id) {
// ......
// 上面是通过MySQL预处理获取数据的过程
// 每循环一次,就获取一次数据,写入到redis中
$rkey = $config['redisprefix'].':'.(string)$j;
$redis -> hSet($rkey, 'id', $id);
$redis -> hSet($rkey, 'title', $title);
$j++; //每条记录加一,防止键重复
}
上面的核心代码段基本上就是后台的工作重点。写好之后只需要把这个PHP文件放入定时任务里面,让他每天凌晨四点执行一次,就可以获取到最新内容了。
数据获取
如果你打开过F12开发者面板,你会发现本站会请求/anyAPI
路径下的接口,这就是笔者放置一些自编的特殊用途API的地方。在新增功能的时候,如果涉及到获取数据,笔者不是很想采用SSR(服务端渲染)的形式去进行操作,因为其实一直有想法把本站做到完全的前后端分离,然后前端长时间缓存在CDN边缘节点,SSR的方式生成内容就会导致动态变化的内容也一并被缓存下来了,会破坏功能的正常运行。此外,采用前端渲染,就可以保证网页的首屏先加载出来,屏幕之外的地方加载慢一些问题也不大(SSR就必须后台生成好所有HTML内容再一口气返回前台),这样多少可以确保用户体验。
对于本例而言,就是getReco.php
这个接口,要从Redis中批量获取数据,最高效的方法当然就是使用SCAN功能:
$iterator = null;
$result = [];
while ($keys = $redis->scan($iterator, $config['redisprefix'].':*')) {
foreach ($keys as $key) {
$data = $redis->hGetAll($key);
if (isset($data['id']) && isset($data['title'])) {
$result[] = (object)[
'id' => $data['id'],
'title' => $data['title']
];
}
}
}
// 然后只需要把这个 $result
// 放到做好的数据结构里面,打包成 json
// 就可以返回给前端使用了
数据展示
每日推荐功能,放在侧边栏是比较合适的——但对于移动端,笔者还在考虑怎么处理比较合适,因为移动端的侧边栏是收折到最下面的,还放侧边栏的话阅读可能不便,不过目前因为笔者正在忙有关于期末考的事情,所以可能以后再来处理这件事,取决于以后还忙不忙吧。
Waxy主题的侧边栏存放在sidebar.php
当中,打开之后,在里面依葫芦画瓢添加内容就行了。
不过笔者掐指一算,当你看到这里的时候,如果你用的是PC端,你应该可以发现,右边的『每日推荐』栏目已经吸附在了距离浏览器顶部10px的地方(如果你没发现,应该往下滚动滚动就差不多了)。熟悉前端的朋友都知道,这样的效果可以在CSS中用position: sticky
去实现,于是你可以很轻松地编写出来以下代码,插入到sidebar里面:
<style>
.custom-sticky {
position: sticky;
top: 10px;
}
.x-reco-link {
display: block;
width: 100%;
padding: 5px 0px 0px;
border-bottom: 1px solid #dbdbdb;
}
</style>
<div class="widget custom-sticky">
<h4 class="title">每日推荐</h4>
<div class="content">
<div class="recent-single-post" id="x-daily-reco-wrapper">
<span>Loading.....</span>
</div>
</div>
</div>
<script>
// 加载接口内容并清空上面wrapper,createElement后
// 使用insertAdjacentElement插入<a>标签的代码,略
</script>
然而,如果你保存后会发现,sticky效果并没有生效,滚动的时候还是被滚上去了。道理很简单:在原版Waxy主题里,有如下页面结构:
<div class="row">
<main class="col-md-8 main-content main-no-grap">
<!-- 页面主体 -->
</main>
<aside class="col-md-4 sidebar">
<!-- 侧边栏 -->
</aside>
</div>
根据一堆CSS文件的定义,以及bootstrap默认行为,上面最终呈现的效果是<aside>
的高度与其中内容高度一致,而不是与旁边的<main>
一致,这就导致上面那个div已经没有多余空间去给他呈现sticky效果了。
解决办法很直观:只要让<aside>
高度与<main>
一致就行,笔者最后采用的方法是,在最外层div上加上display: flex
属性去解决问题,不确定这么干正不正规,但是管用。
具体办法是,在自定义CSS的地方加入如下查询器:
/* 992px确保只在电脑端生效 */
@media (min-width: 992px) {
div.x-div-row {
display: flex;
flex-wrap: nowrap;
}
}
然后依次修改各个页面的文件,在上面的div的class中加入x-div-row
,即可实现想要的效果:

写在最后
装修不难,但是如果想和人描述清楚自己做了写什么,还是一件不太容易的事情。
这大概就是写博客最大的收获之一。
(完)
每日推荐可用。确实简洁不简单的博客
谢谢