博客装修记:幽灵发布时间
写完关于硬件Passkey的文章之后,发现各大聚合站较少收录,点进去一看才发现,这篇博文的名义发布时间变成了12月28号,而实际的发布时间却是12月30号,导致依赖RSS订阅中的发布时间来排序的聚合站,错误的把文章排到了之前的日子里。很显然,前两天升级的typecho,所带来的小bug还在追我。
这是博友圈抓取的情况所以,2025年最后一篇文章,献给陪伴了我两年时光的Typecho。
1. 前言
笔者写文章有一个习惯,就是在后台开一篇草稿,然后花几天时间慢慢地把内容填写完,最后才点发布。在过去的后台版本中,发布日期是点发布键的一瞬间才确定下来的,但新版本似乎改成了以创建草稿的时间为准,且此值是后端确定的,即使前端提交值为空,也会被写入『第一次执行保存草稿操作』的时间。所以,那篇文章虽然发布于30日,但创建草稿的时间正好就是28日,就对上了。
我们需要让Typecho回到原来版本的逻辑,也就是:
- 保存草稿,发布时间默认为某特殊值
- 点击发布,才确定最终发布时间
- 对已发布的文章进行修改(revision),不修改发布时间
对比以前的记录发现,『已经存有但暂未填充的草稿』其在数据库中的记录的created字段值为0,这就印证了我们上面的猜想。
2. 定位并修复问题
与其说是定位问题,不如说是给他打补丁,而且还是比较Hacky的那种。思考再三,最后采用的解决方案是:在前端手动挂一个特殊值,并在正式发布的时候清空此特殊值,让系统根据服务器时间来正确填写实际发布时间。
首先是挂特殊值的问题。打开/admin/write-post.php,向下滚动到95行附近,在『发布日期』这个label下面,可以找到一个name和id均为date的<input>。在value值里面,本来是有一个加载编辑时间值的机制的,写成了三元表达式的形式,但不知为何没有生效。稳妥起见,笔者略作改写,并填上我们所需要的特殊值:
<input
class="typecho-date w-100"
type="text" name="date" id="date" autocomplete="off"
value="<?php echo(($post->have() && $post->created > 0) ? $post->date('Y-m-d H:i') : '1970-01-01 08:01'); ?>"
/>然后,需要修改一些js代码来配合此行为。该说不说,Typecho的后台还是有很多混合写法的,比如文章编辑用的是write-post.php,里面提交文章内容用的是<form>,但系统本身也支持AJAX上传(按Ctrl+S保存)。我们的原则是尽量少改动,因此确定以下修改路径:
- 给
<form>添加一个id,把『发布文章』按钮的类型从submit改为button 进入
write-js.php,在合适位置,比如178行附近,添加代码:// 点击发布按钮时确认,并刷新发布时间 document.querySelector('button#btn-submit').addEventListener('click', () => { if(!confirm("你确定要发布本文章吗?")) { return false; } // 正式发布时如果还是特殊值,就清空发布时间框,让后台自己确定 if(document.querySelector('input#date').value === '1970-01-01 08:01') { document.querySelector('input#date').value = ''; } changed = false; // 内部变量,防止二次弹窗 document.querySelector('form#write-post-form').submit(); });
眼尖的读者可能已经发现,这里的特殊值选用的是1970-01-01 08:01,为什么不是选八点整(也就是UNIX时间戳0)呢?
虽然我们的原则是尽量少改动,但如果真有这么简单就好了。前端写入1970-01-01 08:00,其实会触发一个暗坑:
- 保存请求被路由到
/var/Widget/Content/Post/Edit.php,分发到writePost - 走草稿分支,调用位于
/var/Widget/Content/EditTrait.php的save - 再调用位于
/var/Widget/Base/Contents.php的insert insert里面构建插入结构,代码如下:
public function insert(array $rows): int
{
/** 构建插入结构 */
$insertStruct = [
'created' => empty($rows['created']) ? $this->options->time : $rows['created'],
'modified' => $this->options->time,
// ... 省略 ...
];
// ... 省略 ...
}是的,这里使用empty()来判断是否存在created字段,但问题是,PHP的empty函数有一个天坑:翻开官方手册可以看到这么一句话:
当 var 不存在、值为空、等于 0、为 false 时,返回 true。
而原程序中,大约在第二步调用save前,会把1970-01-01 08:00转换为UNIX时间戳0,结果就是在这里被PHP认定为说空值,作废,替换为系统当前时间。
所以其实在很多文章中都有提到,避免(但并非绝对不要)使用PHP的empty函数,应该替换为更加准确的写法,比如如果你想要判断一个变量是否被声明且其值不为NULL,应该使用isset函数;想要判断false,就直接字面量判断,等等,否则就会像这里这样出现暗坑。
话题回来。因为笔者并不确定这里使用empty是否有什么特殊含义,所以我们最好不要改动此处代码,而是把我们构建的特殊值增加一点,从1970-01-01 08:00改为1970-01-01 08:01,让最后算出来大于0。
这就完了吗?当然没有,改完之后发现,如果直接从在页面填写内容后发布,那就工作正常,看起来没事了。但是,如果是从具有特殊值的草稿进行发布,那么最终发布时间就会保留为特殊值,导致文章发布后无法显示。
其实这里的门道位于/var/Widget/Contents/EditTrait.php里面的publish()函数,关键点在于,如果说是后者的情况,因为已经有对应文章的记录,程序不会再走insert函数,会直接把修改好的内容存盘,读出数据库中原修改时间(特殊值)并覆盖掉正确值,把数据库里对应记录的状态码改为post,完成发布操作,于是特殊值就被保留下来了。
想要把特殊值修改为正常值,需要退一步,在调用发布函数之前,不让系统覆盖日期。具体来说,是同一个文件里面的getCreated(),也就是上面提到的把可读日期转换为UNIX时间戳的函数,有这么一个分支:
/* ... 省略 ... */
elseif ($this->have() && $this->created > 0) {
//如果是修改文章
$created = $this->created;
}
/* ... 省略 ... */将此处判断大于0,改为判断大于60,正好位于我们的特殊值之上,就不会执行分支里的覆盖代码,可以避免这个问题了。
3. 写在最后
改代码还真不是一件容易事,因为路上随时有坑,甚至可能因为『bug依靠bug运行』而无法第一时间看出来,结果在日后某一个时间点,就出事了。
看来,这建站也不是一个容易事啊。
提前预祝各位读者 2026 年新年快乐!
(完)
感觉可以给Typecho开个 feature issue,加上一个选择是以创建草稿还是以发布时的日期为准
倒是提醒我了,我去看看
感觉升级1.3又会出现一堆bug
主要是和以前的版本有一些逻辑上的不同,只能说更新为了安全吧,不更新保不准有什么老漏洞