WordPress自作テーマとプラグイン入門

Docker

はじめに

本記事では、自作テーマやプラグインの開発を通じて、実務でも通用するWordPressスキルを身につけることを目指します。
特に、投稿を自在に扱う Loop や、WordPressの処理に割り込んで機能を追加できる フック の仕組みを理解し、サイトを自由に拡張できる力を養うことができます。

WordPressの基本は下記記事で解説しています。

Dockerで学ぶWordPress入門 テーマとプラグイン基礎
DockerでWordPressとMariaDBを構築し、テーマやプラグイン、子テーマや自作プラグインの基礎を学べる初心者向け解説記事です。

WordPress Loop の活用

Loopとは何か

Loop(ループ)とは、WordPressが投稿記事を順番に取り出して画面に表示する仕組みのことです。
ブログ記事一覧や固定ページの本文など、投稿タイプのコンテンツは基本的にこのLoopを通じて表示されます。

基本構文の例

以下のコードは、投稿が存在する場合に1件ずつ取り出してタイトルと本文を表示するシンプルなLoopです。

<?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>
    <h2><?php the_title(); ?></h2>
    <?php the_content(); ?>
<?php endwhile; endif; ?>
  • have_posts():まだ投稿があるかどうかを確認
  • the_post():1件分の投稿データを取り出す
  • the_title():投稿タイトルを出力
  • the_content():投稿本文を出力

条件分岐やカスタムクエリ

Loopは条件分岐やカスタムクエリと組み合わせることで柔軟に使えます。

カテゴリごとに出し分ける例

<?php if ( in_category('news') ) : ?>
    <p>この記事はニュースカテゴリです。</p>
<?php endif; ?>

WP_Query を使って独自の投稿一覧を取得する例

<?php
$args = [
    'post_type'      => 'post',
    'posts_per_page' => 5,
    'category_name'  => 'news'
];
$query = new WP_Query($args);

if ( $query->have_posts() ) :
    while ( $query->have_posts() ) : $query->the_post(); ?>
        <h2><?php the_title(); ?></h2>
        <?php the_excerpt(); ?>
    <?php endwhile;
endif;

wp_reset_postdata();
?>

Loopを理解すると、「どの投稿を、どんな条件で、どのように表示するか」を自分で制御できるようになり、テーマ開発の幅が一気に広がります。

フック(アクション・フィルター)

フックとは

フック(Hook) とは、WordPressの処理の特定のタイミングに割り込んで、独自の機能を追加・変更できる仕組みです。
プラグインやテーマの多くは、このフックを活用してWordPressを拡張しています。

アクションフックとフィルターフックの違い

  • アクションフック(add_action)
    • WordPressの処理の流れの中で「ここで何かを実行したい」ときに使います。
    • 例:ヘッダーやフッターにメッセージを追加、記事保存時に処理を実行 など。
  • フィルターフック(add_filter)
    • WordPressが生成したデータを「出力前に加工・変更」するときに使います。
    • 例:記事本文に広告を差し込む、タイトルの文字列を整形する など。

使用例

アクションフックの例(フッターにメッセージ追加)

function my_footer_message() {
    echo '<p style="text-align:center;">このサイトはWordPressで作成されています。</p>';
}
add_action('wp_footer', 'my_footer_message');

フィルターフックの例(本文の末尾に文字列を追加)

function my_content_filter($content) {
    if (is_single()) {
        $content .= '<p>最後までお読みいただきありがとうございます。</p>';
    }
    return $content;
}
add_filter('the_content', 'my_content_filter');

アクション=処理を追加するもの、フィルター=データを加工するもの と覚えると理解しやすいです。

ウィジェットとショートコード

ウィジェットの作り方

ウィジェットは、サイドバーやフッターなど、テーマが用意しているウィジェットエリアに機能を追加できる仕組みです。
管理画面の「外観 → ウィジェット」から配置でき、例えば検索フォームや最近の投稿一覧などをドラッグ&ドロップで設置できます。
開発者が独自のウィジェットを作ることも可能で、WP_Widget クラスを継承して新しいウィジェットを登録します。

以下は、サイドバーに「Hello Widget!」というテキストを表示する簡単なウィジェットの例です。

<?php
// 1) ウィジェットクラスを定義
class My_Hello_Widget extends WP_Widget {
    // コンストラクタ
    function __construct() {
        parent::__construct(
            'my_hello_widget', // ウィジェットID
            'Hello Widget',    // 管理画面に表示される名前
            array('description' => 'シンプルな挨拶を表示するウィジェット')
        );
    }

    // フロント側に表示される内容
    public function widget($args, $instance) {
        echo $args['before_widget'];
        echo $args['before_title'] . '挨拶' . $args['after_title'];
        echo '<p>Hello Widget!</p>';
        echo $args['after_widget'];
    }

    // 管理画面での設定フォーム(今回はシンプルなので省略)
    public function form($instance) {
        echo '<p>設定はありません。</p>';
    }
}

// 2) ウィジェットを登録
function my_register_widgets() {
    register_widget('My_Hello_Widget');
}
add_action('widgets_init', 'my_register_widgets');

ショートコード

ショートコードは、記事や固定ページの本文に簡単なタグを埋め込むことで、特定の処理を呼び出す仕組みです。

以下は「Hello World!」と表示するシンプルな例です。

add_shortcode('hello', function() {
    return "Hello World!";
});

記事本文に次のように書くだけで動作します

[hello]

使いどころ

  • よく使うカスタムHTMLやパーツを簡単に呼び出せる
  • 問い合わせフォームやボタンなど、複雑なレイアウトを再利用できる
  • コードを書けないユーザーでも、記事に機能を挿入できる

WP_Query でのデータ取得

WP_Query クラスの概要

WordPress では、記事データの取得に WP_Query クラス を利用できます。
通常の Loop は「現在のページに応じた投稿一覧」を表示しますが、WP_Query を使えば 条件を自由に指定して投稿を取得 できます。

基本的な使用例(投稿5件を取得)

以下の例では、投稿タイプが「post」の記事を最新から5件取得します。

<?php
$query = new WP_Query([
    'post_type'      => 'post',
    'posts_per_page' => 5,
]);

if ( $query->have_posts() ) :
    while ( $query->have_posts() ) : $query->the_post(); ?>
        <h2><?php the_title(); ?></h2>
        <?php the_excerpt(); ?>
    <?php endwhile;
    wp_reset_postdata();
endif;
?>

ポイント

  • have_posts()the_post() を組み合わせて Loop と同じように使う
  • wp_reset_postdata() を最後に書くことで、グローバル変数 $post を元に戻す

カスタム投稿タイプの取得

例えば、商品情報を管理する product というカスタム投稿タイプを登録している場合は、次のように取得できます。

<?php
$product_query = new WP_Query([
    'post_type'      => 'product',
    'posts_per_page' => 10,
    'orderby'        => 'date',
    'order'          => 'DESC',
]);

このように、投稿タイプやカテゴリー、タグ、メタ情報など多彩な条件でデータを取得できるのが WP_Query の強みです。

自作テーマをゼロから構築

フォルダを作成

wp-content/themes/my-minimal-theme/

style.css を作成(必須)

wp-content/themes/my-minimal-theme/style.css

/*
 Theme Name: My Minimal Theme
 Theme URI: https://example.com/
 Author: Your Name
 Description: 最小構成の学習用クラシックテーマ
 Version: 1.0.0
*/

ポイントstyle.css のヘッダーコメントでテーマが認識されます。

index.php を作成

wp-content/themes/my-minimal-theme/index.php

<?php
/** 最小構成のテーマ:記事一覧を表示 */
?><!doctype html>
<html <?php language_attributes(); ?>>
<head>
  <meta charset="<?php bloginfo('charset'); ?>">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title><?php bloginfo('name'); ?></title>
  <?php wp_head(); ?>
  <style>
    body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; padding: 24px; line-height: 1.8; }
    header, footer { text-align: center; opacity: .85; }
    .post { margin: 2.2rem 0; border-bottom: 1px solid #eee; padding-bottom: 1.6rem; }
    .post h2 { margin: 0 0 .6rem; font-size: 1.4rem; }
    .post .meta { font-size: .9rem; color: #666; margin-bottom: .6rem; }
    nav.pager a { margin-right: .8rem; }
  </style>
</head>
<body <?php body_class(); ?>>

<header>
  <h1><a href="<?php echo esc_url(home_url('/')); ?>" style="text-decoration:none;"><?php bloginfo('name'); ?></a></h1>
  <p><?php bloginfo('description'); ?></p>
</header>

<main>
<?php if ( have_posts() ) : ?>
  <?php while ( have_posts() ) : the_post(); ?>
    <article <?php post_class('post'); ?>>
      <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
      <div class="meta">
        <time datetime="<?php echo esc_attr(get_the_date('c')); ?>"><?php echo esc_html(get_the_date()); ?></time>
        <?php the_category(', '); ?>
      </div>
      <div class="excerpt">
        <?php the_excerpt(); ?>
      </div>
    </article>
  <?php endwhile; ?>

  <nav class="pager">
    <span><?php previous_posts_link('← 新しい投稿'); ?></span>
    <span><?php next_posts_link('古い投稿 →'); ?></span>
  </nav>

<?php else : ?>
  <p>投稿がありません。</p>
<?php endif; ?>
</main>

<footer>
  <p>© <?php echo date('Y'); ?> <?php bloginfo('name'); ?></p>
</footer>

<?php wp_footer(); ?>
</body>
</html>

ポイント

  • have_posts()the_post()Loop
  • the_title(), the_excerpt(), the_permalink() で一覧を組み立て。
  • wp_head() / wp_footer() はプラグインやエディタ互換に重要なので必ず入れる。

有効化

  1. 管理画面 → 外観 → テーマ
  2. My Minimal Theme」を 有効化
  3. トップページで 記事一覧 が表示されれば成功です。

簡単なプラグインを作る

プラグインの目的

  • ショートコード [simple_contact] を記事・固定ページに挿入するとフォームを表示します。
  • 送信時に CSRF対策(nonce)サニタイズ を行い、
    • wp_mail() で管理者宛にメール送信
    • 専用テーブルに保存
      を実行します。

ファイル配置

wp-content/plugins/simple-contact/
└─ simple-contact.php

simple-contact.php

<?php
/*
Plugin Name: Simple Contact
Description: ショートコードで表示する簡易お問い合わせフォーム(メール送信 + 任意でDB保存)
Version: 1.0.0
Author: Your Name
License: GPL-2.0-or-later
Text Domain: simple-contact
*/

if ( ! defined('ABSPATH') ) exit;

/** ---------------------------
 *  設定(必要に応じて編集)
 *  --------------------------- */
define('SC_EMAIL_TO', get_option('admin_email')); // 送信先(デフォルト: 管理者メール)
define('SC_SAVE_TO_DB', true);                    // DB保存する場合は true

/** ---------------------------
 *  有効化時: テーブル作成(DB保存を使う場合)
 *  --------------------------- */
function sc_activate() {
    if ( ! SC_SAVE_TO_DB ) return;

    global $wpdb;
    $table = $wpdb->prefix . 'simple_contact';
    $charset = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE IF NOT EXISTS {$table} (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        name VARCHAR(191) NOT NULL,
        email VARCHAR(191) NOT NULL,
        message TEXT NOT NULL,
        created_at DATETIME NOT NULL,
        PRIMARY KEY (id),
        KEY email (email)
    ) {$charset};";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta($sql);
}
register_activation_hook(__FILE__, 'sc_activate');

/** ---------------------------
 *  ショートコード: [simple_contact]
 *  --------------------------- */
function sc_render_form() {
    // 送信処理
    $notice = '';
    if ( $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['sc_form_submit']) ) {
        // CSRF
        if ( ! isset($_POST['_wpnonce']) || ! wp_verify_nonce($_POST['_wpnonce'], 'sc_form') ) {
            $notice = '<div class="sc-error">不正なリクエストです。</div>';
        } else {
            // サニタイズ
            $name    = sanitize_text_field( $_POST['sc_name'] ?? '' );
            $email   = sanitize_email( $_POST['sc_email'] ?? '' );
            $message = wp_kses_post( $_POST['sc_message'] ?? '' );

            // バリデーション
            $errors = [];
            if ( $name === '' )  $errors[] = 'お名前は必須です。';
            if ( ! is_email($email) ) $errors[] = 'メールアドレスの形式が正しくありません。';
            if ( trim(wp_strip_all_tags($message)) === '' ) $errors[] = 'お問い合わせ内容は必須です。';

            if ( empty($errors) ) {
                // ① メール送信
                $subject = '【お問い合わせ】' . $name;
                $body    = "お名前: {$name}\nメール: {$email}\n\n内容:\n{$message}\n";
                $headers = [ 'Content-Type: text/plain; charset=UTF-8', 'Reply-To: ' . $email ];
                $sent    = wp_mail( SC_EMAIL_TO, $subject, $body, $headers );

                // ② DB保存(任意)
                if ( SC_SAVE_TO_DB ) {
                    global $wpdb;
                    $wpdb->insert(
                        $wpdb->prefix . 'simple_contact',
                        [
                            'name'       => $name,
                            'email'      => $email,
                            'message'    => $message,
                            'created_at' => current_time('mysql'),
                        ],
                        [ '%s', '%s', '%s', '%s' ]
                    );
                }

                if ( $sent ) {
                    $notice = '<div class="sc-success">送信が完了しました。ありがとうございました。</div>';
                    // フォーム値をクリア
                    $_POST = [];
                } else {
                    $notice = '<div class="sc-error">送信に失敗しました。時間をおいて再度お試しください。</div>';
                }
            } else {
                $notice = '<div class="sc-error">' . esc_html( implode(' ', $errors) ) . '</div>';
            }
        }
    }

    // 入力値(再表示用)
    $v_name    = isset($_POST['sc_name'])    ? esc_attr($_POST['sc_name']) : '';
    $v_email   = isset($_POST['sc_email'])   ? esc_attr($_POST['sc_email']) : '';
    $v_message = isset($_POST['sc_message']) ? esc_textarea($_POST['sc_message']) : '';

    // フォームHTML
    ob_start(); ?>
    <div class="sc-wrapper">
        <?php echo $notice; ?>
        <form method="post" class="sc-form">
            <?php wp_nonce_field('sc_form'); ?>
            <p>
                <label for="sc_name">お名前<span style="color:#e11;">*</span></label><br>
                <input type="text" id="sc_name" name="sc_name" value="<?php echo $v_name; ?>" required>
            </p>
            <p>
                <label for="sc_email">メールアドレス<span style="color:#e11;">*</span></label><br>
                <input type="email" id="sc_email" name="sc_email" value="<?php echo $v_email; ?>" required>
            </p>
            <p>
                <label for="sc_message">お問い合わせ内容<span style="color:#e11;">*</span></label><br>
                <textarea id="sc_message" name="sc_message" rows="6" required><?php echo $v_message; ?></textarea>
            </p>
            <p>
                <button type="submit" name="sc_form_submit" class="sc-button">送信する</button>
            </p>
        </form>
    </div>
    <style>
        .sc-form input[type="text"],
        .sc-form input[type="email"],
        .sc-form textarea { width: 100%; max-width: 680px; padding: .6rem .8rem; }
        .sc-button { padding: .6rem 1.2rem; border-radius: .4rem; cursor: pointer; }
        .sc-success, .sc-error { margin: .8rem 0; padding: .8rem; border-radius: .4rem; }
        .sc-success { background: #ecfdf5; border: 1px solid #10b981; }
        .sc-error   { background: #fef2f2; border: 1px solid #ef4444; }
    </style>
    <?php
    return ob_get_clean();
}
add_shortcode('simple_contact', 'sc_render_form');

使い方

  • プラグインを有効化します。
  • 固定ページを作成し、本文に [simple_contact] と記載します。
  • 送信すると
    • 管理者メール(設定 → 一般 → メールアドレス)に通知が届きます。
    • SC_SAVE_TO_DBtrue の場合、wp_simple_contact テーブルに保存されます。

ローカル開発のメール送信について

Docker環境では実メールが届かない場合があります。
そのためMailHog(SMTPキャプチャ)とWP Mail SMTP プラグインで代用して確認します。

MailHog

docker-compose.ymlMailHogを追加します。

services:
  db:
    image: mariadb:11.3
    command: >
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_unicode_ci
    environment:
      MARIADB_DATABASE: wordpress
      MARIADB_USER: wordpress
      MARIADB_PASSWORD: wordpress
      MARIADB_ROOT_PASSWORD: root
    volumes:
      - ./db_data:/var/lib/mysql

  wordpress:
    image: wordpress:php8.2-apache
    depends_on:
      - db
      - mailhog 
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - ./wordpress_data:/var/www/html
  mailhog:
    image: mailhog/mailhog:v1.0.1
    ports:
      - "8025:8025"
      - "1025:1025"

起動します

docker compose up -d

Web UI はここです

WP Mail SMTP

WP Mail SMTP プラグインをインストールして有効化します。

設定 → WP Mail SMTP で以下を指定します

  • Mailer: Other SMTP(任意のSMTP)
  • SMTP Host: mailhog
  • SMTP Port: 1025
  • Encryption: None(暗号化なし)
  • Authentication: Off(認証なし)
  • From Email/Name は任意(例: noreply@example.local

画面下部の Email Test でテスト送信
http://localhost:8025 を開くと、届いたメールが一覧表示されます。
(正しく設定されていないようですと出てきますがスルーでOKです)

セキュリティの要点

  • CSRF対策wp_nonce_field() / wp_verify_nonce() を実装しています。
  • サニタイズsanitize_text_field() / sanitize_email() / wp_kses_post() を実装しています。
  • エスケープ:再表示時に esc_attr() / esc_textarea() を使用しています。

まとめ

本記事では下記を記載しました。

  • Loop/フック/WP_Query/ウィジェット/ショートコードを実装し、表示制御と機能拡張の基礎を記載
  • 最小テーマ作成と問い合わせプラグイン開発を行い、MailHogでメール動作も確認できる方法を記載。

本記事が開発の手助けになれば幸いです。

コメント

タイトルとURLをコピーしました