WordPress の自作CPTに BlogPosting 構造化データ(JSON-LD)を後付けした話
先日、当サイトに スタッフブログ機能 を新設したのですが、その仕上げとして JSON-LD 構造化データ を追加しました。地味ながら SEO 実務では効きの大きい仕込みなので、今回どこを直したのか、コード込みでシェアしておきます。
なぜ BlogPosting 構造化データを足すのか
JSON-LD の構造化データは、Google など検索エンジンに「このページは何を書いたコンテンツなのか」を明示的に伝えるための仕組みです。うちの場合、SEOプラグイン(Rank Math 系)が入っていて、Organization・WebPage・BreadcrumbList などは自動で出してくれていました。
ただし、ブログ記事に本命の BlogPosting(あるいは Article)が付いていない状態でした。Google のガイドラインでは、記事コンテンツには BlogPosting を推奨していて、これがあると次のような要素を検索エンジンに伝えられます。
- 誰が書いたのか(
author) - いつ公開・更新したのか(
datePublished/dateModified) - どんな概要か(
headline/description) - どのカテゴリの記事か(
articleSection) - 文字数(
wordCount)/画像/発行元(publisher)
これがない状態でもインデックスはされますが、リッチリザルト表示や、AI回答(AI Overview 等)に引用される確度は明確に下がります。今後スタッフブログを本気で育てていくにあたって、ここは最初に埋めておくべき土台と判断しました。
まず「今どこまで出ているか」を確認する
実装の前に、既存のページに出ている JSON-LD を確認しました。ブラウザで対象URLを開いて DevTools → Sources から application/ld+json スクリプトタグを見るのが一番早いです。CLI 派の方には次のようなワンライナーもオススメ:
curl -s "https://example.com/blog/xxx/" \
| python3 -c "
import sys, re, json
html = sys.stdin.read()
for s in re.findall(r'<script[^>]*ld\+json[^>]*>(.*?)</script>', html, re.DOTALL):
d = json.loads(s.strip())
print(json.dumps(d, ensure_ascii=False, indent=2)[:800])
"
結果、Organization・Person・WebSite・WebPage・BreadcrumbList は既に出力されていましたが、BlogPosting は無し。ここを補完する方針で進めます。
実装:WordPress プラグインに wp_head フック
今回はスタッフブログ CPT 用の自作プラグイン cielo-blog にコードを足しました。テーマ側に書かない理由はシンプルで、テーマを差し替えても機能が消えないようにしたいからです。
実際に足したコードの中身がこちらです(要点のみ抜粋)。
add_action( 'wp_head', function () {
if ( ! is_singular( 'blog' ) ) return;
$post = get_post();
$author = get_userdata( $post->post_author );
// アイキャッチ or OGP画像フォールバック
$image_url = get_the_post_thumbnail_url( $post->ID, 'full' )
?: home_url( '/wp-content/uploads/2026/04/01_cielodesign_OGP.jpg' );
// カスタムタクソノミー blog_category を articleSection / keywords に
$terms = get_the_terms( $post->ID, 'blog_category' );
$section = ( ! empty( $terms ) ) ? $terms[0]->name : 'ブログ';
$data = [
'@context' => 'https://schema.org',
'@type' => 'BlogPosting',
'@id' => get_permalink( $post ) . '#blogposting',
'mainEntityOfPage' => [ '@type' => 'WebPage', '@id' => get_permalink( $post ) ],
'headline' => get_the_title( $post ),
'description' => wp_strip_all_tags( get_the_excerpt( $post ) ),
'image' => [ $image_url ],
'datePublished' => mysql2date( 'c', $post->post_date_gmt, false ),
'dateModified' => mysql2date( 'c', $post->post_modified_gmt, false ),
'inLanguage' => 'ja-JP',
'articleSection' => $section,
'author' => [
'@type' => 'Person',
'name' => $author->display_name,
'url' => home_url( '/staff/' ),
],
'publisher' => [
'@type' => 'Organization',
'name' => 'CIELO DESIGN',
'url' => home_url( '/' ),
],
];
echo '<script type="application/ld+json">';
echo wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
echo '</script>';
}, 20 );
ポイントは 3 つあります。
is_singular('blog')でフィルタ:スタッフブログ CPT のみに絞って、他ページのSEOに影響しないように- SEOプラグインの JSON-LD と共存:Google は複数の JSON-LD ブロックがあっても merged で扱ってくれるので、重複を恐れず既存出力を残したまま 足すのが安全
- アイキャッチ未設定時のフォールバック:OGP画像を差し込んで、image 未定義でエラー扱いされないようにしておく
検証:本当に出ているか
デプロイ後、いくつかの方法で確認しました。
- ページソースを開いて
<script type="application/ld+json">が2つ(既存のグラフ + 追加した BlogPosting)出ていること - Google の リッチリザルトテストにURLを投げて BlogPosting が認識されること
- Schema.org Validator でエラー・警告が0であること
結果、想定通り BlogPosting が出力され、headline / author.name / articleSection / wordCount / datePublished がすべて期待値通り入りました。
実装してみて感じたこと
SEOプラグインを入れていると、つい「もう構造化データは全部やってくれているんじゃないか」と思いがちですが、プラグインが記事タイプ(CPT)を認識していない場合、BlogPosting は自動で出ないことがよくあります。今回のように blog という独自 CPT を切ったサイトでは、明示的に補完する必要がありました。
また、AI 時代のSEOという文脈でも、構造化データは「AIに引用されるための共通言語」として重要度が増しています。ChatGPTやClaudeが検索結果を要約する時、JSON-LDの内容は間違いなく参照しています。「読まれる」だけでなく「引用される」ためにも、この一手は入れておく価値があります。
まとめ
ブログ記事の BlogPosting 構造化データは、SEOプラグインが入っていても自動で全部やってくれるとは限らないので、CPT を切っているサイトでは自力で補完する必要があります。実装は wp_head フックにJSON-LDを1つ echo するだけと軽量。テーマではなくプラグイン側に置くと、テーマ切替に強くなります。
今回のコードとロジックが、他社のCPT実装や、Rank Math等のSEOプラグインを使いながら「一部だけ足りない」状況の参考になれば幸いです。
お問い合わせ
Webサイト制作・保守・広告運用・Web戦略チーム支援など、私たちシエロデザインが伴走できる領域はさまざまです。
「うちの場合どうすれば?」というご相談も、まずはお気軽にお声がけください。
