博客装修记:Typecho 插件与 JSON-LD
包括Google和Bing在内的很多搜索引擎,他在爬取页面的时候都支持通过读取页面开头的JSON-LD数据结构,快速获取网页内容的描述内容,所以是有利于SEO的。
很多情况下,这样的功能可以通过插件来实现(而且你真的去搜索的话,也会发现有很多大神开发了给Typecho使用的,输出类似数据结构的插件),但是本着不折腾会死的精神,笔者决定自己动手改造。况且给Typecho编写插件是笔者一直想要做但没做成的事情,倒不是因为笔者不会PHP,而是因为巧妇难为无米之炊。
1. JSON-LD基本语法
开始之前先提一嘴JSON-LD。看名字就知道,这是基于json的一种数据结构。插入到HTML页面的时候,要使用特殊的<script>标签将其包裹起来,然后放到页面开头的地方(一般是<head>块里面),以便于搜索引擎爬虫阅读。
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"url": "https://example.com/",
"name": "网站名称",
"description": "网站的整体介绍或主题",
"publisher": {
"@type": "Organization",
"name": "网站名称",
"logo": {
"@type": "ImageObject",
"url": "https://example.com/logo.png"
}
},
"potentialAction": {
"@type": "SearchAction",
"target": "https://example.com/search?q={search_term_string}",
"query-input": "required name=search_term_string"
}
"datePublished": "2025-08-30T08:00:00+08:00",
"dateModified": "2025-08-30T09:30:00+08:00"
}
</script>由于数据内容并不是一成不变的(很显然里面有日期,链接,类型等),所以我们必须在后台动态生成这个标签(当然,可以缓存。如果你用的是静态博客系统,那么自然也可以预先计算这个标签的内容)。同时,还需要针对不同类型的页面搜集不同信息,来准确反映页面内容,比如你的博文网页,就可以加上创建/更新时间段;你的个人介绍页,就可以添加sameAs字段,然后附上你所需要的其他Profile页面,比如GitHub主页。
2. 实现
正如上文所说,我们可以给Typecho装个插件——只是有个小小的问题:Typecho插件开发文档站已经很久没有更新,而且内容缺失也非常严重。笔者不知道目前可用的文档站究竟在何处,因为还能看到人们源源不断地产出插件,所以必然还有某种指引。不过目前一时半会没有拿到文档,就只能用一些变通方式来处理了。
2.1 分析插件文档
经检查,插件文档已经于 2025/09/02 修复,且补上了相关示例,因此可以放心地跳过本节内容
此外,新文档中对于Hook点的写法变成了Widget\Archive之类的,并非原来的Widget_Archive。阅读源代码可知这不受影响,主程序内有Common::nativeClassName()这个方法,可以把新别名翻译为旧别名(至少1.2.0版是如此)
Typecho文档站提供的插件接口及功能列表出现了大量的排版错乱的情况,以及包括了大量不应该出现在此处的HTML标签。为了便于阅读,在开始之前,需要先处理一下文档。

好在页面最下方显示了文档的更新时间是2024-06,那么我们只需要打开网页时光机,找到2024-06之前的页面存档,就有一定概率能看到正确的页面——而且我们的运气还不错,2024-03的这份存档是可以使用的:

根据我们的需求,JSON-LD数据需要在<header>里面给出,所以我们需要找到生成<header>相关的代码。Ctrl+F全页搜索『header』,很容易就能发现,Widget_Archive下面发现了一个叫做header的接口,与目的比较接近。

大致的方向有了,接下来就是检查是否符合需求。可惜的是,即使是网页时光机里面的存档,也依然有不少缺失的内容,包括这个接口,『描述』区域也是一片空白。不过好在类名的命名规则官方已经给出来了,其实就是对应的文件夹名称,所以我们可以直接阅读Typecho源代码,来推断接口作用,调用时机,传参内容。比如对于Widget_Archive来说,需要看的源码路径就是/var/Widget/Archive.php。
此处根目录不是Linux的根目录,而是Typecho软件的根目录(即网站根目录)
2.2 源码分析
打开/var/Widget/Archive.php,搜索header,很快就能找到有点眉目的东西:
/**
* 输出头部元数据
*
* @param string|null $rule 规则
*/
public function header(?string $rule = null)
{
/* 此处省略生成$header的代码..... */
/** 输出header */
echo $header;
/** 插件支持 */
self::pluginHandle()->header($header, $this);
}观察上面的代码可知,此处执行的动作是:
- 生成Header(但不包括开头和结尾的
<header></header>) - 输出Header
- 调用一个叫做
header的接口,并把$header以及类的$this传入
换句话说,只要注释为『插件支持』的那行代码里,调用了任何的echo(),结果就是内容被输出到<header>块里面。这个行为正好符合我们的需求。于是,我们只需要按照文档要求,在插件中实现这个接口,使其能被此处的代码调用,就行了。
2.3 插件框架搭建
查阅官方提供的示例代码(幸好这个没有炸掉),我们需要在plugins目录下新建一个以插件名命名的文件夹,里面放一个Plugin.php。
至于Plugin.php文件的内容,笔者试验了几下,最后得出的结论是,必须最少包含以下内容:
<?php
/**
* 这里是你插件的描述
*
* @package 插件名,建议纯英文,且与文件夹名字一致
* @author 作者
* @version 版本
* @link 作者主页地址
*/
// 此处类名似乎必须是『插件名_Plugin』的形式
// 例如我给插件取名 MakeJSONLD,这里就必须是MakeJSONLD_Plugin
class MakeJSONLD_Plugin implements Typecho_Plugin_Interface {
// activate 用来注册插件实现的方法
// 可以return个字符串作为提示,比如 return "已启用";
public static function activate() {}
// deactivate 里面可以return个『已停用』
// 防止插件被识别为『即插即用』
public static function deactivate() {}
// 第一个实现全局配置,第二个实现『个人配置』(据说是用得不多)
// 即使你的插件不需要配置任何东西,这两个函数也必须要存在(即使内容为空)
// 否则系统无法正常处理,会抛异常
public static function config(Typecho_Widget_Helper_Form $form) {}
public static function personalConfig(Typecho_Widget_Helper_Form $form) {}
// 这是你自己实现的方法,业务代码就放在里面
// 叫什么名字无所谓,毕竟待会还要在activate方法里面注册
// 也就是对应的官方文档给出的自己定义的render方法
public static function getJSONLD($para1, $para2) {}
}
搭建好框架,我们就可以开始往里面填充所需的代码了。
2.4 注册方法
要想让我们的方法被博客程序调用,第一步自然是要先注册。注册用的代码自然是要放入到activate()这个方法里面了(这个注册似乎并不需要在deactivate()中做额外的解除处理):
public static function activate() {
// 我们希望被 Widget_Archive 接口中的 header 方法调用
// 调用目标就是 MakeJSONLD_Plugin 这个插件里面的 getJSONLD
Typecho_Plugin::factory('Widget_Archive')->header = array('MakeJSONLD_Plugin', 'getJSONLD');
return _t('已启用');
}这样就算完成了,我们就可以开始编写业务代码。
2.5 业务代码
2.5.1 判断页面类型
根据Typecho提供的is-syntax文档可知,有一个$this -> is()方法可以快速判断当前页面的类型。由于这个$this具体是什么内容得看上下文,所以我们在这里最好不直接写$this,先确定一下$this到底是什么。
继续翻看/var/Widget/Archive.php,可以发现在生成header的代码中出现了对这个$this -> is()方法的使用,而在进入插件之前,这个$this被作为第二个参数传入到了我们的插件处理程序里面。换句话说,我们只需要读取第二个参数,就能获得和$this一样的效果(第一个参数是系统生成的header内容,这里我们用不上它)。得到这个结论之后,我们就可以编写一些测试代码,来确定我们所需的页面类型:
public static function getJSONLD($h, $t) {
// $t 就是上文提到的 第二个参数
if($t -> is('index')) {
echo "<!-- is index -->";
}
if($t -> is('archive')) {
echo "<!-- is archive -->";
}
if($t -> is('post')) {
echo "<!-- is post -->";
}
if($t -> is('page')) {
echo "<!-- is page -->";
}
if($t -> is('category')) {
echo "<!-- is category -->";
}
}把这部分代码合入到上面的框架中之后,保存此文件。然后进入博客后台-控制台-插件,启用这个MakeJSONLD插件。然后我们开启博客的一些页面,检查源代码,在</header>之前,应该能看到以HTML注释形式存在的标识符:
这是首页
这是博文页面
这是单独页面这样就达成了我们的目的。
2.5.2 获取元数据
JSON-LD根据页面类型不同,需要填写的字段也不一致。有了上面获取的页面类型之后,经过分析取舍,决定采用如下几种类型:首页、文章、页面(各种单页),分类、归档、其他。每种类型都有重点需要获取的数据。比如说,首页,就要获取站点标题和站点描述,而文章页就需要获取文章标题,作者名称,文章大致描述,等等。
但很可惜,Typecho中能提供这些数据的接口文档也是缺失的,所以最后只能参考其他插件,依葫芦画瓢找到对应的数据接口,让插件能跑起来。综合起来之后,就能得到如下的数据源:
// $t 就是上文提到的第二个参数,也就是$this
// 页面URL
$t -> getArchiveUrl();
// 首页:站点描述
// 文章:文章描述
$t -> getDescription();
// 文章:文章标题
// 页面:页面标题
$t -> getArchiveTitle();
// 文章/页面:创建和修改时间(Timestamp)
$t -> created
$t -> modified
// 文章:作者
$t -> author -> screenName
// 下面的变量需要引入这个接口
// 更多变量请参考`var/Widget/Options.php 中的注释
$option = Typecho_Widget::widget('Widget_Options');
// 站点标题
$option -> title
// 站点关键词
$option -> keywords2.5.3 生成并输出结构数据
确定好数据源之后,就可以填充我们所需的JSON-LD了。为了节省篇幅,这里仅以首页为例,其他类型的JSON-LD格式请自行查阅文档——或者像笔者这样去问ChatGPT——然后在if分支中依葫芦画瓢添加。
public static function getJSONLD($h, $t) {
$option = Typecho_Widget::widget('Widget_Options');
$data = [];
if($t -> is('index')) {
$data = [
'@context' => 'https://schema.org',
'@type' => 'WebSite',
'url' => $t -> getArchiveUrl(),
'name' => $option -> title,
'description' => $t -> getDescription(),
'keywords' => $option -> keywords
];
}
echo('<script type="application/ld+json">');
echo(json_encode((object)$data, JSON_UNESCAPED_UNICODE));
echo("</script>");
}
写完以上代码,保存,JSON-LD结构数据就已经在站点上生效了。
3. 验证
最简单的验证办法就是查看页面源代码,看看我们的JSON-LD是否已经插入到对应位置:

还可以通过一些SEO检查工具,来查看数据情况
左边是经典的CF验证码,不必管他4. 写在最后
插件写得太乱了,就不拿出来丢人现眼了。如果你也想给站点加上类似东西,笔者建议您优先考虑成品插件,其次才是根据上面的步骤自己做插件。
至于这个JSON-LD有没有用,用处大不大,那就真的得交给时间去验证了。
兴许对于SEO是个锦上添花的效果,那也不错。
(完)