2026年8月6日
2026年8月6日
WordPressのメディアフォルダ構造を年月からカテゴリー別に移行する方法
WordPressのアップロード先を年月フォルダ(2026/10/ など)からカテゴリー別フォルダに変えるには、「これからアップロードする画像の保存先を変える」作業と、「既存の画像ファイルを移動し、データベースと本文中のURLを書き換える」作業の2段階があります。前者は upload_dir フィルターで比較的安全に実現できますが、後者はURLが変わるため、リダイレクトまで含めた計画が要ります。この記事では、両方の手順と、そもそも物理的な移動が必要かを判断するための考え方を説明します。
先に確認したいこと:本当にファイルを移動する必要があるか
WordPressのメディアには、そもそも「カテゴリー」という属性がありません。画像が属するのは「アップロードしたときの投稿(親投稿)」だけで、同じ画像を複数の投稿で使うこともできます。そのため「画像のカテゴリー」は、親投稿のカテゴリーから推定するしかありません。
管理画面で画像を探しやすくしたいだけなら、ファイルを動かさずに、メディアライブラリ上に仮想フォルダを作るプラグイン(FileBird、Real Media Library など)を使う方が安全です。URLが変わらないので、リンク切れや検索エンジンへの影響を気にせずに済みます。プラグインの機能や無料版・有料版の範囲は版によって変わるため、導入前に確認してください。
物理的な移動が向いているのは、サーバー上のファイルを外部のツールやバックアップ単位でカテゴリーごとに扱いたい場合などに限られます。次のどれかに当てはまるなら、既存ファイルは動かさず、新規分だけ保存先を変える方法を検討してください。
- 公開から時間がたち、画像URLが外部サイトやSNSで共有されている
- ページビルダーやテーマのオプションに画像URLが保存されている
- 1つの画像を複数カテゴリーの記事で使い回している
新しいアップロードの保存先をカテゴリー別にする
年月フォルダの設定を確認する
「設定」→「メディア」にある、アップロードしたファイルを年月ベースのフォルダに整理する項目は、内部的には uploads_use_yearmonth_folders オプションです。これをオフにするとファイルは uploads/ 直下に置かれます。以下のフィルターは年月フォルダの部分を差し替えるので、この設定はどちらでも動作します。
upload_dir フィルターで保存先を変える
upload_dir フィルターはアップロード以外の場面(画像URLの生成など)でも呼ばれるため、アップロード処理の間だけ有効にします。wp_handle_upload_prefilter で追加し、wp_handle_upload で外すのが定番の書き方です。コードは独自プラグインかmu-pluginに置きます。
<?php
/**
* Plugin Name: Upload Dir by Category
*/
defined( 'ABSPATH' ) || exit;
function ucat_upload_dir( $dirs ) {
$post_id = 0;
if ( isset( $_POST['post_id'] ) ) { // 旧来のメディアモーダル
$post_id = absint( $_POST['post_id'] );
} elseif ( isset( $_POST['post'] ) ) { // ブロックエディター(REST API)
$post_id = absint( $_POST['post'] );
}
if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
return $dirs;
}
$cats = get_the_category( $post_id );
if ( empty( $cats ) ) {
return $dirs;
}
$sub = '/cat/' . sanitize_title( $cats[0]->slug );
$dirs['subdir'] = $sub;
$dirs['path'] = $dirs['basedir'] . $sub;
$dirs['url'] = $dirs['baseurl'] . $sub;
return $dirs;
}
add_filter( 'wp_handle_upload_prefilter', function ( $file ) {
add_filter( 'upload_dir', 'ucat_upload_dir' );
return $file;
} );
add_filter( 'wp_handle_upload', function ( $upload ) {
remove_filter( 'upload_dir', 'ucat_upload_dir' );
return $upload;
} );
アップロード自体の権限確認とnonce検証はWordPress本体が行います。このコードでは、送られてきた投稿IDを absint() で整数にし、その投稿の編集権限を確認してからカテゴリーを読みます。フォルダ名は sanitize_title() を通したスラッグなので、日本語名のカテゴリーでもURLに使える文字列になります。年月フォルダと区別するため cat/ を挟んでいます。
この方法の限界
- 新規投稿でカテゴリーを選ぶ前に画像をアップロードすると、その時点のカテゴリー(多くは既定の「未分類」)のフォルダに入ります。運用では、カテゴリーを先に決めてから画像を入れるようにします。
- 後から投稿のカテゴリーを変えても、ファイルは移動しません。
- メディアライブラリから直接アップロードした画像は親投稿が無いため、通常の年月フォルダに入ります。
- 投稿IDの受け渡し方はエディターの版によって変わる可能性があります。動作をステージングで確かめ、取れない場合は既定の保存先に戻る作りにしておきます(上のコードはそうなっています)。
既存のファイルを移動する手順
ここからはURLが変わる作業です。全件を移す必要はなく、対象を絞っても構いません。進める場合は、データベースと wp-content/uploads の両方のバックアップを取り、ステージング環境で最後まで試してから本番で行ってください。
移動で更新が必要なもの
1枚の画像を移動すると、次のすべてを揃えて更新する必要があります。
- 元画像と、すべての中間サイズのファイル(
photo-300x200.jpgなど)。大きな画像では、縮小版-scaledの元になったオリジナルファイルも残っています - 添付ファイルのメタ情報
_wp_attached_file(uploadsからの相対パス) - メタ情報
_wp_attachment_metadataのfile(中間サイズはファイル名だけで保存されているので、同じフォルダに移せばそのまま使えます) - 投稿本文などに直接書かれている画像URL
- 外部からの古いURLへのアクセスに対するリダイレクト
WP-CLIで移動するスクリプト
wp eval-file で実行するスクリプトの例です。引数に dry を付けると、移動せずに予定だけを表示します。移動した旧URLと新URLの対応はCSVに記録し、後の置換とリダイレクトに使います。
<?php
// wp eval-file move-media.php dry ← まず dry で確認
$dry = in_array( 'dry', $args, true );
$up = wp_get_upload_dir();
$basedir = trailingslashit( $up['basedir'] );
$baseurl = trailingslashit( $up['baseurl'] );
$csv = fopen( getcwd() . '/media-move-map.csv', 'a' );
$ids = get_posts( array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'posts_per_page' => -1,
'fields' => 'ids',
'post_parent__not_in' => array( 0 ),
) );
foreach ( $ids as $id ) {
$rel = get_post_meta( $id, '_wp_attached_file', true );
if ( ! preg_match( '#^\d{4}/\d{2}/#', $rel ) ) {
continue; // 移動済み、または年月フォルダ以外
}
$cats = get_the_category( wp_get_post_parent_id( $id ) );
if ( empty( $cats ) ) {
continue;
}
$old_dir = dirname( $rel );
$new_dir = 'cat/' . sanitize_title( $cats[0]->slug );
$meta = wp_get_attachment_metadata( $id );
$files = array( basename( $rel ) );
foreach ( (array) ( $meta['sizes'] ?? array() ) as $s ) {
$files[] = $s['file'];
}
if ( ! empty( $meta['original_image'] ) ) {
$files[] = $meta['original_image'];
}
$files = array_unique( $files );
foreach ( $files as $f ) {
if ( file_exists( "{$basedir}{$new_dir}/{$f}" ) ) {
WP_CLI::warning( "同名ファイルあり、スキップ: {$id} {$new_dir}/{$f}" );
continue 2;
}
}
if ( $dry ) {
WP_CLI::log( "{$id}: {$old_dir} -> {$new_dir} (" . count( $files ) . ' files)' );
continue;
}
wp_mkdir_p( $basedir . $new_dir );
foreach ( $files as $f ) {
if ( file_exists( "{$basedir}{$old_dir}/{$f}" ) && rename( "{$basedir}{$old_dir}/{$f}", "{$basedir}{$new_dir}/{$f}" ) ) {
fputcsv( $csv, array( "{$baseurl}{$old_dir}/{$f}", "{$baseurl}{$new_dir}/{$f}" ) );
}
}
update_attached_file( $id, "{$basedir}{$new_dir}/" . basename( $rel ) );
if ( is_array( $meta ) ) {
$meta['file'] = $new_dir . '/' . basename( $rel );
wp_update_attachment_metadata( $id, $meta );
}
WP_CLI::log( "moved: {$id}" );
}
fclose( $csv );
移動済みのファイルは年月パスでなくなるため、途中で止まっても再実行すれば残りから続きます。件数が多い場合はアクセスの少ない時間帯に実行します。別の投稿の画像と同じファイル名がある場合は上書きを避けてスキップするので、警告が出たものは個別に判断します。
本文中のURLを置換する
wp_get_attachment_url() などの関数はメタ情報からURLを作るため、移動後は新しいURLを返します。一方、投稿本文に書き込まれたURLは古いままです。CSVの各行について、まず --dry-run で件数を確認してから置換します。
wp search-replace 'https://example.com/wp-content/uploads/2024/05/photo.jpg' \
'https://example.com/wp-content/uploads/cat/news/photo.jpg' \
--dry-run --report-changed-only
wp search-replace はシリアライズされたデータ(テーマ設定やページビルダーの保存データ)も壊さずに置換します。件数が多い場合は、CSVを読んで順に実行する小さなシェルスクリプトにします。置換後は wp cache flush でオブジェクトキャッシュを消し、ページキャッシュも削除します。
古いURLをリダイレクトする
外部サイトや検索結果からの古いURLへのアクセスに備えて、CSVからサーバーのリダイレクト設定を作ります。Apacheなら .htaccess に次のような行を並べます(Nginxでは map などで同等の設定を書きます)。
Redirect 301 /wp-content/uploads/2024/05/photo.jpg /wp-content/uploads/cat/news/photo.jpg
リダイレクトの設定は数が多いとサーバーの処理が重くなることがあります。アクセスログを見て、実際にアクセスがある古いURLに絞るのも一つの方法です。
移行後の確認
- カテゴリーごとにいくつかの記事を開き、画像がすべて表示されるか、開発者ツールのネットワークタブで404が出ていないかを確認する
- メディアライブラリで画像を開き、URLとサムネイルが新しいフォルダを指しているか確認する
- 古い画像URLを直接開き、301で新しいURLに転送されるか確認する
- Search Consoleなどで、しばらくのあいだ画像のクロールエラーが増えていないかを見る
まとめ
- メディアにはカテゴリーという属性が無く、親投稿のカテゴリーから推定します。管理画面で探しやすくしたいだけなら、仮想フォルダ型のプラグインの方が安全です。
- 新規アップロードは、
wp_handle_upload_prefilterの間だけupload_dirフィルターを有効にして保存先を変えます。投稿IDの権限確認とスラッグのサニタイズを入れます。 - 既存ファイルの移動では、全サイズのファイル・
_wp_attached_file・メタ情報・本文中のURL・リダイレクトを揃えて更新します。 - 作業はバックアップを取り、ステージングで
dry実行と--dry-runから始めます。WordPressやWP-CLIの挙動(2026年10月現在)は版によって変わることがあるため、本番の前に手順を通しで確認してください。