博客装修记:『每日推荐』

系列内文章:
博客的 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,即可实现想要的效果:

写在最后

装修不难,但是如果想和人描述清楚自己做了写什么,还是一件不太容易的事情。

这大概就是写博客最大的收获之一。

(完)


木头箱子脆脆,但是这样正好

如无特殊声明,本站内容均遵循 CC BY-NC-SA 4.0 协议

转载请注明出处并保留作者信息,谢谢!

none
最后修改于:2025年06月28日 20:49

已有 2 条评论

  1. awacido awacido

    每日推荐可用。确实简洁不简单的博客

    1. 墨枫梧桐 墨枫梧桐

      谢谢

添加新评论

提醒:站长手头紧,没有配备『评论回复邮件提醒』功能
评论后,劳烦您隔一段时间回到本页面查看站长回复(一般都会回)