WordPressで独自の投稿フォームをプラグイン化する方法
WordPressの管理画面外から記事を投稿したい、と考えたことはありませんか?例えば、会員専用の投稿フォームをWebサイトのフロントエンドに設置したい場合などです。この記事では、WordPressの標準関数 wp_insert_post() を活用し、独自の投稿フォーム機能を持つプラグインを作成する手順を、コードを交えて具体的に解説します。
記事投稿の核となる関数「wp_insert_post()」
wp_insert_post() は、WordPressの記事(投稿や固定ページなど)をデータベースに挿入・更新するための非常に強力な関数です。この関数を使うことで、管理画面を介さずにプログラムから直接記事を操作できます。
基本的な使い方は、投稿したい内容を配列($post)にまとめ、引数として渡すだけです。オプションの第2引数 $wp_error を true に設定すると、エラー発生時に WP_Error オブジェクトを返してくれます。
$my_post = array(
'ID' => [ <投稿 ID> ], // 既存の投稿を更新する場合に指定
'post_content' => [ <文字列> ], // 投稿の全文(必須)
'post_title' => [ <文字列> ], // 投稿のタイトル(必須)
'post_status' => [ 'publish' | 'draft' | 'pending' ... ], // 投稿ステータス
'post_name' => [ <文字列> ], // 投稿のスラッグ
'post_type' => [ 'post' | 'page' | 'カスタム投稿タイプ' ], // 投稿タイプ
'post_category'=> [ array(<カテゴリー ID>, ...) ], // カテゴリーIDの配列
'tags_input' => [ 'タグ1, タグ2, ...' | array ] // タグ
);
// 投稿を実行
wp_insert_post( $my_post, $wp_error );
※post_title と post_content は、最低限必要な引数です。
プラグイン実装までの3ステップ
この関数を使って、ショートコードで呼び出せる投稿フォームプラグインを作成します。主な実装ステップは以下の3つです。
ステップ1. 投稿フォームのHTMLを作成する
まずは、記事データを送信するための基本的なHTMLフォームを用意します。method="POST" でデータを送信するのが一般的です。
<form action="[送信するURL]" method="POST">
<!-- タイトル入力 -->
<input type="text" name="post_title" placeholder="ここにタイトルを入力">
<!-- 本文入力 -->
<textarea name="post_content"></textarea>
<button type="submit">送信する</button>
</form>
ステップ2. セキュリティ対策:「nonce」の設定
フォームからデータベースを操作する際は、セキュリティ対策が不可欠です。特に、不正なリクエスト(CSRF:クロスサイト・リクエスト・フォージェリ攻撃など)を防ぐために「nonce(ノンス)」と呼ばれる一時的な認証キーを使用します。
WordPressでは wp_nonce_field() という便利な関数を使うだけで、簡単にnonceをフォームに組み込めます。
<form action="[送信するURL]" method="POST">
<?php
// nonceフィールドをフォーム内に追加
wp_nonce_field('outside_postform', 'nonce_outside_postform');
?>
<!-- 入力フィールドの設定 -->
<input type="text" name="post_title" placeholder="ここにタイトルを入力">
<textarea name="post_content"></textarea>
<button type="submit">送信する</button>
</form>
データを受け取った側では、wp_verify_nonce() 関数を使って、設定したnonceが正しいかを必ずチェックします。
ちなみに、wp_insert_post() は、内部でデータのサニタイズ(無害化処理)を行ってくれますが、nonceによる認証チェックは別途必ず実行しましょう。
ステップ3. 重複投稿を防ぐリダイレクト処理
フォームを送信した後、ユーザーがブラウザの「更新(リロード)」ボタンを押すと、同じデータが再度POSTされ、記事が二重に投稿されてしまう問題があります。
これを防ぐため、wp_insert_post() で処理が完了したら、wp_redirect() を使って同じページにリダイレクトさせます。このとき、URLのパラメータ(GETパラメータ)に処理結果のメッセージを付与するのがポイントです。
// (run_insert_post関数内を想定)
// ... wp_insert_post() の処理 ...
if ($insert_status) {
$mesg = '投稿完了';
} else {
$mesg = '投稿失敗';
}
// ...
// 現在のページのURLにステータスを付与してリダイレクト
$url = add_query_arg(array('outside_postform_func_status' => $mesg), get_the_permalink($this->this_post->ID));
wp_redirect($url);
exit;
アラート表示とURLのクリーンアップ
リダイレクト先(つまり同じページ)では、URLパラメータをチェックし、JavaScript(alert())を実行します。アラートを表示した後は、location.href を使ってURLから不要なパラメータを取り除いた状態に再度遷移させると、見た目がクリーンになります。
// アラート後にページの遷移
public function run_alert()
{
$link = get_the_permalink($this->this_post->ID);
$insert_status = urldecode($this->get_data['outside_postform_func_status']);
// JavaScriptを出力
print <<< EOT
<script>
alert("{$insert_status}");
location.href = "{$link}"; // パラメータを除去したURLに遷移
</script>
EOT;
}
このリダイレクト処理(run_insert_post)はHTMLが出力される前に実行する必要があるため get_header フックを、JavaScriptの出力(run_alert)は <head> タグ内に記述したいため wp_head フックを利用します。
投稿フォームプラグインの全コード
ここまでの内容をまとめ、ショートコードで投稿フォームを呼び出せるプラグイン(クラス形式)の全コードです。
<?php
/*
Plugin Name: Outside Postform
Description: 外部フォームから投稿を可能にするプラグイン
Version: 1.0
Author: (あなたの名前)
*/
// クラスのインスタンスを作成
$outside_postform = new OutsidePostform();
class OutsidePostform
{
private $plugin_path;
private $post_data;
private $get_data;
private $this_post; // リダイレクト元の投稿オブジェクトを保持
// コンストラクタ:フックの設定など
public function __construct()
{
$this->plugin_path = WP_PLUGIN_URL . '/' . str_replace(basename(__FILE__), "", plugin_basename(__FILE__));
// ショートコード [outside_postform] を登録
add_shortcode('outside_postform', array($this, 'outside_postform_func'));
// 必要なCSSを読み込む
add_action('wp_enqueue_scripts', array($this, 'theme_name_scripts'));
// (おまけ:CSVのアップロードを許可する場合)
add_filter('upload_mimes', array($this, 'custom_mime_types'));
// --- POSTデータの処理 ---
$args = array(
'nonce_outside_postform' => FILTER_SANITIZE_ENCODED
);
$this->post_data = filter_input_array(INPUT_POST, $args);
// nonceが送信されてきた場合
if ($this->post_data['nonce_outside_postform']) {
// HTML出力前に記事登録処理を実行
add_action('get_header', array($this, 'run_insert_post'));
}
// --- GETデータの処理(リダイレクト後) ---
$args = array(
'outside_postform_func_status' => FILTER_SANITIZE_ENCODED
);
$this->get_data = filter_input_array(INPUT_GET, $args);
// ステータスパラメータがURLにある場合
if ($this->get_data['outside_postform_func_status']) {
// wp_headでアラート用のJSを実行
add_action('wp_head', array($this, 'run_alert'));
}
}
// (おまけ:CSVのMIMEタイプを許可)
public function custom_mime_types($mimes)
{
$mimes['csv'] = 'text/csv';
return $mimes;
}
// ショートコード [outside_postform] の中身
public function outside_postform_func()
{
$form = '';
// ログインユーザーのみにフォームを表示
if (is_user_logged_in()) {
$nonce = wp_nonce_field('outside_postform', 'nonce_outside_postform', true, false);
$action_url = $_SERVER['REQUEST_URI'];
// --- 投稿タイプ一覧を取得 ---
$select_option = '';
$args = array(
'public' => true,
);
$post_types = get_post_types($args, 'names');
unset($post_types['attachment']); // attachment(メディア)は除外
foreach ($post_types as $v) {
$select_option .= '<option>' . $v . '</option>';
}
unset($v);
// --- カテゴリーとタグのリストを取得 ---
$cats_li = $this->get_post_category_terms('category');
$tags_li = $this->get_post_category_terms('post_tag');
// --- フォームHTML(ヒアドキュメント) ---
$form = <<< EOT
<form id="outside_postform_func" action="{$action_url}" method="POST">
{$nonce}
<div>
<input type="number" name="ID" value="" placeholder="ID">
<span class="sub">※修正の場合はIDを入力する</span>
</div>
<div>
<input type="text" name="post_title" value="" placeholder="ここにタイトルを入力" required>
</div>
<div>
<input type="text" name="post_name" value="" placeholder="スラッグ">
</div>
<div>
<span class="heading">投稿タイプ</span>
<select name="post_type">{$select_option}</select>
</div>
<div>
<span class="heading">カテゴリー</span>
{$cats_li}
</div>
<div>
<span class="heading">タグ</span>
{$tags_li}
</div>
<div>
<textarea name="post_content" placeholder="本文を入力"></textarea>
</div>
<button class="button button-large button-primary" type="submit">送信する</button>
</form>
EOT;
} else {
$form = '<p>投稿フォームを表示するにはログインが必要です。</p>';
}
return $form;
}
// カテゴリーやタグのリスト(チェックボックス)を生成
private function get_post_category_terms($taxonomies)
{
$checkbox_li = '<ul class="terms_li">';
$args = array(
'hide_empty' => false,
);
$cats = get_terms($taxonomies, $args);
foreach ($cats as $v) {
$checkbox_li .= <<< EOT
<li>
<label><input type="checkbox" name="{$taxonomies}[]" value="{$v->term_id}">{$v->name}</label>
</li>
EOT;
}
unset($v);
$checkbox_li .= '</ul>';
return $checkbox_li;
}
// 外部CSSファイルの読み込み
public function theme_name_scripts()
{
// プラグインディレクトリ内の 'assets/css/style.css' を読み込む
wp_enqueue_style('style-name', $this->plugin_path . 'assets/css/style.css');
}
// 記事の挿入・更新処理を実行
public function run_insert_post()
{
// ログインユーザーか確認
if (is_user_logged_in()) {
$nonce = $this->post_data['nonce_outside_postform'];
// nonceを検証
if (wp_verify_nonce($nonce, 'outside_postform')) {
// 投稿データ($my_post配列)の準備
$my_post = array(
'post_status' => 'publish', // とりあえず公開
'post_title' => $_POST['post_title'],
'post_name' => $_POST['post_name'],
'post_type' => $_POST['post_type'],
'post_content' => $_POST['post_content'],
'post_category' => $_POST['category'], // name="category[]" で送信された配列
'tags_input' => $this->insert_post_tag() // タグ配列を整形
);
// IDがあり、かつその投稿が存在する場合(=編集の場合)
if (isset($_POST['ID']) && get_post($_POST['ID'])) {
$my_post['ID'] = $_POST['ID'];
}
// WordPressデータベースに挿入(または更新)
$insert_status = wp_insert_post($my_post);
if ($insert_status) {
$mesg = '投稿完了';
} else {
$mesg = '投稿失敗';
}
} else {
$mesg = '不正な投稿';
}
// リダイレクト処理
global $post;
$this->this_post = $post; // 現在の投稿オブジェクトを保持
$url = add_query_arg(array('outside_postform_func_status' => $mesg), get_the_permalink($this->this_post->ID));
wp_redirect($url);
exit;
}
}
// 送信されたタグIDの配列を、カンマ区切りのタグ名文字列に変換
private function insert_post_tag()
{
$tag_array = array();
if (is_array($_POST['post_tag'])) {
foreach ($_POST['post_tag'] as $v) {
$tag_data = get_tag($v); // タグIDからタグ情報を取得
$tag_array[] = $tag_data->name;
}
unset($v);
}
return implode(",", $tag_array); // "タグA,タグB" の形式に
}
// アラート表示後にパラメータを除去してリダイレクト
public function run_alert()
{
// $this_post は run_insert_post() でセットされたもの
$link = get_the_permalink($this->this_post->ID);
$insert_status = urldecode($this->get_data['outside_postform_func_status']);
print <<< EOT
<script>
alert("{$insert_status}");
location.href = "{$link}";
</script>
EOT;
}
}
?>
プラグインの使用方法
上記のコードを outside-postform.php といったファイル名で保存し、WordPressの wp-content/plugins/ ディレクトリにアップロードして有効化します。
その後、投稿フォームを表示したい固定ページや投稿の編集画面(ブロックエディタまたはクラシックエディタ)で、以下のショートコードを貼り付けます。
[outside_postform]
なお、このプラグインはセキュリティのため、WordPressにログインしているユーザーにのみフォームを表示する仕様になっています。
よくある質問(FAQ)
- Q1. ログインしていない人(ゲスト)にも投稿フォームを使わせたいです。
- A1. コード内の
is_user_logged_in()によるチェックを外すことで技術的には可能ですが、セキュリティリスクが非常に高くなります。スパム投稿(ボットによる自動投稿)の格好の的になるため、Google reCAPTCHAの導入など、別途厳重なスパム対策が必須となります。 - Q2. 記事と一緒にカスタムフィールドも登録・更新したいです。
- A2.
wp_insert_post()はカスタムフィールドを直接扱えません。wp_insert_post()を実行した後、返り値として取得した投稿ID($insert_statusに入ります)を使い、update_post_meta($insert_status, 'フィールド名', $value);のように関数を追加で実行する必要があります。 - Q3. タイトルが未入力の場合、「タイトルは必須です」とエラーを出したいです。
- A3.
run_insert_post()関数内で、wp_insert_post()を実行する前に$_POST['post_title']が空かどうかをチェックします。もし空だった場合は$mesg = 'タイトルは必須です';のようにエラーメッセージを設定し、wp_insert_post()を実行せずにリダイレクト処理に進むように分岐させます。