Steamer Lane Studio技術備忘録WordPress

WordPressで親子関係のカテゴリーを一画面で作り保存させる機能を付与するコード

wordpress WordPressで親子関係のカテゴリーを一画面で作り保存させる機能を付与するコード 作成日: 2025年9月2日

WordPressでサイト制作を行う際に、予めカテゴリーの階層構造などが分かっている場合(普通設計に要るから解るはずだけど)、親作ってから子を作ってその際に都度親選ぶやり方=通常のやり方でも良いのだが、特にカテゴリーの加増等をユーザーサイドで行う場合などUI上よろしくないのでfunction.php追記するコードを作った。
当スタジオのWPテーマ『Initialize』では、カテゴリー設定ページの『カテゴリーの説明』をカテゴリーページのmeta descriptionに呼び出しているが、keywordsフィールドに相当するものはデフォルトでは無いのでこのフィールドを追加。
整合性のためデフォルトのカテゴリー編集画面にも、このkeywords入力欄を設置。
現状meta keywordsはSEO上無用とされているが、いつあった方が良いになるか判らないし、Google1強(YahooがGoogleのエンジンだし)といえる現状が変わるかもしれないので、ネガティブ事象を作らない・msnなど向けまたはアルゴリズム変化への担保として、UI向上によるカテゴリー設定の一部となるように配置した。

WP6.8.2環境で開発、PHP8で多分動くと思うが、REST APIを使ってるので、4.4以降は必須。テーマなどによりREST API関連のエラーが出るかもしれません。

コード

<?php/*** カスタムカテゴリ編集UIの管理画面とAPIエンドポイントを登録** このコードは、WordPressのテーマ(または子テーマ)のfunctions.phpファイルに追記してください。*/

// 1. カスタム管理画面を登録するadd_action('admin_menu', 'my_plugin_add_admin_menu');

function my_plugin_add_admin_menu() {add_menu_page('カテゴリー&子カテゴリー管理', // ページタイトル'カテゴリー&子カテゴリー', // メニュータイトル'manage_categories', // 必要な権限'event-class-manager', // スラッグ'my_plugin_render_admin_page', // ページの内容を表示する関数'dashicons-category', // メニューアイコン25 // 表示位置);}

// 2. カスタム管理画面の内容を表示する関数function my_plugin_render_admin_page() {?><div class="wrap"><h2>カテゴリー&子カテゴリー管理</h2><!-- UIが表示される場所 --><div id="custom-ui-container"></div></div><script>// UIのHTML内容をJavaScriptで挿入document.addEventListener('DOMContentLoaded', () => {const container = document.getElementById('custom-ui-container');if (container) {container.innerHTML = `<div class="container bg-white p-8 rounded-lg shadow-lg"><h1 class="text-2xl font-bold mb-6">カテゴリー&子カテゴリーを編集</h1><!-- カテゴリー名の編集フォーム --><div id="parent-category-form" class="mb-6 border-b pb-4"><label for="event-name" class="block text-gray-700 font-semibold mb-2">カテゴリー名</label><input type="text" id="event-name" placeholder="例: カテゴリー"class="w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"></div>

<!-- カテゴリーの説明フォーム --><div id="category-description-form" class="mb-6 border-b pb-4"><label for="category-description" class="block text-gray-700 font-semibold mb-2">カテゴリーの説明</label><textarea id="category-description" placeholder="例: このカテゴリーはホゲホゲです" rows="4"class="w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea></div><!-- メタキーワードのフォーム --><div id="meta-keywords-form" class="mb-6 border-b pb-4"><label for="meta-keywords" class="block text-gray-700 font-semibold mb-2">メタキーワード</label><input type="text" id="meta-keywords" placeholder="例: カテゴリー, ホゲホゲ, 2023"class="w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"></div>

<!-- 子カテゴリーの追加エリア --><div id="child-categories-container" class="mb-6"><h2 class="text-xl font-bold mb-4">子カテゴリー</h2></div>

<!-- 子カテゴリー追加ボタン --><button id="add-child-btn" class="w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-lg transition-colors duration-200">+ 子カテゴリーを追加</button>

<!-- 保存ボタン --><button id="save-categories-btn" class="w-full mt-6 bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-4 rounded-lg transition-colors duration-200">保存</button>

<!-- 動作確認用のメッセージエリア --><div id="message-area" class="mt-6 p-4 rounded-lg bg-gray-200 text-gray-800 hidden"></div></div>`;}

// Tailwind CSS の読み込みconst tailwindScript = document.createElement('script');tailwindScript.src = "https://cdn.tailwindcss.com";document.head.appendChild(tailwindScript);

// UIを動作させるJavaScriptコードconst addJs = () => {const addChildBtn = document.getElementById('add-child-btn');const saveBtn = document.getElementById('save-categories-btn');const childContainer = document.getElementById('child-categories-container');const messageArea = document.getElementById('message-area');

let childCount = 0;

const addChildField = (childData = { name: '', description: '', keywords: '' }) => {const childId = `child-${childCount++}`;const div = document.createElement('div');div.id = childId;div.className = 'flex flex-col space-y-2 mb-4 p-4 border rounded-lg shadow-sm';div.innerHTML = `<div class="flex items-center space-x-2"><input type="text" value="${childData.name}" placeholder="子カテゴリー名"class="flex-grow p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 child-input-name"><button type="button" class="remove-child-btn bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center font-bold">-</button></div><textarea placeholder="子カテゴリーの説明" rows="2"class="w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 child-input-desc">${childData.description}</textarea><input type="text" value="${childData.keywords}" placeholder="メタキーワード"class="w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 child-input-keywords">`;childContainer.appendChild(div);

div.querySelector('.remove-child-btn').addEventListener('click', () => {div.remove();});};

addChildBtn.addEventListener('click', () => {addChildField();});

saveBtn.addEventListener('click', () => {const parentName = document.getElementById('event-name').value.trim();const categoryDescription = document.getElementById('category-description').value.trim();const metaKeywords = document.getElementById('meta-keywords').value.trim();const childElements = document.querySelectorAll('#child-categories-container > div');const children = Array.from(childElements).map(element => {const name = element.querySelector('.child-input-name').value.trim();const description = element.querySelector('.child-input-desc').value.trim();const keywords = element.querySelector('.child-input-keywords').value.trim();return { name, description, keywords };}).filter(child => child.name.length > 0);

if (!parentName && children.length === 0) {showMessage('カテゴリー名か子カテゴリー名を最低1つ入力してください。');return;}

// 実際のWordPressへのデータ送信fetch('/wp-json/my-plugin/v1/save-categories', {method: 'POST',headers: {'Content-Type': 'application/json','X-WP-Nonce': '<?php echo wp_create_nonce('wp_rest'); ?>'},body: JSON.stringify({parent: parentName,children: children,description: categoryDescription,keywords: metaKeywords})}).then(response => response.json()).then(data => {showMessage(data.message, 'success');}).catch(error => {console.error('Error:', error);showMessage('データの保存に失敗しました。', 'error');});});

const showMessage = (message, type = 'error') => {messageArea.textContent = message;messageArea.classList.remove('hidden');messageArea.classList.remove('bg-red-200', 'bg-green-200');if (type === 'success') {messageArea.classList.add('bg-green-200');} else {messageArea.classList.add('bg-red-200');}};

// 初期表示として子カテゴリの入力欄を1つ追加addChildField();};

// スクリプトの読み込み後に実行tailwindScript.onload = addJs;});</script><?php}

// 3. REST APIエンドポイントadd_action('rest_api_init', 'my_plugin_register_category_route');

function my_plugin_register_category_route() {register_rest_route('my-plugin/v1', '/save-categories', array('methods' => 'POST','callback' => 'my_plugin_save_categories','permission_callback' => function () {return current_user_can('manage_categories');}));}

function my_plugin_save_categories(WP_REST_Request $request) {$data = $request->get_json_params();$parent_name = sanitize_text_field($data['parent']);$description = sanitize_textarea_field($data['description']);$keywords = sanitize_text_field($data['keywords']); // キーワードを取得$child_data = $data['children'];

// 親カテゴリが存在するか確認し、なければ作成$parent_term = term_exists($parent_name, 'category');if ($parent_term === 0 || $parent_term === null) {$parent_term = wp_insert_term($parent_name, 'category', array('description' => $description,));}

if (is_wp_error($parent_term)) {return new WP_REST_Response(array('message' => 'Failed to create parent category.'), 500);}$parent_id = $parent_term['term_id'];

// 親カテゴリのキーワードをメタフィールドとして保存update_term_meta($parent_id, 'cat_keyword', $keywords);

// 子カテゴリをループで処理foreach ($child_data as $child) {$child_name = sanitize_text_field($child['name']);$child_description = sanitize_textarea_field($child['description']);$child_keywords = sanitize_text_field($child['keywords']); // 子カテゴリのキーワードを取得

if (!empty($child_name)) {$child_term = term_exists($child_name, 'category', $parent_id);if ($child_term === 0 || $child_term === null) {$inserted_child = wp_insert_term($child_name, 'category', array('parent' => $parent_id,'description' => $child_description,));if (!is_wp_error($inserted_child)) {update_term_meta($inserted_child['term_id'], 'cat_keyword', $child_keywords);}} else {update_term_meta($child_term['term_id'], 'cat_keyword', $child_keywords);}}}

return new WP_REST_Response(array('message' => 'Categories saved successfully!'), 200);}

// 4. デフォルトのカテゴリ追加画面にキーワード入力フィールドを追加function my_plugin_add_keyword_field() {?><div class="form-field term-keywords-wrap"><label for="cat_keyword">メタキーワード</label><input type="text" name="cat_keyword" id="cat_keyword" value="" placeholder="例: カテゴリー, キーワード, 2023" /><p class="description">カテゴリページ用のメタキーワードを入力します(ACFフィールドと同様に動作します)。</p></div><?php}add_action('category_add_form_fields', 'my_plugin_add_keyword_field');

// 5. デフォルトのカテゴリ編集画面にキーワード入力フィールドを追加function my_plugin_edit_keyword_field($term) {// 既存のキーワードを取得$keywords = get_term_meta($term->term_id, 'cat_keyword', true);?><tr class="form-field term-keywords-wrap"><th scope="row"><label for="cat_keyword">メタキーワード</label></th><td><input type="text" name="cat_keyword" id="cat_keyword" value="<?php echo esc_attr($keywords); ?>" placeholder="例: カテゴリー, キーワード, 2023" /><p class="description">カテゴリページ用のメタキーワードを入力します(ACFフィールドと同様に動作します)。</p></td></tr><?php}add_action('category_edit_form_fields', 'my_plugin_edit_keyword_field');

// 6. メタキーワードを保存するfunction my_plugin_save_keyword($term_id) {if (isset($_POST['cat_keyword'])) {$keywords = sanitize_text_field($_POST['cat_keyword']);update_term_meta($term_id, 'cat_keyword', $keywords);}}add_action('edited_category', 'my_plugin_save_keyword');add_action('create_category', 'my_plugin_save_keyword');