2026年8月18日
2026年8月18日
WordPressのカレンダーウィジェット(get_calendar())をカスタマイズしてイベントカレンダーを作る方法
WordPress標準のカレンダーウィジェットは「投稿を公開した日」を示すアーカイブ用のカレンダーで、そのままでは「開催日」で並ぶイベントカレンダーにはなりません。この記事では、標準カレンダーの仕組みと調整できる範囲を確認したうえで、イベント用の投稿タイプと開催日フィールドを用意し、月表示のイベントカレンダーをショートコードで出力するところまでを実装します。
なお、タイトルにある「calendar_widget()」という名前のコア関数はありません。標準カレンダーの中身は get_calendar() 関数で、ウィジェット版は WP_Widget_Calendar クラス、ブロック版は「カレンダー」ブロック(core/calendar)がこの関数を使って描画しています。
標準カレンダーでできること・できないこと
get_calendar() は、表示中の月に公開された投稿を日付ごとに数え、投稿がある日に日別アーカイブへのリンクを付けた表を出力します。生成したHTMLはオブジェクトキャッシュに保存され、投稿の保存・削除や「週の始まり」設定の変更などで破棄されます。
調整できるのは主に次の範囲です。
- 見た目:出力される表にはクラスが付いているので、テーマのCSSで装飾できる
- HTMLの加工:
get_calendarフィルターで出力済みのHTMLを受け取り、書き換えて返せる - 週の始まり:「設定」→「一般」の「週の始まり」の値が反映される
一方、基準になるのは投稿の公開日です。バージョンによっては対象の投稿タイプを指定できる引数も用意されていますが(2026年10月現在)、それでも「開催日」のような独自の日付で並べ替えることはできません。開催日を未来日付の公開日として予約投稿する方法も考えられますが、予約投稿は公開されるまでカレンダーにもサイトにも表示されないため、告知には向きません。
そこで、イベントを扱いたい場合は「開催日を別の項目として持ち、自分でカレンダーを描く」か「イベント用プラグインを使う」かのどちらかになります。
自作するかプラグインを使うか
繰り返しイベント、チケット販売、場所の地図表示、iCal出力などが必要なら、The Events Calendar や Events Manager のような実績のあるプラグインを検討するのが現実的です。無料版と有料版で使える機能が分かれており、その内容は更新で変わるため、導入前に公式サイトで最新の機能一覧を確認してください。
単発のイベントを月表示で並べたいだけなら、以下の自作で十分です。プラグイン依存が増えず、デザインもテーマに合わせやすいのが利点です。作業前にバックアップを取り、ステージング環境で試してください。コードはテーマを変えても残るよう、mu-plugin か自作プラグインに置くのがおすすめです。
手順1:イベント用の投稿タイプを登録する
<?php
add_action( 'init', function () {
register_post_type( 'event', array(
'label' => 'イベント',
'public' => true,
'has_archive' => true,
'show_in_rest' => true, // ブロックエディターで編集できるようにする
'supports' => array( 'title', 'editor', 'thumbnail' ),
'menu_icon' => 'dashicons-calendar-alt',
) );
} );
// 表示する月を受け取るクエリ変数(例:?ev_month=2026-11)
add_filter( 'query_vars', function ( $vars ) {
$vars[] = 'ev_month';
return $vars;
} );
投稿タイプを追加した後は、「設定」→「パーマリンク」を開いて保存し、URLのルールを再生成します。これを忘れると、イベントの個別ページが404になります。
手順2:開催日の入力欄を追加する
開催日は投稿メタ _event_date に 2026-11-03 の形で保存します。この形式にしておくと、文字列の並びと日付の並びが一致し、比較や並べ替えがしやすくなります。キー名を _ で始めると、標準の「カスタムフィールド」欄に表示されず、誤って編集されにくくなります。
<?php
add_action( 'add_meta_boxes_event', function () {
add_meta_box( 'my_event_date', '開催日', 'my_event_date_box', 'event', 'side' );
} );
function my_event_date_box( $post ) {
wp_nonce_field( 'my_event_date_save', 'my_event_date_nonce' );
$value = get_post_meta( $post->ID, '_event_date', true );
printf(
'<label for="my_event_date">開催日</label><input type="date" id="my_event_date" name="my_event_date" value="%s">',
esc_attr( $value )
);
}
add_action( 'save_post_event', function ( $post_id ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
$nonce = isset( $_POST['my_event_date_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['my_event_date_nonce'] ) ) : '';
if ( ! wp_verify_nonce( $nonce, 'my_event_date_save' ) || ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
$date = isset( $_POST['my_event_date'] ) ? sanitize_text_field( wp_unslash( $_POST['my_event_date'] ) ) : '';
$dt = DateTime::createFromFormat( 'Y-m-d', $date );
if ( $dt && $dt->format( 'Y-m-d' ) === $date ) {
update_post_meta( $post_id, '_event_date', $date );
} elseif ( '' === $date ) {
delete_post_meta( $post_id, '_event_date' );
}
} );
保存処理では、nonceの検証、自動保存の除外、edit_post 権限の確認を行ってから値を扱います。日付は DateTime::createFromFormat() で読み込み、書式を戻して一致するかを比べることで、2026-02-31 のような存在しない日付を弾いています。
手順3:月表示のカレンダーをショートコードで出力する
固定ページなどに [event_calendar] と書くと、その月のイベントを開催日のマスに並べて表示します。
<?php
add_shortcode( 'event_calendar', 'my_event_calendar' );
function my_event_calendar() {
global $wp_locale;
$tz = wp_timezone();
$now = new DateTimeImmutable( wp_date( 'Y-m-01' ), $tz );
$min = $now->modify( '-24 months' );
$max = $now->modify( '+24 months' );
$ym = sanitize_text_field( (string) get_query_var( 'ev_month' ) );
$first = DateTimeImmutable::createFromFormat( '!Y-m', $ym, $tz );
if ( ! $first || $first->format( 'Y-m' ) !== $ym || $first < $min || $first > $max ) {
$first = $now;
}
$last = $first->modify( 'last day of this month' );
$q = new WP_Query( array(
'post_type' => 'event',
'post_status' => 'publish',
'posts_per_page' => 100,
'no_found_rows' => true,
'meta_key' => '_event_date',
'orderby' => 'meta_value',
'order' => 'ASC',
'meta_query' => array(
array(
'key' => '_event_date',
'value' => array( $first->format( 'Y-m-d' ), $last->format( 'Y-m-d' ) ),
'compare' => 'BETWEEN',
'type' => 'DATE',
),
),
) );
$by_day = array();
foreach ( $q->posts as $p ) {
$day = (int) substr( get_post_meta( $p->ID, '_event_date', true ), 8, 2 );
$by_day[ $day ][] = $p;
}
$sow = (int) get_option( 'start_of_week' );
$offset = ( (int) $first->format( 'w' ) - $sow + 7 ) % 7;
$days = (int) $first->format( 't' );
$prev = $first->modify( '-1 month' );
$next = $first->modify( '+1 month' );
$out = '<div class="ev-cal"><table class="ev-cal__table">';
$out .= '<caption>' . esc_html( wp_date( 'Y年n月', $first->getTimestamp() ) ) . '</caption><thead><tr>';
for ( $i = 0; $i < 7; $i++ ) {
$name = $wp_locale->get_weekday( ( $sow + $i ) % 7 );
$out .= '<th scope="col">' . esc_html( $wp_locale->get_weekday_abbrev( $name ) ) . '</th>';
}
$out .= '</tr></thead><tbody><tr>' . str_repeat( '<td class="ev-cal__pad"></td>', $offset );
for ( $d = 1; $d <= $days; $d++ ) {
if ( $d > 1 && 0 === ( $offset + $d - 1 ) % 7 ) {
$out .= '</tr><tr>';
}
$out .= '<td><span class="ev-cal__day">' . $d . '</span>';
if ( ! empty( $by_day[ $d ] ) ) {
$out .= '<ul class="ev-cal__events">';
foreach ( $by_day[ $d ] as $p ) {
$out .= sprintf( '<li><a href="%s">%s</a></li>', esc_url( get_permalink( $p ) ), esc_html( get_the_title( $p ) ) );
}
$out .= '</ul>';
}
$out .= '</td>';
}
$out .= str_repeat( '<td class="ev-cal__pad"></td>', ( 7 - ( $offset + $days ) % 7 ) % 7 );
$out .= '</tr></tbody></table><p class="ev-cal__nav">';
if ( $prev >= $min ) {
$out .= '<a href="' . esc_url( add_query_arg( 'ev_month', $prev->format( 'Y-m' ) ) ) . '">前の月</a> ';
}
if ( $next <= $max ) {
$out .= '<a href="' . esc_url( add_query_arg( 'ev_month', $next->format( 'Y-m' ) ) ) . '">次の月</a>';
}
return $out . '</p></div>';
}
設計上のポイントをまとめます。
- **ショートコードは
echoせずreturnする**:echoすると本文の先頭など意図しない位置に表示されます。 - 月の値は外部入力として検証する:
?ev_month=の値は書式を確認し、前後24か月の範囲外なら今月に戻しています。範囲を決めずに「次の月」リンクを無限に出すと、検索エンジンのクローラーが終わりのない月のページをたどり続ける原因になります。 - 日付はサイトのタイムゾーンで扱う:
wp_timezone()とwp_date()を使うと、「設定」→「一般」のタイムゾーンに沿った月の区切りになります。PHPのdate()はサーバーの設定に左右されるため避けています。 - 曜日名と週の始まりはWordPressの設定から取る:
$wp_localeとstart_of_weekを使うと、サイトの言語設定と「週の始まり」に合わせた見出しになります。 - 出力はすべてエスケープする:タイトルは
esc_html()、URLはesc_url()を通しています。
手順4:見た目を整える
表のマスが狭いスマートフォンでは、イベント名が詰まって読みにくくなります。CSSは wp_enqueue_style() で読み込み、狭い画面ではマス内の文字を小さくするか、別途リスト形式に切り替えるのが現実的です。
.ev-cal__table { width: 100%; table-layout: fixed; border-collapse: collapse; }
.ev-cal__table th, .ev-cal__table td { border: 1px solid #ddd; vertical-align: top; padding: 4px; }
.ev-cal__events { margin: 4px 0 0; padding: 0; list-style: none; font-size: 0.85em; }
@media (max-width: 600px) {
.ev-cal__events { font-size: 0.75em; }
}
動作確認とパフォーマンス
- 開催日を入れたイベントを今月・来月に数件作り、正しい日のマスに出るか確認する
- 月末の31日や、週の始まりを月曜にした場合のマスのずれがないか確認する
- 「前の月」「次の月」で範囲の端まで移動し、リンクが止まるか確認する
meta_query は投稿メタのテーブルを検索するため、イベントが数千件規模になると遅くなることがあります。その場合は、fields で必要な情報だけを取る、結果をトランジェントで短時間キャッシュする(イベント保存時に削除する)などの対策を検討します。ページキャッシュを使っている場合は、ev_month 付きのURLが別ページとしてキャッシュされるかどうかも確認してください。
まとめ
- 「calendar_widget()」という関数はなく、標準カレンダーの実体は
get_calendar()で、基準は投稿の公開日 - 見た目や出力HTMLの調整なら、CSSと
get_calendarフィルターで対応できる - 開催日で並べるなら、イベント用の投稿タイプと日付メタを用意し、カレンダーを自分で描く
- 入力はnonce・権限確認・日付検証、表示は範囲チェックとエスケープを行う
- 繰り返しイベントやチケット販売が必要なら、実績のあるイベントプラグインを検討する