WordPress×AIOSEO:CPT news の多言語URLをサイトマップに追加(/en・/tc・アーカイブ対応)

目的
CPT news の各記事・アーカイブに対して、対応する /en/tc のURLを AIOSEO(All in One SEO)のサイトマップへ確実に載せる方法をまとめます。
この記事では プラグイン(mu-plugins)版functions.php に追記する版 の両方を掲載します。
以降のURLはすべてダミー例です。

  • 例(単一記事)
    https://example.com/news/1286/https://example.com/en/news/1286/ / https://example.com/tc/news/1286/
  • 例(アーカイブ)
    https://example.com/news/https://example.com/en/news/ / https://example.com/tc/news/
    ページング:/news/page/2//en/news/page/2/ / /tc/news/page/2/

実現方針(2レイヤーで対応)

  1. 単一記事(CPT news
    AIOSEO の aioseo_sitemap_post フィルタで、各記事のサイトマップエントリに 翻訳URL(/en, /tc)を“直接”埋め込みます(hreflang 対応)。
  2. アーカイブ(/news//news/page/N/
    AIOSEO の Additional Pages(追加ページ) 機能を使って、addl-sitemap.xml/en, /tc を登録します。
    追加ページはサイトマップインデックス(/sitemap.xml)に自動で紐づきます。

前提条件

  • 多言語URLは ルート直下プレフィックスで到達可能:/en/...(英語)、/tc/...(繁体字)
    ※異なる規則の場合は後述ヘルパー関数内の生成ロジックを調整
  • AIOSEO v4 系を利用
  • CPT news がアーカイブを持つ(/news/ が表示可能)

実装A:プラグイン版(mu-plugins/推奨)

メリット:テーマ変更の影響を受けず、確実に読み込まれます。
ファイルを作成:wp-content/mu-plugins/aioseo-news-localized-sitemaps.php

<?php
/**
 * Plugin Name: AIOSEO - CPT news の多言語URL(/en, /tc)をサイトマップへ
 * Description: 単一記事は各ポストタイプのサイトマップに翻訳URLとして直付け。アーカイブは addl-sitemap.xml に“追加ページ”として登録。
 * Version: 1.0.0
 */

if (!defined('ABSPATH')) exit;

/** 先頭に /{locale} を付与してURLを生成(既に /en,/tc 配下なら何もしない) */
function ns_loc_inject(string $url, string $locale): string {
    $p = wp_parse_url($url);
    if (empty($p['scheme']) || empty($p['host'])) return $url;
    $path = $p['path'] ?? '/';

    // 既に同ロケール、または他ロケール配下なら何もしない
    if (preg_match('#^/'.preg_quote($locale,'#').'(/|$)#', $path)) return $url;
    if (preg_match('#^/(en|tc)(/|$)#', $path)) return $url;

    $newPath = rtrim('/'.$locale.'/'.ltrim($path,'/'), '/').'/';

    $built = $p['scheme'].'://'.$p['host'].(!empty($p['port'])?':'.$p['port']:'').$newPath;
    if (!empty($p['query']))    $built .= '?'.$p['query'];
    if (!empty($p['fragment'])) $built .= '#'.$p['fragment'];
    return $built;
}

/**
 * 1) 単一投稿(news):そのポストタイプのサイトマップへ“翻訳URL”として直接埋め込む
 *    hreflang キーはサイト方針に合わせて調整可(例:en, zh-Hant / zh-TW など)
 */
add_filter('aioseo_sitemap_post', function ($entry, $postId, $postType) {
    if ($postType !== 'news') return $entry;

    $origin = get_permalink($postId);
    if (!$origin) return $entry;

    $path = wp_parse_url($origin, PHP_URL_PATH) ?: '/';
    if (preg_match('#^/(en|tc)(/|$)#', $path)) return $entry; // 多言語配下は起点にしない

    $entry['translations'] = array_filter([
        'en'      => ns_loc_inject($origin, 'en'), // 例: https://example.com/en/news/1286/
        'zh-Hant' => ns_loc_inject($origin, 'tc'), // 例: https://example.com/tc/news/1286/
    ]);

    return $entry;
}, 10, 3);

/**
 * 2) アーカイブ(/news/, /news/page/N/):“追加ページ”として addl-sitemap.xml に登録
 */
add_filter('aioseo_sitemap_additional_pages', function (array $additional): array {
    $locales = ['en','tc'];

    // 既存の追加URLを控えて重複回避
    $existing = [];
    foreach ($additional as $it) {
        if (!empty($it['loc'])) $existing[$it['loc']] = true;
    }

    $archive_base = get_post_type_archive_link('news');
    if (!$archive_base) return $additional;

    // 総ページ数(公開済み件数 / posts_per_page)
    $counts = wp_count_posts('news');
    $publish = $counts && isset($counts->publish) ? (int) $counts->publish : 0;
    $ppp = max(1, (int) get_option('posts_per_page', 10));
    $total_pages = max(1, (int) ceil($publish / $ppp));

    // 代表 lastmod(最新更新の news)
    $q_last = new WP_Query([
        'post_type'      => ['news'],
        'post_status'    => 'publish',
        'posts_per_page' => 1,
        'orderby'        => 'modified',
        'order'          => 'DESC',
        'no_found_rows'  => true,
    ]);
    $archive_lastmod = gmdate('c');
    if ($q_last->have_posts()) {
        $q_last->the_post();
        $archive_lastmod = get_post_modified_time('c', true, get_the_ID());
    }
    wp_reset_postdata();

    // 1ページ目 + ページング
    $urls = [trailingslashit($archive_base)];
    if ($total_pages > 1) {
        for ($i = 2; $i <= $total_pages; $i++) {
            $urls[] = trailingslashit($archive_base).'page/'.$i.'/';
        }
    }

    foreach ($urls as $url) {
        $path = wp_parse_url($url, PHP_URL_PATH) ?: '/';
        if (preg_match('#^/(en|tc)(/|$)#', $path)) continue; // 多言語側が起点になるケースは想定外

        foreach ($locales as $lc) {
            $loc = ns_loc_inject($url, $lc);
            if (isset($existing[$loc])) continue;
            $additional[] = [
                'loc'        => $loc,
                'lastmod'    => $archive_lastmod,
                'changefreq' => 'weekly',
                'priority'   => 0.5,
            ];
            $existing[$loc] = true;
        }
    }

    return $additional;
});

設置後の操作

  • 管理画面 → All in One SEO → Sitemaps → General SitemapAdditional PagesON
  • ページ/オブジェクト/サーバ/CDN の各キャッシュを削除
  • https://example.com/sitemap.xml(インデックス)から該当XMLを辿って反映確認
  • 単一記事:news のサイトマップ内で 翻訳URL が付与されていること
  • アーカイブ:addl-sitemap.xml 内に /en /tc のアーカイブURLがあること

実装B:functions.php に追記(手早いがテーマ依存)

メリット:すぐ試せる。
デメリット:テーマ更新/切替で消えるリスク。安定運用になったらプラグイン版へ移行推奨。

<?php
// 有効テーマの functions.php に追記

if (!defined('ABSPATH')) exit;

function ns_loc_inject(string $url, string $locale): string {
    $p = wp_parse_url($url);
    if (empty($p['scheme']) || empty($p['host'])) return $url;
    $path = $p['path'] ?? '/';
    if (preg_match('#^/'.preg_quote($locale,'#').'(/|$)#', $path)) return $url;
    if (preg_match('#^/(en|tc)(/|$)#', $path)) return $url;
    $newPath = rtrim('/'.$locale.'/'.ltrim($path,'/'), '/').'/';
    $built = $p['scheme'].'://'.$p['host'].(!empty($p['port'])?':'.$p['port']:'').$newPath;
    if (!empty($p['query']))    $built .= '?'.$p['query'];
    if (!empty($p['fragment'])) $built .= '#'.$p['fragment'];
    return $built;
}

// 単一投稿(news): 翻訳URLを直付け
add_filter('aioseo_sitemap_post', function ($entry, $postId, $postType) {
    if ($postType !== 'news') return $entry;
    $origin = get_permalink($postId);
    if (!$origin) return $entry;
    $path = wp_parse_url($origin, PHP_URL_PATH) ?: '/';
    if (preg_match('#^/(en|tc)(/|$)#', $path)) return $entry;
    $entry['translations'] = array_filter([
        'en'      => ns_loc_inject($origin, 'en'),
        'zh-Hant' => ns_loc_inject($origin, 'tc'),
    ]);
    return $entry;
}, 10, 3);

// アーカイブ: 追加ページで addl-sitemap.xml に
add_filter('aioseo_sitemap_additional_pages', function (array $additional): array {
    $locales = ['en','tc'];
    $existing = [];
    foreach ($additional as $it) { if (!empty($it['loc'])) $existing[$it['loc']] = true; }

    $archive_base = get_post_type_archive_link('news');
    if (!$archive_base) return $additional;

    $counts = wp_count_posts('news');
    $publish = $counts && isset($counts->publish) ? (int) $counts->publish : 0;
    $ppp = max(1, (int) get_option('posts_per_page', 10));
    $total_pages = max(1, (int) ceil($publish / $ppp));

    $q_last = new WP_Query([
        'post_type'      => ['news'],
        'post_status'    => 'publish',
        'posts_per_page' => 1,
        'orderby'        => 'modified',
        'order'          => 'DESC',
        'no_found_rows'  => true,
    ]);
    $archive_lastmod = gmdate('c');
    if ($q_last->have_posts()) { $q_last->the_post(); $archive_lastmod = get_post_modified_time('c', true, get_the_ID()); }
    wp_reset_postdata();

    $urls = [trailingslashit($archive_base)];
    if ($total_pages > 1) {
        for ($i = 2; $i <= $total_pages; $i++) {
            $urls[] = trailingslashit($archive_base).'page/'.$i.'/';
        }
    }

    foreach ($urls as $url) {
        $path = wp_parse_url($url, PHP_URL_PATH) ?: '/';
        if (preg_match('#^/(en|tc)(/|$)#', $path)) continue;
        foreach ($locales as $lc) {
            $loc = ns_loc_inject($url, $lc);
            if (isset($existing[$loc])) continue;
            $additional[] = [
                'loc'        => $loc,
                'lastmod'    => $archive_lastmod,
                'changefreq' => 'weekly',
                'priority'   => 0.5,
            ];
            $existing[$loc] = true;
        }
    }

    return $additional;
});

設定・確認手順

  1. Additional Pages を有効化
    管理画面 → All in One SEO → Sitemaps → General Sitemap → Additional PagesON
  2. キャッシュ削除
    プラグイン/オブジェクト/サーバ/CDN のキャッシュを削除。可能なら *.xml サイトマップはキャッシュ除外に。
  3. 反映確認
    https://example.com/sitemap.xml(インデックス)→
  • news のサイトマップ(CPT用)で 各記事に翻訳URL(/en, /tc) が付いているか
  • addl-sitemap.xmlアーカイブの /en, /tc が含まれているか

注意点・運用メモ

  • URL規則の差異
    英語・繁体字のURLが /en/ /tc/ 以外のルールなら、ns_loc_inject() の生成ロジックを調整してください。
  • hreflang の言語コード
    ここでは例として 'en''zh-Hant' を使用。サイト方針に合わせて 'zh-TW' などへ変更可能。
  • 重複回避
    既に /en /tc 配下のURLは“起点”にしません。フィルタ内で正規表現チェックしてスキップしています。
  • 大規模サイトのパフォーマンス
    追加ページ側は比較的軽量ですが、必要に応じて lastmod 取得(最新1件クエリ)を Transients で短期キャッシュすると安全です。
  • テーマ依存の回避
    安定運用になったら mu-plugins(プラグイン版) へ移行を推奨。wp-content/mu-plugins/ に置くだけで常時読み込みされます。
  • アーカイブのページング規則
    標準は /news/page/N/。独自構成を使っている場合は該当箇所を調整してください。

トラブルシュート

  • 反映されない
    まずフック実行をログで確認:
  add_filter('aioseo_sitemap_post', function($e){ error_log('[AIOSEO] post hook'); return $e; }, 10, 3);
  add_filter('aioseo_sitemap_additional_pages', function($a){ error_log('[AIOSEO] addl hook'); return $a; });

ログが出ない → コードが読まれていない/別環境
ログは出るのにXMLが変わらない → キャッシュ/見ているXMLが違う(インデックスから辿って確認)

  • addl-sitemap.xml が増えた
    追加ページをONにすると AIOSEO が addl-sitemap.xml を生成します。仕様どおりで問題ありません。

まとめ

  • 単一記事(CPT newsaioseo_sitemap_post翻訳URL(/en・/tc)を直接埋め込み
  • アーカイブaioseo_sitemap_additional_pagesaddl-sitemap.xml に登録
  • プラグイン版(mu-plugins) が安定運用に最適、試験導入なら functions.php でも可
  • 実装後は Additional Pages をONキャッシュ削除インデックスから辿って確認 がポイント

コメント

タイトルとURLをコピーしました