博客装修记:Typecho 插件与 JSON-LD

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

包括Google和Bing在内的很多搜索引擎,他在爬取页面的时候都支持通过读取页面开头的JSON-LD数据结构,快速获取网页内容的描述内容,所以是有利于SEO的。

很多情况下,这样的功能可以通过插件来实现(而且你真的去搜索的话,也会发现有很多大神开发了给Typecho使用的,输出类似数据结构的插件),但是本着不折腾会死的精神,笔者决定自己动手改造。况且给Typecho编写插件是笔者一直想要做但没做成的事情,倒不是因为笔者不会PHP,而是因为巧妇难为无米之炊。

1. JSON-LD基本语法

开始之前先提一嘴JSON-LD。看名字就知道,这是基于json的一种数据结构。插入到HTML页面的时候,要使用特殊的<script>标签将其包裹起来,然后放到页面开头的地方(一般是<head>块里面),以便于搜索引擎爬虫阅读。

JSON-LD 示例
<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 分析插件文档

info: 内容过期提醒

经检查,插件文档已经于 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

info:关于路径

此处根目录不是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 -> keywords

2.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验证码,不必管他左边是经典的CF验证码,不必管他

4. 写在最后

插件写得太乱了,就不拿出来丢人现眼了。如果你也想给站点加上类似东西,笔者建议您优先考虑成品插件,其次才是根据上面的步骤自己做插件。

至于这个JSON-LD有没有用,用处大不大,那就真的得交给时间去验证了。

兴许对于SEO是个锦上添花的效果,那也不错。

(完)


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

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

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

本站由 搬瓦工VPS 强力驱动

none
最后修改于:2025年11月19日 16:53

添加新评论

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