[Cloudflare Pages] _redirects で旧WordPressの /投稿ID/ を301リダイレクトする

Tech
[Cloudflare Pages] _redirects で旧WordPressの /投稿ID/ を301リダイレクトする

このブログを WordPress から Astro + Cloudflare Pages に移行しました。 ところが移行後も、旧 WordPress 時代の URL /投稿ID/(例: https://nao-guitarist.com/519/)にまだアクセスが残っていました。

放置すると 404 になってしまうので、旧 URL から新 URL(/blog/slug/)へ 301 リダイレクトして、検索評価とアクセスを引き継ぎます。

Cloudflare Pages なら public/_redirects を置くだけ

Cloudflare Pages は、公開ディレクトリのルートに _redirects というファイルを置くと、その内容でリダイレクトしてくれます。Astro の場合は public/_redirects に置けば、ビルド時に dist/ 直下へコピーされます。

書式は1行1ルールで、移動元 移動先 ステータスコード です。

/519/  /blog/2021-11-14-apache-force-https-htaccess/  301

301 は恒久的な移動(Moved Permanently)。検索エンジンに「新しい URL に評価を移してね」と伝えられます。

トレイリングスラッシュの有無に注意

旧 URL は /519/(末尾スラッシュあり)でしたが、リンク元によっては /519(スラッシュなし)で来ることもあります。Cloudflare Pages の _redirects は完全一致なので、両方書いておくと取りこぼしがありません。

/519/  /blog/2021-11-14-apache-force-https-htaccess/  301
/519   /blog/2021-11-14-apache-force-https-htaccess/  301

投稿ID → 新URL の対応表をどう作るか

問題は「投稿 ID と記事の対応」をどう知るか。これは WordPress の DB(投稿 ID)にしか無い情報なので、WordPress のエクスポート(WXR) から取り出します。

管理画面の ツール > エクスポート から「投稿」を書き出すと、xxxxx.WordPress.YYYY-MM-DD.xml がダウンロードできます。中身を見ると、各記事が wp:post_idtitle を持っています。

<item>
  <title><![CDATA[[apache] http:// でのアクセスを https:// へ強制する(.htaccess)]]></title>
  <link>https://nao-guitarist.com/519/</link>
  <wp:post_id>519</wp:post_id>
  <wp:post_type><![CDATA[post]]></wp:post_type>
  <wp:status><![CDATA[publish]]></wp:status>
</item>

<link> を見れば、旧パーマリンクが /投稿ID/ 形式だったことも確認できます。

あとは、この post_id と、移行後の Markdown ファイル(タイトル)を突き合わせれば対応表が作れます。手作業だと地獄なので、Python でサクッと生成しました。

import xml.etree.ElementTree as ET
import glob, os, re

ns = {'wp': 'http://wordpress.org/export/1.2/'}
tree = ET.parse('nao-guitaristcom.WordPress.2026-06-17.xml')

# WXR から「正規化タイトル -> 投稿ID」を作る(公開記事のみ)
wp = {}
for item in tree.getroot().iter('item'):
    if item.findtext('wp:post_type', '', ns) != 'post':
        continue
    if item.findtext('wp:status', '', ns) != 'publish':
        continue
    pid = item.findtext('wp:post_id', '', ns)
    title = (item.findtext('title', '') or '').strip()
    wp[re.sub(r'\s+', '', title).lower()] = pid

# 移行後の Markdown のタイトルで突合して _redirects を出力
lines = []
for path in sorted(glob.glob('src/content/blog/*.md')):
    slug = os.path.basename(path)[:-3]
    text = open(path, encoding='utf-8').read()
    m = re.search(r'^title:\s*"?(.*?)"?\s*$', text, re.M)
    key = re.sub(r'\s+', '', m.group(1) if m else '').lower()
    if key not in wp:
        continue
    pid = wp[key]
    lines.append(f'/{pid}/  /blog/{slug}/  301')
    lines.append(f'/{pid}   /blog/{slug}/  301')

open('public/_redirects', 'w', encoding='utf-8').write('\n'.join(lines) + '\n')
print(f'{len(lines)} rules written')

タイトルは半角・全角や空白でブレやすいので、空白を除いて小文字化してから突き合わせるとマッチ率が上がります。これで残したい記事ぶんの _redirects が一発で出来上がりました。

デプロイして確認

public/_redirects をコミットして push すれば、Cloudflare Pages 側が勝手に拾ってくれます。デプロイ後に curl で確認できます。

$ curl -sI https://nao-guitarist.com/519/ | grep -i location
location: /blog/2021-11-14-apache-force-https-htaccess/

301 で新 URL に飛んでいれば成功です。

ハマりどころ・メモ

  • _redirectspublic/ 直下に置くsrc/ ではない)。Astro が dist/ にコピーしてくれる。
  • WXR には第三者コメントの個人情報(名前・メール・IP)が含まれることがある。リポジトリにそのままコミットしないよう .gitignore に入れておくと安全。
  • 全記事を移行していない場合、残さなかった記事の旧 URL は 404 のまま。気になるならトップへまとめて飛ばす手もある。

静的サイトへ移行しても、Cloudflare Pages なら 301 がファイル1枚で完結するのは楽でした。

Cloudflare Pages, WordPress, Astro