WordPress サイトマップが HTML 扱いになる問題を解決するまでの道のり

パソコン

Google によるサイトマップの HTML 誤判定を解消するため、Apache vhost/.htaccess/MU プラグインを組み合わせて「確実に XML + no-store」を返す設定を行った手順を紹介します。


サイトマップが HTML と誤判定される現象

Google Search Console や curl で確認すると、本来 application/xml で返るはずのサイトマップが text/html 扱いになってしまうケースがあります。
実際に curl -I https://example.com/wp-sitemap.xml を叩くと、Content-Type: text/html が出力され、さらにキャッシュ制御ヘッダも HTML 用の public, max-age=1800 が付与されていました。


原因:キャッシュ系ヘッダの競合

  • Apache vhost 側で「HTML用に強キャッシュ」を付与
  • WordPress(MU プラグインやコア)側で「no-cache ヘッダ」を付与
  • sitemap.xml にも両方が適用されてしまい、結果として HTML と誤認識される

対策 1: .htaccess で早期分岐

.htaccess の WordPress ブロックより前に、sitemap 系リクエストを振り分けます。

# /wp-sitemap*.xml はプラグインの /sitemap.xml に統一
RewriteRule ^wp-sitemap\.xml$ /sitemap.xml [R=301,L]
RewriteRule ^wp-sitemap-.*\.xml$ /sitemap.xml [R=301,L] # sitemap.xml を打ち止め(WP に渡さない)
#RewriteRule ^sitemap(_index)?\.xml$ - [L]

対策 2: vhost 側の LocationMatch

vhost 設定で sitemap を強制的に「XML + no-store」に統一します。

<LocationMatch "^/(?:sitemap(?:_index)?|[A-Za-z0-9_-]+-sitemap|wp-sitemap(?:-.*)?)\.xml$"> Require all granted <IfModule mod_headers.c> Header always unset Cache-Control Header always set Cache-Control "no-store, max-age=0" Header always unset Expires Header always unset Set-Cookie Header always set Content-Type "application/xml; charset=UTF-8" # 最終上書き(余計な値を潰す) Header always edit Cache-Control ".*" "no-store, max-age=0" </IfModule>
</LocationMatch> # mod_cache からも完全除外
CacheDisable /sitemap.xml
CacheDisable /wp-sitemap.xml

対策 3: MU プラグインでの補強

WordPress 側で sitemap には余計なヘッダを付けないようフィルタをかけます。

<?php
/*
Plugin Name: Cache Headers (Safe)
*/ add_filter('wp_headers', function(array $headers): array { $uri = $_SERVER['REQUEST_URI'] ?? ''; // sitemap はサーバ側に任せる(no-store) if (preg_match('#^/(?:sitemap(?:_index)?\.xml|[A-Za-z0-9_-]+-sitemap(?:\.xml)?|wp-sitemap(?:-.*)?\.xml)$#', $uri)) { unset($headers['Cache-Control'], $headers['Expires'], $headers['Pragma']); return $headers; } // 管理画面やログイン時は no-store if (is_admin() || is_user_logged_in()) { $headers['Cache-Control'] = 'private, no-store, no-cache, must-revalidate, max-age=0'; unset($headers['Expires'], $headers['Pragma']); return $headers; } // 匿名フロントページのみ強キャッシュ $headers['Cache-Control'] = 'public, max-age=1800, s-maxage=1800'; $headers['Expires'] = gmdate('D, d M Y H:i:s', time() + 1800) . ' GMT'; unset($headers['Pragma']); return $headers;
}, 5);

確認コマンド

# サイトマップは XML + no-store
curl -sI -X GET https://example.com/sitemap.xml \ | egrep -i 'http/|cache-control|content-type' # トップページは public キャッシュ
curl -sI -X GET https://example.com/ \ | egrep -i 'http/|cache-control|content-type'

まとめ

  • サイトマップには no-store を徹底してキャッシュさせない
  • HTML ページは public キャッシュ で高速化
  • vhost と MU プラグインの両面から制御して安定動作

これで Google からも sitemap が確実に application/xml として認識されるようになりました。