WordPress 插件开发实战:优雅实现 JSON-LD 结构化数据 (Schema) 输出
优雅实现 JSON-LD 结构化数据 Schema输出
摘要
结构化数据 (Schema) 是 SEO 优化中至关重要的一环。想让搜索引擎更“懂”你的文章、产品、FAQ,最好的方式就是在页面头部输出 JSON-LD 格式的 Schema 标记。本文将手把手带你设计一个灵活的 Schema 模块:从插件检测、配置档管理,到 wp_head 精准输出,并结合代码详解每一步的实现思路与核心细节。
一、为什么要在插件中内置 Schema 支持?
很多人会把 Schema 交给 Yoast、Rank Math 这类 SEO 插件去处理,但有时我们会有更特殊的诉求:
- 你正在开发一个内容自动推送插件,需要根据桌面端生成的模板输出复杂的 Schema 图(例如同时包含
Article、FAQPage、Product)。 - 你希望兼容多种 SEO 插件,在 Yoast 输出基础 Schema 的同时,由插件额外补充 FAQ 等片段,避免重复输出。
- 你需要为多站点、多模板的场景提供统一的 Schema 管理,让推送端只需指定一个
profileId。
接下来,我们就以 autoGeo Connector 的设计为例,看看如何构建这样一套 Schema 体系。
二、整体架构设计
我们的 Schema 模块由三个核心类组成:
| 类 | 职责 |
|---|---|
AutoGeo_Schema_Profiles |
定义内置的 Schema 配置档(template profiles),提供可用的 ID 列表和拼装逻辑 |
AutoGeo_Schema |
负责实际的 Schema 组装、消毒、存入 post_meta,并通过 wp_head 输出到前端 |
AutoGeo_Seo_Adapter |
检测当前启用的 SEO 插件,决定如何与现有 Schema 共存(避免重复输出 Article) |
工作流程如下:
- 文章发布时,根据桌面端传入的
schemaProfile或完整的jsonLd数据,调用AutoGeo_Schema::apply()将 Schema 存入_autogeo_json_ld自定义字段。 - 前端请求文章时,挂载在
wp_head上的render_head()函数读取该字段,输出<script type="application/ld+json">代码。 - 如果已安装 Yoast 等插件,我们只输出非冲突的类型(如只输出 FAQPage),避免一张页面出现两个
Article。
三、核心代码实现
3.1 配置档管理:AutoGeo_Schema_Profiles
配置档的作用是允许推送端只传一个字符串 ID,插件就能自动拼装出对应的 JSON-LD 图。我们内置了三个配置文件:
class AutoGeo_Schema_Profiles {
public static function list_ids(): array {
return ['article_faq_v1', 'enterprise_product_v1', 'news_article_v1'];
}
public static function build( string $profile_id, WP_Post $post, array $seo, array $geo ): array {
switch ( $profile_id ) {
case 'article_faq_v1':
return self::build_article_faq( $post, $seo, $geo );
case 'enterprise_product_v1':
return self::build_enterprise_product( $post, $seo, $geo );
case 'news_article_v1':
return self::build_news_article( $post, $seo, $geo );
default:
return [];
}
}
private static function build_article_faq( WP_Post $post, array $seo, array $geo ): array {
$graph = [];
// Article
$graph[] = [
'@type' => 'Article',
'headline' => $seo['metaTitle'] ?? get_the_title( $post ),
'description' => $seo['metaDescription'] ?? '',
'datePublished' => get_the_date( 'c', $post ),
'dateModified' => get_the_modified_date( 'c', $post ),
'author' => [
'@type' => 'Person',
'name' => get_the_author_meta( 'display_name', $post->post_author ),
],
];
// FAQPage
if ( ! empty( $geo['faq'] ) ) {
$questions = [];
foreach ( $geo['faq'] as $faq ) {
$questions[] = [
'@type' => 'Question',
'name' => $faq['question'],
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => $faq['answer'],
],
];
}
$graph[] = [
'@type' => 'FAQPage',
'mainEntity' => $questions,
];
}
return $graph;
}
// 其他配置档类似...
}
这样,桌面端只需在 payload 中设置 "schemaProfile": "article_faq_v1",就能获得一套标准的 Article + FAQPage 结构。
3.2 核心 Schema 处理类:AutoGeo_Schema
这个类负责两项主要工作:存储和输出。
class AutoGeo_Schema {
/**
* 将 Schema 存储到文章元数据
*/
public static function apply( int $post_id, string $title, string $excerpt, array $seo, array $geo ): void {
$schemas = [];
// 情况 1:桌面端已经传了完整 JSON-LD
if ( ! empty( $geo['jsonLd'] ) && is_array( $geo['jsonLd'] ) ) {
$schemas = self::sanitize_json_ld_graph( $geo['jsonLd'] );
}
// 情况 2:根据配置档 ID 构建
elseif ( ! empty( $geo['schemaProfile'] ) ) {
$profile_id = sanitize_text_field( $geo['schemaProfile'] );
$post = get_post( $post_id );
if ( $post && in_array( $profile_id, AutoGeo_Schema_Profiles::list_ids(), true ) ) {
$schemas = AutoGeo_Schema_Profiles::build( $profile_id, $post, $seo, $geo );
}
}
if ( ! empty( $schemas ) ) {
update_post_meta( $post_id, '_autogeo_json_ld', wp_json_encode( $schemas, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) );
}
}
/**
* 前端输出(挂载到 wp_head)
*/
public static function render_head(): void {
if ( ! is_singular() ) {
return;
}
$post_id = get_the_ID();
$json_ld = get_post_meta( $post_id, '_autogeo_json_ld', true );
if ( empty( $json_ld ) ) {
return;
}
// 兼容 Yoast / Rank Math:如果它们已经输出了 Article,我们就只输出剩余类型
$schemas = json_decode( $json_ld, true );
if ( ! is_array( $schemas ) ) {
return;
}
// 检测 SEO 插件并过滤已输出的类型
$seo_plugin = AutoGeo_Seo_Adapter::detect_plugin();
if ( in_array( $seo_plugin, [ 'yoast', 'rankmath' ] ) ) {
$schemas = self::filter_overlapping_types( $schemas, [ 'Article', 'WebPage' ] );
}
if ( empty( $schemas ) ) {
return;
}
// 包装成 @graph
$output = [
'@context' => 'https://schema.org',
'@graph' => $schemas,
];
printf(
'<script type="application/ld+json" class="autogeo-schema">%s</script>' . "\n",
wp_json_encode( $output, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES )
);
}
/**
* 过滤掉指定类型的 Schema 节点
*/
private static function filter_overlapping_types( array $schemas, array $exclude_types ): array {
return array_filter( $schemas, function( $node ) use ( $exclude_types ) {
$type = $node['@type'] ?? '';
return ! in_array( $type, $exclude_types, true );
} );
}
/**
* 简单的消毒:确保每个节点都有 @type,并递归清理危险标签
*/
private static function sanitize_json_ld_graph( array $graph ): array {
$clean = [];
foreach ( $graph as $node ) {
if ( empty( $node['@type'] ) ) {
continue;
}
$clean[] = self::recursive_sanitize( $node );
}
return $clean;
}
private static function recursive_sanitize( array $data ): array {
$safe = [];
foreach ( $data as $key => $value ) {
if ( is_string( $value ) ) {
$safe[ $key ] = wp_kses( $value, [] );
} elseif ( is_array( $value ) ) {
$safe[ $key ] = self::recursive_sanitize( $value );
} else {
$safe[ $key ] = $value;
}
}
return $safe;
}
}
// 注册 wp_head 钩子
add_action( 'wp_head', [ 'AutoGeo_Schema', 'render_head' ], 20 );
3.3 与 SEO 插件的智能共存
为了避免重复输出 Article 等基础类型,我们通过 AutoGeo_Seo_Adapter 检测当前 SEO 插件:
class AutoGeo_Seo_Adapter {
public static function detect_plugin(): string {
if ( defined( 'WPSEO_VERSION' ) ) {
return 'yoast';
}
if ( class_exists( 'RankMath' ) ) {
return 'rankmath';
}
if ( defined( 'AIOSEO_PLUGIN_FILE' ) ) {
return 'aioseo';
}
// ... 其他
return 'native';
}
}
当检测到 Yoast 或 Rank Math 时,render_head() 会过滤掉 Article 和 WebPage 节点,只输出它们不提供的类型(如 FAQPage、Product)。这样就实现了“补充输出”,而非“重复输出”。
四、前端输出效果
最终在页面源代码中,你会看到类似这样的脚本:
<script type="application/ld+json" class="autogeo-schema">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "如何安装插件?",
"acceptedAnswer": {
"@type": "Answer",
"text": "上传至 wp-content/plugins 并启用即可。"
}
}
]
}
]
}
</script>
这正是搜索引擎爬虫最喜爱的结构化数据格式。
五、进阶:支持远程推送模板
在 autoGeo Connector 的实际应用中,桌面端可以直接传递完整的 jsonLd 数组,插件仅做消毒和存储。这使得 Schema 的生成完全由桌面端的 AI 或模板引擎控制,极大增强了灵活性。
// 桌面端 POST /publish 的部分 payload
{
"geo": {
"jsonLd": [
{
"@type": "Article",
"headline": "...",
...
},
{
"@type": "FAQPage",
"mainEntity": [...]
}
]
}
}
插件端的 apply() 方法会优先检测 jsonLd 字段,若存在则跳过内置配置档的拼装。这样既保留了自动化的便利,又提供了完全自定义的能力。
六、总结
通过上面这套设计,我们实现了一个既轻量又灵活的 WordPress Schema 模块:
- 配置档机制:让常见结构可以“开箱即用”。
- 消毒与安全:任何外部 JSON 数据都经过
wp_kses和类型校验。 - 智能共存:自动检测 Yoast / Rank Math,避免重复标记。
- 可扩展:新增 Schema 类型只需在
AutoGeo_Schema_Profiles中添加一个配置档 ID 和对应构建方法。
无论你是在开发自己的插件,还是在为站群自动化建设赋能,这套 Schema 方案都能让你事半功倍。希望本文的代码和思路能为你提供实用的参考。如果有更好的实现方式,欢迎在评论区交流!
更多推荐



所有评论(0)