博客装修记:幽灵发布时间

warning: 这篇文章距离上次修改已过158天,其中的内容可能已经有所变动。

写完关于硬件Passkey的文章之后,发现各大聚合站较少收录,点进去一看才发现,这篇博文的名义发布时间变成了12月28号,而实际的发布时间却是12月30号,导致依赖RSS订阅中的发布时间来排序的聚合站,错误的把文章排到了之前的日子里。很显然,前两天升级的typecho,所带来的小bug还在追我。

这是博友圈抓取的情况这是博友圈抓取的情况

所以,2025年最后一篇文章,献给陪伴了我两年时光的Typecho。

1. 前言

笔者写文章有一个习惯,就是在后台开一篇草稿,然后花几天时间慢慢地把内容填写完,最后才点发布。在过去的后台版本中,发布日期是点发布键的一瞬间才确定下来的,但新版本似乎改成了以创建草稿的时间为准,且此值是后端确定的,即使前端提交值为空,也会被写入『第一次执行保存草稿操作』的时间。所以,那篇文章虽然发布于30日,但创建草稿的时间正好就是28日,就对上了。

我们需要让Typecho回到原来版本的逻辑,也就是:

  1. 保存草稿,发布时间默认为某特殊值
  2. 点击发布,才确定最终发布时间
  3. 对已发布的文章进行修改(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保存)。我们的原则是尽量少改动,因此确定以下修改路径:

  1. <form>添加一个id,把『发布文章』按钮的类型从submit改为button
  2. 进入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,其实会触发一个暗坑:

  1. 保存请求被路由到/var/Widget/Content/Post/Edit.php,分发到writePost
  2. 走草稿分支,调用位于/var/Widget/Content/EditTrait.phpsave
  3. 再调用位于/var/Widget/Base/Contents.phpinsert
  4. 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 年新年快乐!

(完)


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

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

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

本站由 搬瓦工VPS 强力驱动

none
最后修改于:2026年02月03日 12:25

已有 4 条评论

  1. main main

    感觉可以给Typecho开个 feature issue,加上一个选择是以创建草稿还是以发布时的日期为准

    1. 倒是提醒我了,我去看看

  2. 感觉升级1.3又会出现一堆bug

    1. 主要是和以前的版本有一些逻辑上的不同,只能说更新为了安全吧,不更新保不准有什么老漏洞

添加新评论

提醒:『评论回复邮件提醒』功能正在测试中
评论后,如果站长有回复,会有邮件通知