0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WordPress JSON-LDが二重出力されたときの見つけ方と直し方:Yoast・Rank Mathのフィルタを実測した

0
Last updated at Posted at 2026-10-03

Organizationが二重に出力される実例と確認環境

Yoast SEOやRank Mathを入れたWordPressサイトで、自前のJSON-LDや別のプラグインを足すと、Organizationが2つ出る場合があります。

Rank Mathを入れ、自前のOrganizationを同じ@idで足したトップページの確認結果は、次のとおりです。

[{"script":0,"class":"rank-math-schema","type":"Organization","id":"https://example.com/#organization"},
 {"script":1,"class":"(none)","type":"Organization","id":"https://example.com/#organization"}]

Rank Mathが出力したOrganizationと、自前のwp_head出力が、同じ@idのまま別のscriptに並んでいます。自前出力の@idは、なし・別の値・同じ値のどれにしても、トップにはOrganizationが2つ並びました。

確認環境は表のとおりです。

項目 内容
WordPress 7.1.2
PHP 8.3.33
テーマ Twenty Twenty-Five 1.5
Yoast SEO 28.6
Rank Math SEO 1.0.279
Sugata 1.2.5
確認日 2026-10-02

確認はブラウザ上のWordPress Playgroundで実施しました。サイトは新規の空サイトで、トップは「最新の投稿」表示です。各プラグインの設定は、管理画面ではなくオプション値の直接設定で入れています。設定画面の項目名と並びは未確認です。

Yoastの開発者ドキュメントは、Organizationの@idを#/schema/Organization/1の形で記しています(確認日2026-10-02)。Yoast 28.6の実出力は#organizationでした。Rank Math 1.0.279も同じ#organizationで、記事には実出力の値を載せます。

二重出力の出どころの特定方法

出どころは、scriptタグのクラスで見分けます。出力元ごとのscriptの形は、次の表のとおりでした。

出力元 scriptの形
Yoast SEO class="yoast-schema-graph"の1つのscript。ノードは@graph内
Rank Math class="rank-math-schema"の1つのscript。ノードは@graph内
自前のwp_head出力 クラスなしの単独script

ページのDOMからld+jsonを数えて、Organizationのノードだけを表にするコードを使うと、出力元と@idをまとめて確認できます。ブラウザのコンソールに貼って使います。

[...document.querySelectorAll('script[type="application/ld+json"]')].flatMap((s, i) => {
  const j = JSON.parse(s.textContent);
  return (j['@graph'] || [j]).map(n => ({ script: i, class: s.className || '(none)', type: [].concat(n['@type']).join('|'), id: n['@id'] || '(no @id)' }));
}).filter(r => /Organization/.test(r.type))

結果にOrganizationの行が2つ以上あれば、重複しています。classが違い、@idが同じなら、別の出力元が同じノードを出している状態です。

出力されるページの範囲にも差があります。YoastとRank Mathは、実測ではトップ・投稿・固定ページのすべてにOrganizationを出しました。トップにだけ自前のOrganizationを出している場合は、重なるのもトップだけです。

なお、Yoastのサイト表現を「人」にしている場合、#organizationは出ません。Organizationは["Person","Organization"]の1つのノードに置き換わり、@idも#/schema/person/…の形に変わります。上のコードでは、typeの列がPerson|Organizationの表記で出る点にも注意が必要です。

ページのソースでOrganizationを検索して数える手順は、以前の記事にまとめています。WordPressでOrganization JSON-LDを実装する手順と注意点です。

残す側を決める判断基準

残す側は、内容を誰が保守するかで決めます。更新のたびに片方だけ古くなる状態を避けられるからです。判断軸は3つあります。

判断軸 プラグイン側を残す 自前・別プラグイン側を残す
保守する場所 プラグインの設定画面 コードまたは別プラグインの設定
出力される範囲 実測ではトップ・投稿・固定ページ 実装しだい(今回の例はトップのみ)
項目の揃い方 Yoastは名前のみだと出ない。Rank Mathのトップはnameのみ 任意の項目を足せる(例:telephone)

運用で迷うなら、会社情報の編集を1か所に集められる側を残すのが無難です。プラグイン側を残すなら、止めるのは自前の出力です。自前側を残すなら、次の節の方法で、プラグイン側のOrganizationだけを止めます。

重複したOrganizationを、Googleがどう扱うかについて、公式に明示された記述は確認できませんでした。「混乱する」「無害」のどちらも、公式の根拠はありません。ここでは、影響の有無ではなく、運用の理由から1つに揃える立場をとります。同じ@idのノードが2つ並んだとき、検証ツールが1つに統合するかも未確認です。

プラグイン側の出力を止める方法

止め方には、粒度が3段あります。JSON-LDを全部止める方法、Organizationだけを止める方法、止めずに自前の項目を統合する方法です。調べた範囲では、多くの解説が1段目の一行フィルタで終わっていました。2段目には、止めたあとに別の問題が出ます。

Yoastの出力を止める方法

Yoastでは、次の3つのフィルタを試しました。どれか1つを、テーマのfunctions.phpなどに書きます。

functions.php
// 1. YoastのJSON-LDをすべて止める(WebSite・Articleも消える)
add_filter('wpseo_json_ld_output', '__return_false');

// 2. YoastのOrganizationだけを止める
add_filter('wpseo_schema_needs_organization', '__return_false');

// 3. Yoast側のOrganizationに自前の項目を足して1つにまとめる
add_filter('wpseo_schema_organization', function ($data) {
    $data['telephone'] = '+81-3-0000-0000';
    return $data;
});

実測の結果は、次の表のとおりです。

フィルタ 結果
wpseo_json_ld_outputを__return_false JSON-LDが0個。WebSite・WebPage・Breadcrumb・Article・Personも消える
wpseo_schema_needs_organizationを__return_false Organizationだけが消える。WebSiteのpublisherなどが#organizationを指したまま残る
wpseo_schema_graph_pieces・wpseo_schema_graphでOrganizationを外す 前の行と同じ結果
wpseo_schema_organizationで項目を追加 Yoast側のOrganizationにtelephoneが加わる(@idとlogoは維持)

全部止める方法は、WebSiteやArticleまで消える点が難点です。Organizationだけを止める方法は、一見うまくいきますが、参照切れが残ります。統合する方法では、自前の出力を止めると1つに揃いました。wpseo_schema_needs_organizationは、公式ドキュメントの例がPersonを有効にする向きだけで、Organizationを止める例は見当たりませんでした。Organizationだけが消える点は、実測で確認しています。

Rank Mathの出力を止める方法

Rank Mathの公式ナレッジベースには、rank_math/json_ldでpublisherを外す方法が載っています(Organizationスキーマの削除)。同じページが、個別のエンティティの削除は推奨しないとも警告しています。publisherを外すフィルタを、優先度10と99で試しました。テーマのfunctions.phpと同じafter_setup_themeで登録しています。

functions.php
add_action('after_setup_theme', function () {
    $unset = function ($data) {
        unset($data['publisher']);
        return $data;
    };
    add_filter('rank_math/json_ld', $unset, 10, 2); // 優先度10: Organizationが残った
    // add_filter('rank_math/json_ld', $unset, 99, 2); // 優先度99: Organizationが消えた
});

優先度10ではOrganizationが残り、優先度99では消えました。優先度10が効かない理由は、フックの登録状況から読んだ推測です。Rank Math側がpublisherを足す処理も優先度10で、先に走るため、外す時点ではpublisherがまだ無かったとみています。

そのほかのフィルタと設定の結果は、表にまとめました。

方法 結果
rank_math/json_ldでpublisherを外す(優先度99) Organizationが消える。WebSiteのpublisherなどが宙に浮く
rank_math/json_ldを__return_empty_arrayにする(優先度10) 効かない
同じく優先度99 JSON-LDが0個。メタ・OGPは出力され続ける
rank_math/schema/add_global_entitiesを__return_false トップでは効かない。投稿ではOrganization・WebSite・WebPage・Personが消える
Schemaモジュールをオフ(rank_math_modulesからrich-snippetを除外) JSON-LDが0個。Organizationは残らなかった
rank_math/json_ld(優先度99)でpublisherに項目を追加 Rank Math側のOrganizationにtelephoneが加わる

add_global_entitiesの副作用は、公式ナレッジベースの警告どおりです。ナレッジベースには、Schemaモジュールをオフにしても「Organizationが残る場合がある」との記述もありますが、今回の設定では再現しませんでした。統合する方法は、次のコードで動きました。

functions.php
add_action('after_setup_theme', function () {
    add_filter('rank_math/json_ld', function ($data) {
        if (isset($data['publisher'])) {
            $data['publisher']['telephone'] = '+81-3-0000-0000';
        }
        return $data;
    }, 99, 2);
});

止めたあとの参照切れの再検証

Organizationを止めたあとは、@idで参照しているノードが残っていないかを確認します。参照だけがあり、定義するノードがない状態を、ここでは「宙に浮いた参照」と呼びます。

YoastのOrganizationだけを止めた場合は、トップのCollectionPage(about)とWebSite(publisher)が宙に浮きました。投稿でも、ArticleとWebSiteのpublisherが同じ状態でした。Rank Mathでpublisherを外した場合は、WebSite・CollectionPage・Person・BlogPostingの参照が宙に浮きました。

ページ内の全ld+jsonから、宙に浮いた参照を探すコードを示します。

const nodes = [...document.querySelectorAll('script[type="application/ld+json"]')].map(s => JSON.parse(s.textContent));
const defs = new Set(), refs = [];
const walk = (o, p) => {
  if (Array.isArray(o)) { o.forEach((x, i) => walk(x, `${p}[${i}]`)); return; }
  if (o && typeof o === 'object') {
    const k = Object.keys(o);
    if (k.length === 1 && k[0] === '@id') { refs.push({ at: p, id: o['@id'] }); return; }
    if (o['@id']) defs.add(o['@id']);
    k.forEach(key => walk(o[key], `${p}.${key}`));
  }
};
nodes.forEach((j, i) => walk(j, `s${i}`));
refs.filter(r => !defs.has(r.id)).map(r => `${r.at} -> ${r.id}`)

@idだけを持つオブジェクトを参照とみなし、同じページ内のどのノードにも定義がないものを一覧にします。結果が空の配列なら、参照切れはありません。Playgroundでの測定には、同じ判定をfetchで取得したHTMLに適用する版を使いました。ここに載せたのはページのDOMを直接読む版で、合成データでの動作確認にとどまります。

自前のOrganizationを同じ@idで残すと、トップでは参照が解決しました。自前の出力がトップだけの場合、投稿ページの参照は宙に浮いたままです。統合する方法では、Organizationのノード自体が残るため、参照は切れないはずです(この組み合わせでの判定は実行していません)。

Schema Markup ValidatorやRich Results Testでの見え方は、今回は確認していません。自分のサイトでは、止めたあとに両方のツールでの確認も求められます。

Sugataを併用した場合の挙動

この節で紹介するSugataは、筆者が開発している無料のWordPressプラグインです。重複を扱うための補助ツールの一例であり、利用を前提にした記事ではありません。導入によって検索順位などの効果が得られることを保証するものでもありません。

Organizationを出す専用プラグインの例として、Sugata 1.2.5をYoast・Rank Mathと併用して確認しました。SugataのOrganizationとWebSiteは、@idが#organizationと#websiteで、プラグイン側と同じ値です。出力はトップページのみで、会社名が入力済みのときに出ます。この状態でYoastかRank Mathを有効にすると、トップにOrganizationとWebSiteが2つずつ並びました。

管理画面には、重複の可能性を知らせる黄色の警告と、入力項目の「may overlap」タグが表示されます。この検知は、ほかのSEOプラグインが有効かどうかの判定にもとづいており、実際に同じ出力をしているかは見ていません。フィルタで重複を解消しても、通知は表示されたままでした。Sugataは出力を自動では変えないため、解消には前の節の手動の方法を使います。掲載ページ(Sugata(WordPress公式プラグインディレクトリ))からは、インストールせずにブラウザ上で動作を試せます。

確認した範囲と再発防止の運用

出どころの特定から残す側の判断を経て、止めるか統合するかを選び、参照を再検証する順で対処します。止めるだけでは参照が宙に浮くため、止める前に残す側を決めておくのが要点です。

確認した範囲を、以下にまとめます。

  • WordPress Playground上の新規空サイト
  • 各プラグイン1バージョンのみ
  • 確認日は2026-10-02
  • トップは「最新の投稿」表示のみ

未確認の範囲は、次のとおりです。

  • 静的フロントページ・アーカイブの出力
  • 管理画面の項目名と並び
  • Googleの検証ツールでの表示
  • ほかのバージョンでの挙動

プラグインの更新で、挙動が変わる可能性があります。更新のあとにも、ページ内のJSON-LDを数え、参照切れを確認する運用を勧めます。Rank Math 1.0.279は、新規インストール直後だと、登録を済ませるまでフロントに何も出力しません。Playgroundでは、登録画面のスキップに相当するオプションを入れて確認しました。アカウントの作成や認証情報の入力は、していません。

参照した公式情報は、以下のとおりです。

Playgroundでの再現に使った環境の作り方と、出力を採取するコンソール用コードを、次の折りたたみに載せます。確認日はいずれも2026-10-02です。

Playgroundでの再現手順(コンソール用コード)

https://playground.wordpress.net/ を開き、DevToolsのコンソールで実行します。?n=は、ハッシュだけの変更では再読み込みされないための回避です。slugとkindを'seo-by-rank-math','rm'に変えると、Rank Math単体の環境になります。

const lab = String.raw`<?php
/* dup-lab.php : ?t=own-sameid,y-noorg のようにカンマ区切りで切り替える */
$t  = isset($_GET['t']) ? explode(',', $_GET['t']) : [];
$on = function ($k) use ($t) { return in_array($k, $t, true); };

/* 自前のOrganization(トップページのみ)。@idの3パターン */
add_action('wp_head', function () use ($on) {
  if (!is_front_page()) return;
  $patterns = ['own-noid' => null, 'own-diffid' => '#company', 'own-sameid' => '#organization'];
  foreach ($patterns as $key => $frag) {
    if (!$on($key)) continue;
    $d = ['@context' => 'https://schema.org', '@type' => 'Organization', 'name' => 'Example Corp',
          'url' => home_url('/'), 'telephone' => '+81-3-0000-0000'];
    if ($frag) $d['@id'] = home_url('/' . $frag);
    echo '<script type="application/ld+json">' . wp_json_encode($d, JSON_UNESCAPED_SLASHES) . "</script>\n";
  }
}, 20);

/* Yoast SEO */
if ($on('y-all-off')) add_filter('wpseo_json_ld_output', '__return_false');
if ($on('y-noorg'))   add_filter('wpseo_schema_needs_organization', '__return_false');
if ($on('y-merge'))   add_filter('wpseo_schema_organization', function ($d) { $d['telephone'] = '+81-3-0000-0000'; return $d; });

/* Rank Math(テーマのfunctions.phpと同じ after_setup_theme で登録) */
add_action('after_setup_theme', function () use ($on) {
  $unset = function ($d) { unset($d['publisher']); return $d; };
  if ($on('r-unset-10'))     add_filter('rank_math/json_ld', $unset, 10, 2);
  if ($on('r-unset-99'))     add_filter('rank_math/json_ld', $unset, 99, 2);
  if ($on('r-empty-99'))     add_filter('rank_math/json_ld', '__return_empty_array', 99, 2);
  if ($on('r-global-false')) add_filter('rank_math/schema/add_global_entities', '__return_false');
  if ($on('r-merge'))        add_filter('rank_math/json_ld', function ($d) { if (isset($d['publisher'])) $d['publisher']['telephone'] = '+81-3-0000-0000'; return $d; }, 99, 2);
});
`;

const setup = (kind) => `<?php
require_once '/wordpress/wp-load.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
// ロゴ用の600x600 PNGを生成してメディアに登録
$file = wp_upload_dir()['path'] . '/logo.png';
$im = imagecreatetruecolor(600, 600); imagefill($im, 0, 0, imagecolorallocate($im, 30, 100, 200)); imagepng($im, $file);
$id  = wp_insert_attachment(['post_mime_type' => 'image/png', 'post_title' => 'logo', 'post_status' => 'inherit'], $file);
wp_update_attachment_metadata($id, wp_generate_attachment_metadata($id, $file));
$url = wp_get_attachment_url($id);
` + (kind === 'yoast' ? `// Yoast: サイト表現を「組織」にして名前とロゴを設定
$o = get_option('wpseo_titles');
$o['company_or_person'] = 'company'; $o['company_name'] = 'Example Corp';
$o['company_logo'] = $url; $o['company_logo_id'] = $id;
update_option('wpseo_titles', $o);
` : `// Rank Math: 登録スキップ相当のオプション+Local SEO(会社)を設定
add_option('rank_math_registration_skip', true);
$t = get_option('rank-math-options-titles'); if (!is_array($t)) $t = [];
$t['knowledgegraph_type'] = 'company'; $t['knowledgegraph_name'] = 'Example Corp';
$t['knowledgegraph_logo'] = $url; $t['knowledgegraph_logo_id'] = $id;
update_option('rank-math-options-titles', $t);
`);

const bp = (slug, kind) => ({
  landingPage: '/',
  preferredVersions: { php: '8.3', wp: 'latest' },
  features: { networking: true },
  steps: [
    { step: 'login' },
    { step: 'installPlugin', pluginData: { resource: 'wordpress.org/plugins', slug }, options: { activate: true } },
    { step: 'mkdir', path: '/wordpress/wp-content/mu-plugins' },
    { step: 'writeFile', path: '/wordpress/wp-content/mu-plugins/dup-lab.php', data: lab },
    { step: 'runPHP', code: setup(kind) }
  ]
});

// Yoast。Rank Mathなら bp('seo-by-rank-math','rm')
location.href = 'https://playground.wordpress.net/?n=' + Date.now() + '#' + JSON.stringify(bp('wordpress-seo', 'yoast'));

起動後、読み込み完了(約10秒)を待ってから、コンソールで次のコードを実行します。

// Playgroundのスコープ(/scope:xxxx)を取得
const scope = () => [...document.querySelector('iframe').contentDocument.querySelectorAll('iframe')]
  .map(i => i.contentWindow.location.pathname.match(/\/scope:[^\/]+/)[0])[0];

// ページのld+jsonを抽出
const ld = async (path) => {
  const h = await fetch(scope() + path).then(r => r.text());
  const re = /<script([^>]*type=["']application\/ld\+json["'][^>]*)>([\s\S]*?)<\/script>/g;
  const out = []; let m;
  while ((m = re.exec(h))) { let j; try { j = JSON.parse(m[2]); } catch (e) { j = 'PARSE ERROR'; } out.push({ attrs: m[1].trim(), json: j }); }
  return out;
};

// ノードの一覧(@type と @id)を出力元(class)ごとに表示
const list = async (path) => (await ld(path)).map(s => ({
  cls: (s.attrs.match(/class="([^"]*)"/) || [])[1] || '(no class)',
  nodes: (s.json['@graph'] || [s.json]).map(n => [].concat(n['@type']).join('|') + ' ' + (n['@id'] || '(no @id)'))
}));

// 宙に浮いた参照(@idだけのオブジェクトで、同一ページ内に定義が無いもの)
const dangling = async (path) => {
  const defs = new Set(), refs = [];
  const walk = (o, p) => {
    if (Array.isArray(o)) { o.forEach((x, i) => walk(x, p + '[' + i + ']')); return; }
    if (o && typeof o === 'object') {
      const k = Object.keys(o);
      if (k.length === 1 && k[0] === '@id') { refs.push({ at: p, id: o['@id'] }); return; }
      if (o['@id']) defs.add(o['@id']);
      k.forEach(key => walk(o[key], p + '.' + key));
    }
  };
  (await ld(path)).forEach((s, i) => { if (typeof s.json !== 'string') walk(s.json, 's' + i); });
  return refs.filter(r => !defs.has(r.id)).map(r => r.at + ' -> ' + r.id);
};

// 例: 自前Organization(同じ@id)を足したトップ
await list('/?t=own-sameid');
// 例: YoastのOrganizationだけ止めたときの宙吊り参照(投稿ページ)
await dangling('/?p=1&t=y-noorg');

執筆にはAI(Claude)を活用しました。実測の結果は、Playgroundで確認した内容に限っています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?