star back image
people4
電飾 電飾
moon
astronaut

【WordPress】イベントカレンダー

BLOG WEBログWordPressプラグイン
読了約:185分

お店や会社のホームページに、営業日やイベントをカレンダーに表示させて「楽しいことをやってるんだぞ!」と訴求したい。そんな人向けです。

登録したイベントのアイコンをクリックすると、説明とリンクボタンがポップアップするようにしたいです。

以下のようなよくあるやつです。

できるだけシンプルな仕様で軽量、尚且つ導入しやすいWordPressのプラグインにしようと思いました。

サンプルページ
https://neo.astrowave.jp/event-calendar/?cal_month=2026-08

この記事の内容は以下のような感じの流れです。

記事にした理由

むかし要望があったけどプログラムをどうやって作ってよいのかわからず。そのとき諦めた挫折感をずっと持っていたからです。

思い出した要件は以下です。

  • WordPressのCMS管理画面で行いたい
  • カレンダーは1つだけトップページに出したい
  • カレンダーのマークを押したらイベント記事へのリンクがある
  • イベントとはキャンペーンとか臨時休業日のお知らせ、求人など何でも使う
  • 3色ほどイベント種別分けしたい

そんな要望だったと思います。

AIとの相談とプロンプト

geminiでもChatGPTでもどちらでも良いです。
こんな感じで作りたいと、カレンダーWebアプリを見てもらいながら出来たプロンプトは以下です。

何回もやりとりしましたが、長いので1つに合体してもらいました。

プロンプト
WordPressの子テーマに、ACF等の外部プラグイン無しで「開館日・イベントカレンダー」を実装してください。
コードコメントは日本語。jQuery等の外部JSライブラリは使わない(バニラJS)。

【ゴール】
固定ページにショートコードを置くだけで月カレンダーが出る。
管理画面でイベントを登録できる。スマホでも見やすい。

【データ】
- CPT: event_calendar(管理画面のみ。個別ページ・アーカイブ不要)
- meta:
  - event_date(YYYY-MM-DD)
  - event_label(normal / holiday / special。表示名は通常・休館・特別)
  - event_message(案内文・任意)
  - event_link(http/httpsのみ・任意)
- 種別の色分けはCSSクラスで行う(配色UIは不要)

【ファイル】
- inc/event-calendar.php(CPT・metabox・描画・Ajax・設定)
- assets/css/event-calendar.css
- assets/js/event-calendar.js
- functions.php から require
パスは get_stylesheet_directory() / get_stylesheet_directory_uri()

【フロント】
- ショートコード: astrowave_event_calendar
- 日曜始まりの月グリッド(CSS Grid)
- ?cal_month=YYYY-MM で表示月(未指定は今月)
- 前月/翌月は admin-ajax でHTML差し替え(ページ全体リロードなし・トップへ飛ばない)
- 月送り時は軽いフェードorスライド。翌月は左方向、前月は右方向(時間の流れに合わせる)
- PC: イベントクリック/ホバーで詳細ポップアップ(日付・種別・タイトル・message・link。link空なら非表示)
- スマホ(max-width:768px): セル内は色ドットのみ。日付タップで下部ボトムシートに詳細。再タップで閉じる
- .entry-content ul / h2 のテーマ装飾と衝突しない(タイトルはh2にしない、リスト余白は上書き)

【設定(CPT配下の簡易画面でOK)】
- カレンダー下部の補足文(開館時間など)。ショートコードと同じ出力に含める。月送りでも消えない
- 定休日の曜日チェック(セルに「定休日」表示)
- 日本の祝日: holidays-jp のJSONを取得しtransientキャッシュ。祝日セルはピンク下地+赤文字+祝日名
- 追加祝日: 1行「YYYY-MM-DD 名称」で手動追加可

【品質】
- エスケープ・nonce・sanitize を忘れない
- 過剰な管理画面・配色ピッカーは作らない
- まず動く最小実装→必要なら見た目を整える

AIさんがまとめた「方針+ファイル配置+必須仕様」です。

相談ばかりで、あなた何もしてませんね。

そうなんです(( ;`ω´)ゴクリ

使い方

プラグインをダウンロードして有効にします。

イベントカレンダープラグイン

Astrowave Simple Event Calendar
https://wordpress.org/plugins/astrowave-simple-event-calendar/

管理画面のメニューに「カレンダー」が追加されます。
「イベントを追加」でイベント名などを入れて「公開」しましょう。

次は表示させたい場所にショートコードを入れましょう。

固定ページ

ウィジェット

固定ページや記事のページ、ウィジェット。WordPressのphpにショートコードで好きなところに設置できる想定です。

WordPress テーマ内で好きな場所にphpを記述

<?php echo do_shortcode( '[ astrowave_event_calendar ]' ); ?>

phpのファイルに記述する場合、プレーンなHTMLコメントやテキストとして書くのは不可です。<?php ... ?> が必要です。

例:if付き

<?php if ( shortcode_exists( 'astrowave_event_calendar' ) ) : ?>
<div class="blog-event-calendar">
	<?php echo do_shortcode( '[ astrowave_event_calendar ]' ); ?>
</div>
<?php endif; ?>

[…]のショートコード部分、実際に使うときはスペースなし。
[ astrowave_event_calendar ] → [astrowave_eve..
にしてください。

ショートコード貼り付けただけで、出力処理されますw

カレンダー補足

必要最低限を心がけています。

出力ファイル

作成してもらったプロンプトをCursorで実行すると、以下のファイルが作成されました。

astrowave-simple-event-calendar/
├── astrowave-simple-event-calendar.php ← 本体(CPT・メタ・Ajax・設定・ショートコード)
├── readme.txt ← WordPress.org 用説明
├── css/
│ └── event-calendar.css ← カレンダー/補足/祝日/ボトムシート
└── js/
└── event-calendar.js ← 月送りAjax・ポップアップ・スマホUI

ソースコードは以下です。

コードは長いので、興味がある人向けです。

astrowave-simple-event-calendar.php

<?php
/**
 * Plugin Name: Astrowave Simple Event Calendar
 * Plugin URI: https://wordpress.org/plugins/astrowave-simple-event-calendar/
 * Description: Simple facility event calendar. Closed weekdays, optional holidays (JP: holidays-jp / others: Nager, off by default), shortcode.
 * Version: 1.1.2
 * Author: Saigamo
 * Author URI: https://astrowave.jp/
 * License: GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: astrowave-simple-event-calendar
 *
 * CPT / option キーはテーマ実装時と同じ(移行後も既存データを使う)
 * - CPT: event_calendar
 * - meta: event_date / event_label / event_message / event_link
 * - ショートコード: [ astrowave_event_calendar ]
 */

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

/**
 * 同じファイルの二重読み込み(ZIPの入れ子など)では何もしない
 */
if ( defined( 'ASTROWAVE_SIMPLE_EVENT_CALENDAR_LOADED' ) ) {
	return;
}
define( 'ASTROWAVE_SIMPLE_EVENT_CALENDAR_LOADED', true );

/**
 * 子テーマの旧ファイルが残っていると、あとから require されて致命エラーになる
 * 関数の有無では判定しない(プラグイン自身の再読み込みと区別できないため)
 */
$astrowave_event_calendar_theme_file = trailingslashit( get_stylesheet_directory() ) . 'inc/event-calendar.php';
if ( is_readable( $astrowave_event_calendar_theme_file ) ) {
	add_action(
		'admin_notices',
		static function () {
			if ( ! current_user_can( 'activate_plugins' ) ) {
				return;
			}
			echo '<div class="notice notice-error"><p><strong>Astrowave Simple Event Calendar:</strong> 子テーマに <code>inc/event-calendar.php</code> が残っています。プラグインと二重になるため読み込んでいません。FTPで子テーマからこのファイルを削除し、<code>functions.php</code> の require も外してください。</p></div>';
		}
	);
	return;
}

define( 'ASTROWAVE_SIMPLE_EVENT_CALENDAR_VERSION', '1.1.2' );
define( 'ASTROWAVE_SIMPLE_EVENT_CALENDAR_DIR', plugin_dir_path( __FILE__ ) );
define( 'ASTROWAVE_SIMPLE_EVENT_CALENDAR_URL', plugin_dir_url( __FILE__ ) );

/**
 * イベント種別の初期表示名(値=CSSクラス用スラッグ)
 */
function astrowave_event_calendar_label_defaults() {
	return array(
		'normal'  => '通常',
		'holiday' => '休館',
		'special' => '特別',
	);
}

/**
 * 種別表示名の option キー
 */
function astrowave_event_calendar_labels_option_key() {
	return 'astrowave_event_calendar_labels';
}

/**
 * イベント種別(表示名は管理画面で変更可。色はCSSのまま)
 */
function astrowave_event_calendar_labels() {
	$defaults = astrowave_event_calendar_label_defaults();
	$saved    = get_option( astrowave_event_calendar_labels_option_key(), false );
	if ( false === $saved || ! is_array( $saved ) ) {
		return $defaults;
	}

	$labels = array();
	foreach ( $defaults as $slug => $default_text ) {
		$labels[ $slug ] = array_key_exists( $slug, $saved )
			? sanitize_text_field( (string) $saved[ $slug ] )
			: $default_text;
	}
	return $labels;
}

/**
 * カスタム投稿タイプ登録(管理画面専用。個別ページは出さない)
 */
function astrowave_register_event_calendar() {
	register_post_type(
		'event_calendar',
		array(
			'labels'              => array(
				'name'          => 'イベントカレンダー',
				'singular_name' => 'イベント',
				'menu_name'     => 'カレンダー',
				'add_new'       => '新規追加',
				'add_new_item'  => 'イベントを追加',
				'edit_item'     => 'イベントを編集',
				'all_items'     => 'イベント一覧',
			),
			'public'              => false,
			'show_ui'             => true,
			'show_in_menu'        => true,
			'show_in_rest'        => false,
			'exclude_from_search' => true,
			'publicly_queryable'  => false,
			'has_archive'         => false,
			'menu_position'       => 6,
			'menu_icon'           => 'dashicons-calendar-alt',
			'supports'            => array( 'title' ),
		)
	);
}
add_action( 'init', 'astrowave_register_event_calendar' );

/**
 * メタボックス
 */
function astrowave_event_calendar_add_meta_box() {
	add_meta_box(
		'astrowave_event_calendar_meta',
		'開催情報',
		'astrowave_event_calendar_meta_box_html',
		'event_calendar',
		'side',
		'high'
	);
	add_meta_box(
		'astrowave_event_calendar_detail',
		'案内メッセージ・リンク',
		'astrowave_event_calendar_detail_meta_box_html',
		'event_calendar',
		'normal',
		'high'
	);
}
add_action( 'add_meta_boxes', 'astrowave_event_calendar_add_meta_box' );

/**
 * メタボックスHTML
 */
function astrowave_event_calendar_meta_box_html( $post ) {
	wp_nonce_field( 'astrowave_event_calendar_save', 'astrowave_event_calendar_nonce' );

	$date   = get_post_meta( $post->ID, 'event_date', true );
	$label  = get_post_meta( $post->ID, 'event_label', true );
	$labels = astrowave_event_calendar_labels();
	if ( ! $label ) {
		$label = 'normal';
	}
	?>
	<p>
		<label for="event_date"><strong>開催日</strong></label><br>
		<input type="date" id="event_date" name="event_date" value="<?php echo esc_attr( $date ); ?>" style="width:100%;">
	</p>
	<p>
		<label for="event_label"><strong>イベント種別</strong></label><br>
		<select id="event_label" name="event_label" style="width:100%;">
			<?php
			$defaults = astrowave_event_calendar_label_defaults();
			foreach ( $defaults as $value => $default_text ) :
				$text         = isset( $labels[ $value ] ) ? $labels[ $value ] : '';
				$option_label = '' !== $text ? $text : $default_text . '(種別名なし)';
				?>
				<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $label, $value ); ?>>
					<?php echo esc_html( $option_label ); ?>
				</option>
			<?php endforeach; ?>
		</select>
	</p>
	<p class="description">表示名は「カレンダー補足」で変更できます。空欄でもカレンダー上の色アイコンは出ます。</p>
	<?php
}

/**
 * 案内メッセージ・リンクのメタボックス
 */
function astrowave_event_calendar_detail_meta_box_html( $post ) {
	$message = get_post_meta( $post->ID, 'event_message', true );
	$link    = get_post_meta( $post->ID, 'event_link', true );
	?>
	<p>
		<label for="event_message"><strong>開催メッセージ</strong></label><br>
		<textarea id="event_message" name="event_message" rows="5" style="width:100%;"><?php echo esc_textarea( $message ); ?></textarea>
	</p>
	<p>
		<label for="event_link"><strong>詳しくはこちらのリンク</strong></label><br>
		<input type="url" id="event_link" name="event_link" value="<?php echo esc_attr( $link ); ?>" placeholder="https://" style="width:100%;">
	</p>
	<?php
}

/**
 * 外部リンクとして使える http/https のみ残す
 */
function astrowave_event_calendar_sanitize_link( $url ) {
	$url = esc_url_raw( trim( (string) $url ) );
	if ( ! $url ) {
		return '';
	}
	$scheme = wp_parse_url( $url, PHP_URL_SCHEME );
	if ( $scheme && ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
		return '';
	}
	return $url;
}

/**
 * メタ保存
 */
function astrowave_event_calendar_save_meta( $post_id ) {
	if ( ! isset( $_POST['astrowave_event_calendar_nonce'] ) ) {
		return;
	}
	if ( ! wp_verify_nonce( $_POST['astrowave_event_calendar_nonce'], 'astrowave_event_calendar_save' ) ) {
		return;
	}
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
		return;
	}
	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		return;
	}

	$date = isset( $_POST['event_date'] ) ? sanitize_text_field( wp_unslash( $_POST['event_date'] ) ) : '';
	if ( $date && ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
		$date = '';
	}
	update_post_meta( $post_id, 'event_date', $date );

	$labels = astrowave_event_calendar_labels();
	$label  = isset( $_POST['event_label'] ) ? sanitize_key( wp_unslash( $_POST['event_label'] ) ) : 'normal';
	if ( ! isset( $labels[ $label ] ) ) {
		$label = 'normal';
	}
	update_post_meta( $post_id, 'event_label', $label );

	$message = isset( $_POST['event_message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['event_message'] ) ) : '';
	update_post_meta( $post_id, 'event_message', $message );

	$link = isset( $_POST['event_link'] ) ? astrowave_event_calendar_sanitize_link( wp_unslash( $_POST['event_link'] ) ) : '';
	update_post_meta( $post_id, 'event_link', $link );
}
add_action( 'save_post_event_calendar', 'astrowave_event_calendar_save_meta' );

/**
 * 管理画面一覧に開催日・種別を表示
 */
function astrowave_event_calendar_columns( $columns ) {
	$new = array();
	foreach ( $columns as $key => $value ) {
		$new[ $key ] = $value;
		if ( 'title' === $key ) {
			$new['event_date']  = '開催日';
			$new['event_label'] = '種別';
		}
	}
	return $new;
}
add_filter( 'manage_event_calendar_posts_columns', 'astrowave_event_calendar_columns' );

function astrowave_event_calendar_column_content( $column, $post_id ) {
	if ( 'event_date' === $column ) {
		$date = get_post_meta( $post_id, 'event_date', true );
		echo $date ? esc_html( $date ) : '—';
	}
	if ( 'event_label' === $column ) {
		$labels = astrowave_event_calendar_labels();
		$label  = get_post_meta( $post_id, 'event_label', true );
		echo isset( $labels[ $label ] ) && '' !== $labels[ $label ]
			? esc_html( $labels[ $label ] )
			: '(種別名なし)';
	}
}
add_action( 'manage_event_calendar_posts_custom_column', 'astrowave_event_calendar_column_content', 10, 2 );

function astrowave_event_calendar_sortable_columns( $columns ) {
	$columns['event_date'] = 'event_date';
	return $columns;
}
add_filter( 'manage_edit-event_calendar_sortable_columns', 'astrowave_event_calendar_sortable_columns' );

function astrowave_event_calendar_orderby( $query ) {
	if ( ! is_admin() || ! $query->is_main_query() ) {
		return;
	}
	if ( 'event_date' === $query->get( 'orderby' ) ) {
		$query->set( 'meta_key', 'event_date' );
		$query->set( 'orderby', 'meta_value' );
	}
}
add_action( 'pre_get_posts', 'astrowave_event_calendar_orderby' );

/**
 * 定休日の曜日 option キー(0=日 … 6=土)
 */
function astrowave_event_calendar_closed_weekdays_option_key() {
	return 'astrowave_event_calendar_closed_weekdays';
}

/**
 * 定休日の曜日(0=日 … 6=土)
 *
 * @return int[]
 */
function astrowave_event_calendar_closed_weekdays() {
	$saved = get_option( astrowave_event_calendar_closed_weekdays_option_key(), array() );
	if ( ! is_array( $saved ) ) {
		return array();
	}
	$out = array();
	foreach ( $saved as $day ) {
		$day = (int) $day;
		if ( $day >= 0 && $day <= 6 ) {
			$out[] = $day;
		}
	}
	return array_values( array_unique( $out ) );
}

/**
 * POST から定休日曜日をサニタイズ
 *
 * @param mixed $posted チェックされた曜日
 * @return int[]
 */
function astrowave_event_calendar_sanitize_closed_weekdays( $posted ) {
	if ( ! is_array( $posted ) ) {
		return array();
	}
	$out = array();
	foreach ( $posted as $day ) {
		$day = (int) $day;
		if ( $day >= 0 && $day <= 6 ) {
			$out[] = $day;
		}
	}
	return array_values( array_unique( $out ) );
}

/**
 * カレンダー下部の補足テキスト(option)
 */
function astrowave_event_calendar_notes_option_key() {
	return 'astrowave_event_calendar_notes';
}

/**
 * 保存済み補足を取得(未保存・空欄は非表示)
 */
function astrowave_event_calendar_get_notes() {
	return (string) get_option( astrowave_event_calendar_notes_option_key(), '' );
}

/**
 * 補足設定メニュー(イベントカレンダー配下)
 */
function astrowave_event_calendar_notes_menu() {
	add_submenu_page(
		'edit.php?post_type=event_calendar',
		'カレンダー補足',
		'カレンダー補足',
		'edit_posts',
		'astrowave-event-calendar-notes',
		'astrowave_event_calendar_notes_page'
	);
}
add_action( 'admin_menu', 'astrowave_event_calendar_notes_menu' );

/**
 * 補足設定画面
 */
function astrowave_event_calendar_notes_page() {
	if ( ! current_user_can( 'edit_posts' ) ) {
		return;
	}

	$key     = astrowave_event_calendar_notes_option_key();
	$extra_k = astrowave_event_calendar_extra_holidays_option_key();

	if ( isset( $_POST['astrowave_event_calendar_notes_nonce'] )
		&& wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['astrowave_event_calendar_notes_nonce'] ) ), 'astrowave_event_calendar_notes_save' )
	) {
		$raw = isset( $_POST['astrowave_event_calendar_notes'] )
			? wp_unslash( $_POST['astrowave_event_calendar_notes'] )
			: '';
		$clean = wp_kses_post( $raw );
		update_option( $key, $clean, false );

		$extra_raw = isset( $_POST['astrowave_event_calendar_extra_holidays'] )
			? sanitize_textarea_field( wp_unslash( $_POST['astrowave_event_calendar_extra_holidays'] ) )
			: '';
		update_option( $extra_k, $extra_raw, false );

		$label_defaults = astrowave_event_calendar_label_defaults();
		$posted_labels  = isset( $_POST['astrowave_event_calendar_labels'] ) && is_array( $_POST['astrowave_event_calendar_labels'] )
			? wp_unslash( $_POST['astrowave_event_calendar_labels'] )
			: array();
		$clean_labels = array();
		foreach ( $label_defaults as $slug => $default_text ) {
			$text = isset( $posted_labels[ $slug ] ) ? sanitize_text_field( (string) $posted_labels[ $slug ] ) : '';
			$clean_labels[ $slug ] = $text;
		}
		update_option( astrowave_event_calendar_labels_option_key(), $clean_labels, false );

		$closed = isset( $_POST['astrowave_event_calendar_closed_weekdays'] )
			? astrowave_event_calendar_sanitize_closed_weekdays( wp_unslash( $_POST['astrowave_event_calendar_closed_weekdays'] ) )
			: array();
		update_option( astrowave_event_calendar_closed_weekdays_option_key(), $closed, false );

		$use_api = ! empty( $_POST['astrowave_event_calendar_use_nager_holidays'] );
		update_option( astrowave_event_calendar_use_nager_holidays_option_key(), $use_api ? 1 : 0, false );

		$country = isset( $_POST['astrowave_event_calendar_holiday_country'] )
			? astrowave_event_calendar_sanitize_country_code( wp_unslash( $_POST['astrowave_event_calendar_holiday_country'] ) )
			: 'JP';
		update_option( astrowave_event_calendar_holiday_country_option_key(), $country, false );

		// 祝日キャッシュをクリア(追加祝日・ON/OFF・国切替の反映用)
		astrowave_event_calendar_clear_holiday_transients();

		echo '<div class="notice notice-success is-dismissible"><p>カレンダー補足・定休日・祝日設定・種別表示名を保存しました。</p></div>';
	}

	$notes        = astrowave_event_calendar_get_notes();
	$extras       = (string) get_option( $extra_k, '' );
	$labels       = astrowave_event_calendar_labels();
	$closed_wdays = astrowave_event_calendar_closed_weekdays();
	$use_nager    = astrowave_event_calendar_use_nager_holidays();
	$country      = astrowave_event_calendar_holiday_country();
	$countries    = astrowave_event_calendar_nager_countries();
	$wdays        = array( '日', '月', '火', '水', '木', '金', '土' );
	$label_help   = array(
		'normal'  => '青・.event-calendar__item--normal',
		'holiday' => '灰・.event-calendar__item--holiday',
		'special' => '橙・.event-calendar__item--special',
	);
	?>
	<div class="wrap">
		<h1>カレンダー補足</h1>
		<p>フロントのカレンダー直下に表示する注釈です。<code>[ astrowave_event_calendar ]</code> と同じショートコードで一緒に出力されます。空欄にすると非表示になります。</p>
		<form method="post" action="">
			<?php wp_nonce_field( 'astrowave_event_calendar_notes_save', 'astrowave_event_calendar_notes_nonce' ); ?>
			<?php
			wp_editor(
				$notes,
				'astrowave_event_calendar_notes',
				array(
					'textarea_name' => 'astrowave_event_calendar_notes',
					'textarea_rows' => 8,
					'media_buttons' => false,
					'tinymce'       => array(
						'toolbar1' => 'bold,italic,bullist,numlist,link,unlink,undo,redo',
						'toolbar2' => '',
					),
					'quicktags'     => true,
				)
			);
			?>
			<p class="description">例: 通常開館時間・祝日の時間・★の説明など。月送りしても消えません。</p>

			<h2>定休日の曜日</h2>
			<p>チェックした曜日は「定休日」として下地表示されます。その日にイベントを登録すると、定休日の表示に加えてアイコンも出ます。</p>
			<fieldset>
				<legend class="screen-reader-text">定休日の曜日</legend>
				<?php foreach ( $wdays as $i => $wday ) : ?>
					<label style="display:inline-block;margin:0 12px 8px 0;">
						<input
							type="checkbox"
							name="astrowave_event_calendar_closed_weekdays[]"
							value="<?php echo esc_attr( (string) $i ); ?>"
							<?php checked( in_array( $i, $closed_wdays, true ) ); ?>
						>
						<?php echo esc_html( $wday ); ?>曜日
					</label>
				<?php endforeach; ?>
			</fieldset>

			<h2>祝日の自動表示(任意)</h2>
			<p><strong>デフォルトはOFF(通信なし)</strong>です。ONにすると、選んだ国の祝日を取得してキャッシュします。</p>
			<ul style="list-style:disc;margin-left:1.5em;">
				<li><strong>Japan (JP)</strong> … <a href="https://holidays-jp.github.io/" target="_blank" rel="noopener noreferrer">holidays-jp</a>(振替休日・国民の休日など日本向け)</li>
				<li><strong>その他の国</strong> … <a href="https://date.nager.at/" target="_blank" rel="noopener noreferrer">Nager.Date</a></li>
			</ul>
			<p>失敗してもカレンダー自体は動きます。手入力の祝日は常に有効です。APIのURLはプラグイン固定です(任意URLの入力はできません)。</p>
			<p>
				<label>
					<input
						type="checkbox"
						name="astrowave_event_calendar_use_nager_holidays"
						value="1"
						<?php checked( $use_nager ); ?>
					>
					祝日を自動表示する
				</label>
			</p>
			<p>
				<label for="astrowave_event_calendar_holiday_country"><strong>国</strong></label><br>
				<select id="astrowave_event_calendar_holiday_country" name="astrowave_event_calendar_holiday_country">
					<?php foreach ( $countries as $code => $label ) : ?>
						<option value="<?php echo esc_attr( $code ); ?>" <?php selected( $country, $code ); ?>>
							<?php echo esc_html( $label . ' (' . $code . ')' ); ?>
						</option>
					<?php endforeach; ?>
				</select>
			</p>
			<p class="description">一覧に無い国・足りない日は下の手入力を使ってください。</p>

			<h2>追加の祝日・休業日(任意)</h2>
			<p>どの国でも使えます。APIを使わない場合の本命です。1行に1件、<code>YYYY-MM-DD 名称</code>。API ON時に同じ日付があれば、こちらが優先(上書き)します。</p>
			<textarea name="astrowave_event_calendar_extra_holidays" rows="6" class="large-text code" placeholder="2026-08-11 Mountain Day / 山の日(振替)"><?php echo esc_textarea( $extras ); ?></textarea>

			<h2>イベント種別の表示名</h2>
			<p>バッジの色はCSS変数で上書きできます。表示名を空欄にすると、ポップアップの種別名だけ出さず、カレンダー上の色アイコンは出します。</p>
			<p class="description">外観 → カスタマイズ → 追加CSS の例:</p>
			<pre class="code" style="background:#fff;padding:12px;border:1px solid #c3c4c7;max-width:40rem;overflow:auto;">:root {
  --event-calendar-normal: #0b6e99;
  --event-calendar-holiday: #4a4a4a;
  --event-calendar-special: #c43c00;
  --event-calendar-closed-bg: #e8f4fc;
}</pre>
			<div class="astrowave-event-calendar-labels" style="max-width:40rem;margin:1em 0 0;">
				<?php foreach ( astrowave_event_calendar_label_defaults() as $slug => $default_text ) : ?>
					<p style="margin:0 0 1.25em;">
						<label for="event-label-<?php echo esc_attr( $slug ); ?>" style="display:block;font-weight:600;">
							<?php echo esc_html( $default_text ); ?>
						</label>
						<?php if ( isset( $label_help[ $slug ] ) ) : ?>
							<span class="description" style="display:block;margin:4px 0 8px;white-space:nowrap;">(<?php echo esc_html( $label_help[ $slug ] ); ?>)</span>
						<?php endif; ?>
						<input
							type="text"
							class="regular-text"
							id="event-label-<?php echo esc_attr( $slug ); ?>"
							name="astrowave_event_calendar_labels[<?php echo esc_attr( $slug ); ?>]"
							value="<?php echo esc_attr( $labels[ $slug ] ); ?>"
							placeholder="<?php echo esc_attr( $default_text ); ?>"
						>
					</p>
				<?php endforeach; ?>
			</div>

			<?php submit_button( '保存' ); ?>
		</form>
	</div>
	<?php
}

/**
 * 追加祝日 option キー
 */
function astrowave_event_calendar_extra_holidays_option_key() {
	return 'astrowave_event_calendar_extra_holidays';
}

/**
 * Nager.Date 祝日APIを使うか(デフォルトOFF)
 */
function astrowave_event_calendar_use_nager_holidays_option_key() {
	return 'astrowave_event_calendar_use_nager_holidays';
}

/**
 * 祝日APIの国コード option キー
 */
function astrowave_event_calendar_holiday_country_option_key() {
	return 'astrowave_event_calendar_holiday_country';
}

/**
 * Nager.Date で選べる国(プラグイン固定のホワイトリスト)
 * キー = ISO 3166-1 alpha-2
 *
 * @return array<string,string>
 */
function astrowave_event_calendar_nager_countries() {
	return array(
		'JP' => 'Japan / 日本',
		'US' => 'United States',
		'GB' => 'United Kingdom',
		'CA' => 'Canada',
		'AU' => 'Australia',
		'NZ' => 'New Zealand',
		'DE' => 'Germany',
		'FR' => 'France',
		'IT' => 'Italy',
		'ES' => 'Spain',
		'PT' => 'Portugal',
		'NL' => 'Netherlands',
		'BE' => 'Belgium',
		'CH' => 'Switzerland',
		'AT' => 'Austria',
		'SE' => 'Sweden',
		'NO' => 'Norway',
		'DK' => 'Denmark',
		'FI' => 'Finland',
		'IE' => 'Ireland',
		'PL' => 'Poland',
		'CZ' => 'Czechia',
		'KR' => 'South Korea',
		'TW' => 'Taiwan',
		'HK' => 'Hong Kong',
		'SG' => 'Singapore',
		'MY' => 'Malaysia',
		'TH' => 'Thailand',
		'IN' => 'India',
		'BR' => 'Brazil',
		'MX' => 'Mexico',
		'AR' => 'Argentina',
		'ZA' => 'South Africa',
	);
}

/**
 * 国コードをサニタイズ(ホワイトリスト外は JP)
 */
function astrowave_event_calendar_sanitize_country_code( $code ) {
	$code = strtoupper( sanitize_key( (string) $code ) );
	$allowed = astrowave_event_calendar_nager_countries();
	if ( isset( $allowed[ $code ] ) ) {
		return $code;
	}
	return 'JP';
}

/**
 * 祝日API(Nager)を使うか
 * 旧オプション use_jp_holidays が ON なら移行扱い
 */
function astrowave_event_calendar_use_nager_holidays() {
	$saved = get_option( astrowave_event_calendar_use_nager_holidays_option_key(), null );
	if ( null !== $saved ) {
		return (bool) $saved;
	}
	// 1.0.1 以前の「日本の祝日ON」からの引き継ぎ
	return (bool) get_option( 'astrowave_event_calendar_use_jp_holidays', 0 );
}

/**
 * 祝日APIの国(デフォルト JP)
 */
function astrowave_event_calendar_holiday_country() {
	$saved = get_option( astrowave_event_calendar_holiday_country_option_key(), '' );
	if ( $saved ) {
		return astrowave_event_calendar_sanitize_country_code( $saved );
	}
	return 'JP';
}

/**
 * 祝日 transient を消す
 */
function astrowave_event_calendar_clear_holiday_transients() {
	global $wpdb;
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	$wpdb->query(
		"DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_astrowave_jp_holidays_%'
			OR option_name LIKE '_transient_timeout_astrowave_jp_holidays_%'
			OR option_name LIKE '_transient_astrowave_nager_holidays_%'
			OR option_name LIKE '_transient_timeout_astrowave_nager_holidays_%'"
	);
}

/**
 * 手動追加祝日をパース(ymd => 名称)
 */
function astrowave_event_calendar_parse_extra_holidays() {
	$raw   = (string) get_option( astrowave_event_calendar_extra_holidays_option_key(), '' );
	$lines = preg_split( '/\r\n|\r|\n/', $raw );
	$out   = array();
	foreach ( $lines as $line ) {
		$line = trim( $line );
		if ( '' === $line || 0 === strpos( $line, '#' ) ) {
			continue;
		}
		if ( preg_match( '/^(\d{4}-\d{2}-\d{2})\s+(.+)$/u', $line, $m ) ) {
			$out[ $m[1] ] = trim( $m[2] );
		} elseif ( preg_match( '/^(\d{4}-\d{2}-\d{2})$/', $line, $m ) ) {
			$out[ $m[1] ] = 'Holiday';
		}
	}
	return $out;
}

/**
 * 指定年の祝日マス用データ(ymd => 名称)
 * - オプションONのときだけ外部取得(URLは提供元ごとに固定)
 *   - 国 = JP … holidays-jp(振替・国民の休日など日本向け)
 *   - その他 … Nager.Date
 * - 手動追加は常にマージ(同日は手入力優先)
 *
 * @param int $year 西暦年
 * @return array<string,string>
 */
function astrowave_event_calendar_holidays_for_year( $year ) {
	$year = (int) $year;
	if ( $year < 1970 || $year > 2100 ) {
		return array();
	}

	$base = array();

	if ( astrowave_event_calendar_use_nager_holidays() ) {
		$country = astrowave_event_calendar_holiday_country();

		if ( 'JP' === $country ) {
			$base = astrowave_event_calendar_fetch_holidays_jp( $year );
		} else {
			$base = astrowave_event_calendar_fetch_holidays_nager( $year, $country );
		}
	}

	// 手動追加を上書きマージ(国を問わず常に有効・APIより優先)
	foreach ( astrowave_event_calendar_parse_extra_holidays() as $ymd => $name ) {
		if ( 0 === strpos( $ymd, (string) $year . '-' ) ) {
			$base[ $ymd ] = $name;
		}
	}

	return $base;
}

/**
 * holidays-jp から日本の祝日を取得(キャッシュ付き)
 *
 * @param int $year 西暦年
 * @return array<string,string>
 */
function astrowave_event_calendar_fetch_holidays_jp( $year ) {
	$transient = 'astrowave_jp_holidays_' . (int) $year;
	$cached    = get_transient( $transient );
	if ( is_array( $cached ) ) {
		return $cached;
	}

	$base     = array();
	$url      = sprintf( 'https://holidays-jp.github.io/api/v1/%d/date.json', (int) $year );
	$response = wp_remote_get(
		$url,
		array(
			'timeout' => 8,
			'headers' => array( 'Accept' => 'application/json' ),
		)
	);

	if ( ! is_wp_error( $response ) && 200 === (int) wp_remote_retrieve_response_code( $response ) ) {
		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( is_array( $body ) ) {
			foreach ( $body as $ymd => $name ) {
				if ( is_string( $ymd ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $ymd ) ) {
					$base[ $ymd ] = is_string( $name ) ? $name : '祝日';
				}
			}
		}
	}

	set_transient( $transient, $base, $base ? WEEK_IN_SECONDS : DAY_IN_SECONDS );
	return $base;
}

/**
 * Nager.Date から指定国の祝日を取得(キャッシュ付き)
 *
 * @param int    $year    西暦年
 * @param string $country ISO国コード(ホワイトリスト済み想定)
 * @return array<string,string>
 */
function astrowave_event_calendar_fetch_holidays_nager( $year, $country ) {
	$country   = astrowave_event_calendar_sanitize_country_code( $country );
	$transient = 'astrowave_nager_holidays_' . $country . '_' . (int) $year;
	$cached    = get_transient( $transient );
	if ( is_array( $cached ) ) {
		return $cached;
	}

	$base     = array();
	$url      = sprintf( 'https://date.nager.at/api/v3/PublicHolidays/%d/%s', (int) $year, rawurlencode( $country ) );
	$response = wp_remote_get(
		$url,
		array(
			'timeout' => 8,
			'headers' => array( 'Accept' => 'application/json' ),
		)
	);

	if ( ! is_wp_error( $response ) && 200 === (int) wp_remote_retrieve_response_code( $response ) ) {
		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( is_array( $body ) ) {
			foreach ( $body as $row ) {
				if ( ! is_array( $row ) || empty( $row['date'] ) ) {
					continue;
				}
				$ymd = (string) $row['date'];
				if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $ymd ) ) {
					continue;
				}
				$name = '';
				if ( ! empty( $row['localName'] ) && is_string( $row['localName'] ) ) {
					$name = $row['localName'];
				} elseif ( ! empty( $row['name'] ) && is_string( $row['name'] ) ) {
					$name = $row['name'];
				} else {
					$name = 'Holiday';
				}
				$base[ $ymd ] = $name;
			}
		}
	}

	set_transient( $transient, $base, $base ? WEEK_IN_SECONDS : DAY_IN_SECONDS );
	return $base;
}

/**
 * 後方互換エイリアス
 *
 * @param int $year 西暦年
 * @return array<string,string>
 */
function astrowave_event_calendar_jp_holidays_for_year( $year ) {
	return astrowave_event_calendar_holidays_for_year( $year );
}

/**
 * 指定月の祝日だけ(ymd => 名称)
 */
function astrowave_event_calendar_holidays_for_month( $year, $month ) {
	$all  = astrowave_event_calendar_holidays_for_year( $year );
	$pref = sprintf( '%04d-%02d-', $year, $month );
	$out  = array();
	foreach ( $all as $ymd => $name ) {
		if ( 0 === strpos( $ymd, $pref ) ) {
			$out[ $ymd ] = $name;
		}
	}
	return $out;
}

/**
 * 補足HTML(空なら空文字)
 */
function astrowave_event_calendar_notes_markup() {
	$notes = trim( astrowave_event_calendar_get_notes() );
	if ( '' === $notes ) {
		return '';
	}

	// プレーンテキストのみのときは改行を段落にする
	if ( false === strpos( $notes, '<' ) ) {
		$notes = wpautop( esc_html( $notes ) );
	} else {
		$notes = wp_kses_post( $notes );
	}

	return '<aside class="event-calendar__notes" aria-label="カレンダーの補足">' . $notes . '</aside>';
}

/**
 * YYYY-MM を検証して [年, 月] を返す。不正なら null
 */
function astrowave_event_calendar_parse_month( $raw ) {
	if ( ! is_string( $raw ) || ! preg_match( '/^(\d{4})-(\d{2})$/', $raw, $m ) ) {
		return null;
	}
	$year  = (int) $m[1];
	$month = (int) $m[2];
	if ( $month < 1 || $month > 12 || ! checkdate( $month, 1, $year ) ) {
		return null;
	}
	return array( $year, $month );
}

/**
 * 表示対象の年月(?cal_month=YYYY-MM、不正値は今月)
 */
function astrowave_event_calendar_target_month() {
	$raw    = isset( $_GET['cal_month'] ) ? sanitize_text_field( wp_unslash( $_GET['cal_month'] ) ) : '';
	$parsed = astrowave_event_calendar_parse_month( $raw );
	if ( $parsed ) {
		return $parsed;
	}

	return array( (int) wp_date( 'Y' ), (int) wp_date( 'n' ) );
}

/**
 * 指定月のイベントを日付キーでまとめる
 */
function astrowave_event_calendar_events_for_month( $year, $month ) {
	$start = sprintf( '%04d-%02d-01', $year, $month );
	$end   = sprintf( '%04d-%02d-%02d', $year, $month, (int) wp_date( 't', strtotime( $start ) ) );

	$query = new WP_Query(
		array(
			'post_type'      => 'event_calendar',
			'post_status'    => 'publish',
			'posts_per_page' => -1,
			'meta_key'       => 'event_date',
			'orderby'        => 'meta_value',
			'order'          => 'ASC',
			'meta_query'     => array(
				array(
					'key'     => 'event_date',
					'value'   => array( $start, $end ),
					'compare' => 'BETWEEN',
					'type'    => 'DATE',
				),
			),
			'no_found_rows'  => true,
		)
	);

	$grouped = array();
	foreach ( $query->posts as $post ) {
		$date = get_post_meta( $post->ID, 'event_date', true );
		if ( ! $date ) {
			continue;
		}
		$grouped[ $date ][] = array(
			'title'   => get_the_title( $post ),
			'label'   => get_post_meta( $post->ID, 'event_label', true ) ?: 'normal',
			'message' => (string) get_post_meta( $post->ID, 'event_message', true ),
			'link'    => astrowave_event_calendar_sanitize_link( get_post_meta( $post->ID, 'event_link', true ) ),
		);
	}

	return $grouped;
}

/**
 * CSS / JS(ショートコード利用時のみ)
 */
function astrowave_event_calendar_enqueue() {
	$css = ASTROWAVE_SIMPLE_EVENT_CALENDAR_DIR . 'css/event-calendar.css';
	$js  = ASTROWAVE_SIMPLE_EVENT_CALENDAR_DIR . 'js/event-calendar.js';

	wp_enqueue_style(
		'astrowave-event-calendar',
		ASTROWAVE_SIMPLE_EVENT_CALENDAR_URL . 'css/event-calendar.css',
		array(),
		file_exists( $css ) ? (string) filemtime( $css ) : ASTROWAVE_SIMPLE_EVENT_CALENDAR_VERSION
	);

	wp_enqueue_script(
		'astrowave-event-calendar',
		ASTROWAVE_SIMPLE_EVENT_CALENDAR_URL . 'js/event-calendar.js',
		array(),
		file_exists( $js ) ? (string) filemtime( $js ) : ASTROWAVE_SIMPLE_EVENT_CALENDAR_VERSION,
		true
	);
	wp_localize_script(
		'astrowave-event-calendar',
		'astrowaveEventCalendar',
		array(
			'ajaxUrl' => admin_url( 'admin-ajax.php' ),
			'nonce'   => wp_create_nonce( 'astrowave_event_calendar' ),
			'action'  => 'astrowave_event_calendar_month',
		)
	);
}

/**
 * カレンダーJSは defer(テーマ側の一括処理に頼らない)
 */
function astrowave_event_calendar_script_loader_tag( $tag, $handle ) {
	if ( 'astrowave-event-calendar' === $handle && false === strpos( $tag, ' defer' ) ) {
		return str_replace( ' src', ' defer src', $tag );
	}
	return $tag;
}
add_filter( 'script_loader_tag', 'astrowave_event_calendar_script_loader_tag', 10, 2 );

/**
 * 本文にショートコードがあるページでは先に enqueue する
 */
function astrowave_event_calendar_enqueue_if_needed() {
	if ( ! is_singular() ) {
		return;
	}
	$post = get_post();
	if ( $post && (
		has_shortcode( $post->post_content, 'astrowave_event_calendar' )
	) ) {
		astrowave_event_calendar_enqueue();
	}
}
add_action( 'wp_enqueue_scripts', 'astrowave_event_calendar_enqueue_if_needed' );

/**
 * 指定月のカレンダーHTML(.event-calendar 本体)
 */
function astrowave_event_calendar_markup( $year, $month ) {
	$events       = astrowave_event_calendar_events_for_month( $year, $month );
	$holidays     = astrowave_event_calendar_holidays_for_month( $year, $month );
	$labels       = astrowave_event_calendar_labels();
	$closed_wdays = astrowave_event_calendar_closed_weekdays();

	$first_ts   = strtotime( sprintf( '%04d-%02d-01', $year, $month ) );
	$days_in    = (int) wp_date( 't', $first_ts );
	// 日曜始まり(0=日 … 6=土)
	$start_wday = (int) wp_date( 'w', $first_ts );

	$prev = strtotime( '-1 month', $first_ts );
	$next = strtotime( '+1 month', $first_ts );
	$prev_ym = wp_date( 'Y-m', $prev );
	$next_ym = wp_date( 'Y-m', $next );

	$today = wp_date( 'Y-m-d' );
	$wdays = array( '日', '月', '火', '水', '木', '金', '土' );

	ob_start();
	?>
	<div class="event-calendar" data-month="<?php echo esc_attr( sprintf( '%04d-%02d', $year, $month ) ); ?>">
		<div class="event-calendar__nav">
			<a class="event-calendar__nav-link" href="<?php echo esc_url( add_query_arg( 'cal_month', $prev_ym ) ); ?>" data-month="<?php echo esc_attr( $prev_ym ); ?>">前月</a>
			<p class="event-calendar__title"><?php echo esc_html( sprintf( '%d年%d月', $year, $month ) ); ?></p>
			<a class="event-calendar__nav-link" href="<?php echo esc_url( add_query_arg( 'cal_month', $next_ym ) ); ?>" data-month="<?php echo esc_attr( $next_ym ); ?>">翌月</a>
		</div>

		<div class="event-calendar__stage">
		<div class="event-calendar__grid" role="grid" aria-label="<?php echo esc_attr( sprintf( '%d年%d月のカレンダー', $year, $month ) ); ?>">
			<?php foreach ( $wdays as $i => $wday ) : ?>
				<div class="event-calendar__wday event-calendar__wday--<?php echo (int) $i; ?>" role="columnheader"><?php echo esc_html( $wday ); ?></div>
			<?php endforeach; ?>

			<?php
			// 月初までの空セル
			for ( $i = 0; $i < $start_wday; $i++ ) {
				echo '<div class="event-calendar__day event-calendar__day--empty" role="gridcell"></div>';
			}

			for ( $day = 1; $day <= $days_in; $day++ ) {
				$ymd          = sprintf( '%04d-%02d-%02d', $year, $month, $day );
				$wday         = ( $start_wday + $day - 1 ) % 7;
				$day_evts     = isset( $events[ $ymd ] ) ? $events[ $ymd ] : array();
				$holiday_name = isset( $holidays[ $ymd ] ) ? $holidays[ $ymd ] : '';
				$is_closed    = in_array( $wday, $closed_wdays, true );
				$classes      = array(
					'event-calendar__day',
					'event-calendar__day--w' . $wday,
				);
				if ( $ymd === $today ) {
					$classes[] = 'event-calendar__day--today';
				}
				if ( $is_closed ) {
					$classes[] = 'event-calendar__day--closed';
				}
				if ( $holiday_name ) {
					$classes[] = 'event-calendar__day--jp-holiday';
				}
				if ( $day_evts ) {
					$classes[] = 'event-calendar__day--has-event';
				}
				$date_label = sprintf( '%d年%d月%d日(%s)', $year, $month, $day, $wdays[ $wday ] );
				if ( $holiday_name ) {
					$date_label .= '・' . $holiday_name;
				}
				if ( $is_closed ) {
					$date_label .= '・定休日';
				}
				?>
				<div
					class="<?php echo esc_attr( implode( ' ', array_unique( $classes ) ) ); ?>"
					role="gridcell"
					data-ymd="<?php echo esc_attr( $ymd ); ?>"
					data-date="<?php echo esc_attr( $date_label ); ?>"
					<?php if ( $day_evts ) : ?>
						tabindex="0"
						aria-expanded="false"
					<?php endif; ?>
				>
					<span class="event-calendar__date"><?php echo esc_html( (string) $day ); ?></span>
					<?php if ( $holiday_name ) : ?>
						<span class="event-calendar__holiday-name"><?php echo esc_html( $holiday_name ); ?></span>
					<?php endif; ?>
					<?php if ( $is_closed ) : ?>
						<span class="event-calendar__closed-name"><?php echo esc_html( '定休日' ); ?></span>
					<?php endif; ?>
					<?php if ( $day_evts ) : ?>
						<ul class="event-calendar__events">
							<?php foreach ( $day_evts as $evt ) : ?>
								<?php
								$label_key  = $evt['label'];
								$label_txt  = isset( $labels[ $label_key ] ) ? $labels[ $label_key ] : '';
								$item_class = 'event-calendar__item event-calendar__item--' . sanitize_html_class( $label_key );
								?>
								<li
									class="<?php echo esc_attr( $item_class ); ?>"
									tabindex="0"
									role="button"
									data-title="<?php echo esc_attr( $evt['title'] ); ?>"
									data-label="<?php echo esc_attr( $label_txt ); ?>"
									data-date="<?php echo esc_attr( $date_label ); ?>"
									data-message="<?php echo esc_attr( $evt['message'] ); ?>"
									data-link="<?php echo esc_url( $evt['link'] ); ?>"
								>
									<span class="event-calendar__name"><?php echo esc_html( $evt['title'] ); ?></span>
								</li>
							<?php endforeach; ?>
						</ul>
					<?php endif; ?>
				</div>
				<?php
			}
			?>
		</div>
		</div>
	</div>
	<?php
	return ob_get_clean();
}

/**
 * Ajax: 指定月のカレンダーHTML
 */
function astrowave_event_calendar_ajax_month() {
	check_ajax_referer( 'astrowave_event_calendar', 'nonce' );

	$raw    = isset( $_POST['cal_month'] ) ? sanitize_text_field( wp_unslash( $_POST['cal_month'] ) ) : '';
	$parsed = astrowave_event_calendar_parse_month( $raw );
	if ( ! $parsed ) {
		wp_send_json_error( array( 'message' => '不正な年月です。' ), 400 );
	}

	list( $year, $month ) = $parsed;
	wp_send_json_success(
		array(
			'html'  => astrowave_event_calendar_markup( $year, $month ),
			'month' => sprintf( '%04d-%02d', $year, $month ),
		)
	);
}
add_action( 'wp_ajax_astrowave_event_calendar_month', 'astrowave_event_calendar_ajax_month' );
add_action( 'wp_ajax_nopriv_astrowave_event_calendar_month', 'astrowave_event_calendar_ajax_month' );

/**
 * ショートコード [ astrowave_event_calendar ]
 * カレンダー本体+下部の補足をまとめて出力
 */
function astrowave_event_calendar_shortcode() {
	astrowave_event_calendar_enqueue();

	list( $year, $month ) = astrowave_event_calendar_target_month();

	return '<div class="event-calendar-root">'
		. astrowave_event_calendar_markup( $year, $month )
		. astrowave_event_calendar_notes_markup()
		. '</div>';
}
add_shortcode( 'astrowave_event_calendar', 'astrowave_event_calendar_shortcode' );

前月/翌月と、月を移動するとURLが変わります。そのためAjaxをさせています。

css/event-calendar.css

/**
 * Astrowave Simple Event Calendar([ astrowave_event_calendar ])
 *
 * 色の上書き例(外観 → カスタマイズ → 追加CSS):
 * :root {
 *   --event-calendar-normal: #0b6e99;
 *   --event-calendar-holiday: #4a4a4a;
 *   --event-calendar-closed-bg: #e8f4fc;
 * }
 */

:root {
	--event-calendar-normal: #0b6e99;
	--event-calendar-normal-border: #064b6b;
	--event-calendar-holiday: #4a4a4a;
	--event-calendar-holiday-border: #222;
	--event-calendar-special: #c43c00;
	--event-calendar-special-border: #8a2a00;
	--event-calendar-on-color: #fff;
	--event-calendar-closed-bg: #e8f4fc;
	--event-calendar-closed-bg-today: #d9eefb;
	--event-calendar-closed-label: #1a1a1a;
}

.event-calendar-root {
	margin: 1.5em 0 2em;
	container-type: inline-size;
	container-name: event-calendar;
}

.event-calendar-root.is-loading {
	pointer-events: none;
}

.event-calendar-root .event-calendar__stage {
	overflow: hidden;
	transition: opacity 0.22s ease, transform 0.22s ease;
}

.event-calendar-root.is-leaving .event-calendar__stage,
.event-calendar-root.is-entering .event-calendar__stage {
	opacity: 0;
}

.event-calendar-root.is-leaving.is-to-prev .event-calendar__stage,
.event-calendar-root.is-entering.is-to-next .event-calendar__stage {
	transform: translateX(12px);
}

.event-calendar-root.is-leaving.is-to-next .event-calendar__stage,
.event-calendar-root.is-entering.is-to-prev .event-calendar__stage {
	transform: translateX(-12px);
}

.event-calendar {
	font-size: 14px;
	color: #1a1a1a;
}

/* カレンダー下部の補足(参考サイトの開館時間注釈など) */
.event-calendar__notes {
	margin-top: 14px;
	padding: 12px 14px;
	border: 1px solid #d8d8d8;
	border-radius: 4px;
	background: #fafafa;
	font-size: 0.92em;
	line-height: 1.6;
	color: #333;
}

.event-calendar__notes p {
	margin: 0 0 0.4em;
}

.event-calendar__notes p:last-child {
	margin-bottom: 0;
}

.event-calendar__notes ul {
	margin: 0;
	padding-left: 1.2em;
}

.event-calendar__notes li {
	margin: 0.2em 0;
}

.event-calendar__nav {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	margin-bottom: 12px;
}

.event-calendar__title {
	margin: 0;
	padding: 0;
	font-size: 1.25em;
	font-weight: 700 !important;
	text-align: center;
	position: static;
}

/* テーマの .entry-content h2 装飾が万一当たっても打ち消す */
.entry-content .event-calendar__title::before {
	content: none !important;
	display: none !important;
}

.event-calendar__nav-link {
	min-width: 4.5em;
	padding: 4px 10px;
	border: 1px solid #d4d4d4;
	border-radius: 4px;
	color: #1a1a1a;
	text-decoration: none;
	text-align: center;
}

.event-calendar__nav-link:hover,
.event-calendar__nav-link:focus {
	background: #1a1a1a;
	border-color: #1a1a1a;
	color: #fff;
}

.event-calendar__stage {
	min-height: 18em;
}

.event-calendar__grid {
	display: grid;
	grid-template-columns: repeat(7, minmax(0, 1fr));
	border-top: 1px solid #d4d4d4;
	border-left: 1px solid #d4d4d4;
}

.event-calendar__wday,
.event-calendar__day {
	min-height: 5.5em;
	padding: 6px;
	border-right: 1px solid #d4d4d4;
	border-bottom: 1px solid #d4d4d4;
	box-sizing: border-box;
}

.event-calendar__wday {
	min-height: 0;
	background: #f5f5f5;
	font-weight: 700;
	text-align: center;
}

.event-calendar__wday--0 {
	color: #c0392b;
}

.event-calendar__wday--6 {
	color: #1a6fb5;
}

.event-calendar__day--empty {
	background: #fafafa;
}

.event-calendar__day--today {
	background: #fff8e6;
}

/* 日本の祝日:ピンク下地+赤文字 */
.event-calendar__day--jp-holiday {
	background: #ffe4ec;
}

.event-calendar__day--jp-holiday.event-calendar__day--today {
	background: #ffe0e8;
	box-shadow: inset 0 0 0 2px #f0c0cc;
}

/* 定休日:薄いブルー下地(日付の色は曜日どおり) */
.event-calendar__day--closed {
	background: var(--event-calendar-closed-bg);
}

.event-calendar__day--closed.event-calendar__day--today {
	background: var(--event-calendar-closed-bg-today);
	box-shadow: inset 0 0 0 2px #b7d7ea;
}

/* 祝日かつ定休日のときは祝日の下地を優先 */
.event-calendar__day--jp-holiday.event-calendar__day--closed,
.event-calendar__day--jp-holiday.event-calendar__day--closed.event-calendar__day--today {
	background: #ffe4ec;
}

.event-calendar__day--w0 .event-calendar__date {
	color: #c0392b;
}

.event-calendar__day--w6 .event-calendar__date {
	color: #1a6fb5;
}

.event-calendar__day--jp-holiday .event-calendar__date {
	color: #c0392b;
}

.event-calendar__date {
	display: block;
	font-weight: 700;
	line-height: 1.2;
}

.event-calendar__holiday-name {
	display: block;
	margin-top: 2px;
	font-size: 10px;
	font-weight: 700;
	line-height: 1.25;
	color: #c0392b;
	word-break: break-word;
}

.event-calendar__closed-name {
	display: block;
	margin-top: 2px;
	font-size: 10px;
	font-weight: 700;
	line-height: 1.25;
	color: var(--event-calendar-closed-label);
	word-break: break-word;
}

.event-calendar__events,
.entry-content .event-calendar__events {
	margin: 6px 0 0;
	padding: 0;
	list-style: none;
}

.event-calendar__item {
	margin: 0 0 4px;
	padding: 3px 5px;
	border: 1px solid transparent;
	border-radius: 3px;
	line-height: 1.3;
	word-break: break-word;
	cursor: pointer;
}

.event-calendar__item.is-open {
	box-shadow: 0 0 0 2px #1a1a1a;
}

.event-calendar__name {
	font-size: 11px;
	font-weight: 700;
}

.event-calendar__item--normal {
	background: var(--event-calendar-normal);
	border-color: var(--event-calendar-normal-border);
	color: var(--event-calendar-on-color);
}

.event-calendar__item--holiday {
	background: var(--event-calendar-holiday);
	border-color: var(--event-calendar-holiday-border);
	color: var(--event-calendar-on-color);
}

.event-calendar__item--special {
	background: var(--event-calendar-special);
	border-color: var(--event-calendar-special-border);
	color: var(--event-calendar-on-color);
}

/* カレンダー自身の幅(サイドバーなど)。ブラウザ幅ではない */
@container event-calendar (max-width: 480px) {
	.event-calendar__nav-link {
		min-width: 0;
		padding: 4px 6px;
		font-size: 12px;
	}

	.event-calendar__title {
		font-size: 1em;
		margin: 0 !important;
	}

	.event-calendar__stage {
		min-height: 0;
	}

	.event-calendar__wday,
	.event-calendar__day {
		min-height: 0;
		padding: 8px 2px 10px;
		text-align: center;
	}

	.event-calendar__date {
		font-size: 13px;
	}

	.event-calendar__day--has-event {
		cursor: pointer;
	}

	.event-calendar__day--has-event.is-selected {
		background: #eef6fc;
		box-shadow: inset 0 0 0 2px #1a1a1a;
	}

	.event-calendar__day--jp-holiday.event-calendar__day--has-event.is-selected {
		background: #ffd6e2;
	}

	.event-calendar__day--closed.event-calendar__day--has-event.is-selected {
		background: var(--event-calendar-closed-bg-today);
	}

	.event-calendar__holiday-name,
	.event-calendar__closed-name {
		font-size: 9px;
		margin-top: 1px;
	}

	.event-calendar .event-calendar__events {
		display: flex;
		justify-content: center;
		flex-wrap: wrap;
		gap: 3px;
		margin: 5px 0 0;
		padding: 0;
		list-style: none;
		pointer-events: none;
		min-height: 9px;
	}

	.event-calendar .event-calendar__item {
		display: block;
		width: 9px;
		height: 9px;
		margin: 0;
		padding: 0;
		overflow: hidden;
		border-radius: 50%;
		font-size: 0;
		line-height: 0;
		outline: none;
		box-shadow: 0 0 0 1px #fff;
	}

	.event-calendar__item.is-open {
		box-shadow: 0 0 0 2px #1a1a1a;
	}

	.event-calendar__name {
		display: none;
	}
}

.event-calendar-overlay {
	position: fixed;
	inset: 0;
	z-index: 1000;
	pointer-events: none;
}

.event-calendar-overlay.is-visible.is-sheet {
	pointer-events: auto;
}

.event-calendar-overlay__backdrop {
	display: none;
	position: absolute;
	inset: 0;
	background: rgba(0, 0, 0, 0.4);
}

.event-calendar-overlay.is-sheet .event-calendar-overlay__backdrop {
	display: block;
}

.event-calendar-overlay__card {
	position: fixed;
	z-index: 1001;
	pointer-events: auto;
	box-sizing: border-box;
	max-width: min(360px, calc(100vw - 16px));
	max-height: min(70vh, 480px);
	overflow: auto;
	padding: 16px 40px 16px 16px;
	border: 1px solid #c8c8c8;
	border-radius: 10px;
	background: #fff;
	box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
	color: #1a1a1a;
	opacity: 0;
	transform: translateY(8px);
	transition: opacity 0.2s ease, transform 0.2s ease;
}

.event-calendar-overlay.is-visible .event-calendar-overlay__card {
	opacity: 1;
	transform: translateY(0);
}

.event-calendar-overlay__handle {
	display: none;
}

.event-calendar-overlay__close {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	position: absolute;
	top: 8px;
	right: 8px;
	width: 36px;
	height: 36px;
	margin: 0;
	padding: 0;
	border: 0;
	background: transparent;
	box-sizing: border-box;
	font-size: 22px;
	line-height: 1;
	cursor: pointer;
	color: #444;
}

.event-calendar-overlay__date {
	margin: 0 0 10px;
	font-size: 13px;
	font-weight: 700;
	color: #555;
}

.event-calendar-overlay__event + .event-calendar-overlay__event {
	margin-top: 12px;
	padding-top: 12px;
	border-top: 1px solid #e5e5e5;
}

.event-calendar-overlay__meta {
	display: inline-block;
	margin: 0 0 6px;
	padding: 2px 8px;
	border-radius: 999px;
	font-size: 11px;
	font-weight: 700;
	color: var(--event-calendar-on-color);
	background: var(--event-calendar-normal);
}

.event-calendar-overlay__event--holiday .event-calendar-overlay__meta {
	background: var(--event-calendar-holiday);
}

.event-calendar-overlay__event--special .event-calendar-overlay__meta {
	background: var(--event-calendar-special);
}

.event-calendar-overlay__heading {
	margin: 0;
	font-size: 16px;
	font-weight: 700;
	line-height: 1.4;
}

.event-calendar-overlay__message {
	margin: 8px 0 0;
	font-size: 14px;
	line-height: 1.7;
	white-space: pre-wrap;
	word-break: break-word;
}

.event-calendar-overlay__link {
	display: inline-flex;
	align-items: center;
	margin-top: 12px;
	padding: 10px 18px;
	border-radius: 999px;
	background: #1a1a1a;
	color: #fff !important;
	font-size: 13px;
	font-weight: 700;
	line-height: 1.0;
	text-decoration: none;
}

.event-calendar-overlay__link:hover,
.event-calendar-overlay__link:focus {
	background: var(--event-calendar-normal);
	color: var(--event-calendar-on-color) !important;
}

.event-calendar-overlay.is-sheet .event-calendar-overlay__card {
	top: auto;
	left: 0;
	right: 0;
	bottom: 0;
	max-width: none;
	max-height: min(78vh, 560px);
	padding: 10px 16px 24px;
	border: 0;
	border-radius: 16px 16px 0 0;
	transform: translateY(100%);
}

.event-calendar-overlay.is-sheet.is-visible .event-calendar-overlay__card {
	transform: translateY(0);
}

.event-calendar-overlay.is-sheet .event-calendar-overlay__handle {
	display: block;
	width: 40px;
	height: 4px;
	margin: 2px auto 12px;
	border-radius: 999px;
	background: #c8c8c8;
}

.event-calendar-overlay.is-sheet .event-calendar-overlay__close {
	width: 44px;
	height: 44px;
}

body.event-calendar-sheet-open {
	overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
	.event-calendar-root .event-calendar__stage,
	.event-calendar-overlay__card,
	.event-calendar-overlay__backdrop {
		transition: none;
	}
}

デザインはcssで好きに変えてください。

js/event-calendar.js

/**
 * Astrowave Simple Event Calendar — 月送りAjax / 詳細ポップアップ / ボトムシート
 */
(() => {
	'use strict';

	const config = window.astrowaveEventCalendar || {};
	const FADE_MS = 220;
	const COMPACT_MAX = 480;
	const compactQuery = window.matchMedia('(max-width: 480px)');
	const hoverQuery = window.matchMedia('(hover: hover) and (pointer: fine)');

	function calendarRoot(node) {
		return node && node.closest ? node.closest('.event-calendar-root') : null;
	}

	function isCompactRoot(root) {
		if (!root) {
			return compactQuery.matches;
		}
		return root.getBoundingClientRect().width <= COMPACT_MAX;
	}

	function isDayInteraction(node) {
		return isCompactRoot(calendarRoot(node));
	}

	function canHoverFor(node) {
		return hoverQuery.matches && !isDayInteraction(node);
	}

	let overlay = null;
	let activeItem = null;
	let activeDay = null;
	let hoverTimer = null;
	let requestId = 0;
	let pinned = false;

	function prefersReducedMotion() {
		return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
	}

	function wait(ms) {
		return new Promise((resolve) => {
			window.setTimeout(resolve, ms);
		});
	}

	function compareMonth(a, b) {
		return String(a).localeCompare(String(b));
	}

	function currentMonth(root) {
		const cal = root.querySelector('.event-calendar');
		return cal ? cal.getAttribute('data-month') || '' : '';
	}

	function updateUrl(month) {
		try {
			const url = new URL(window.location.href);
			url.searchParams.set('cal_month', month);
			window.history.pushState({ calMonth: month }, '', url);
		} catch (e) {
			// URL更新に失敗しても表示は続ける
		}
	}

	function itemLabelKey(item) {
		const match = String(item.className).match(/event-calendar__item--([a-z0-9_-]+)/i);
		return match ? match[1] : 'normal';
	}

	function readEvent(item) {
		return {
			title: item.getAttribute('data-title') || '',
			label: item.getAttribute('data-label') || '',
			date: item.getAttribute('data-date') || '',
			message: item.getAttribute('data-message') || '',
			link: item.getAttribute('data-link') || '',
			key: itemLabelKey(item),
		};
	}

	function ensureOverlay() {
		if (overlay) {
			return overlay;
		}

		overlay = document.createElement('div');
		overlay.className = 'event-calendar-overlay';
		overlay.hidden = true;
		overlay.innerHTML =
			'<div class="event-calendar-overlay__backdrop" data-overlay-close="1"></div>' +
			'<div class="event-calendar-overlay__card" role="dialog" aria-modal="true" aria-labelledby="event-calendar-overlay-title">' +
			'<div class="event-calendar-overlay__handle" aria-hidden="true"></div>' +
			'<button type="button" class="event-calendar-overlay__close" aria-label="閉じる">×</button>' +
			'<p class="event-calendar-overlay__date" id="event-calendar-overlay-title"></p>' +
			'<div class="event-calendar-overlay__list"></div>' +
			'</div>';

		overlay.addEventListener('pointerenter', () => {
			if (hoverTimer) {
				window.clearTimeout(hoverTimer);
				hoverTimer = null;
			}
		});
		overlay.addEventListener('pointerleave', (event) => {
			if (!hoverQuery.matches || pinned) {
				return;
			}
			if (event.relatedTarget && overlay.contains(event.relatedTarget)) {
				return;
			}
			hideOverlay();
		});

		document.body.appendChild(overlay);
		return overlay;
	}

	function setBodyLock(on) {
		document.body.classList.toggle('event-calendar-sheet-open', Boolean(on));
	}

	function hideOverlay() {
		if (hoverTimer) {
			window.clearTimeout(hoverTimer);
			hoverTimer = null;
		}
		if (activeItem) {
			activeItem.classList.remove('is-open');
			activeItem = null;
		}
		if (activeDay) {
			activeDay.classList.remove('is-selected');
			activeDay.setAttribute('aria-expanded', 'false');
			activeDay = null;
		}
		pinned = false;
		setBodyLock(false);
		if (!overlay || overlay.hidden) {
			return;
		}
		overlay.classList.remove('is-visible', 'is-sheet', 'is-popup');
		overlay.hidden = true;
	}

	function renderEvents(listEl, events) {
		listEl.replaceChildren();
		events.forEach((evt) => {
			const article = document.createElement('article');
			article.className = `event-calendar-overlay__event event-calendar-overlay__event--${evt.key}`;

			if (evt.label) {
				const meta = document.createElement('p');
				meta.className = 'event-calendar-overlay__meta';
				meta.textContent = evt.label;
				article.appendChild(meta);
			}

			const title = document.createElement('h3');
			title.className = 'event-calendar-overlay__heading';
			title.textContent = evt.title;

			article.appendChild(title);

			if (evt.message) {
				const message = document.createElement('p');
				message.className = 'event-calendar-overlay__message';
				message.textContent = evt.message;
				article.appendChild(message);
			}

			if (evt.link) {
				const link = document.createElement('a');
				link.className = 'event-calendar-overlay__link';
				link.href = evt.link;
				link.target = '_blank';
				link.rel = 'noopener noreferrer';
				link.textContent = '詳しくはこちら >';
				article.appendChild(link);
			}

			listEl.appendChild(article);
		});
	}

	function placePopup(anchor) {
		const card = overlay.querySelector('.event-calendar-overlay__card');
		const box = anchor.getBoundingClientRect();
		const cardBox = card.getBoundingClientRect();
		const gap = 10;
		const vw = window.innerWidth;
		const vh = window.innerHeight;

		let top = box.bottom + gap;
		if (top + cardBox.height > vh - 8) {
			top = box.top - cardBox.height - gap;
		}
		if (top < 8) {
			top = 8;
		}

		let left = box.left;
		if (left + cardBox.width > vw - 8) {
			left = vw - cardBox.width - 8;
		}
		if (left < 8) {
			left = 8;
		}

		card.style.top = `${Math.round(top)}px`;
		card.style.left = `${Math.round(left)}px`;
	}

	function showOverlay(events, options = {}) {
		const { dateLabel = '', mode = 'popup', anchor = null, pin = false } = options;
		const node = ensureOverlay();
		const dateEl = node.querySelector('.event-calendar-overlay__date');
		const listEl = node.querySelector('.event-calendar-overlay__list');

		dateEl.textContent = dateLabel;
		renderEvents(listEl, events);

		pinned = Boolean(pin);
		node.hidden = false;
		node.classList.toggle('is-sheet', mode === 'sheet');
		node.classList.toggle('is-popup', mode === 'popup');
		node.classList.add('is-visible');
		setBodyLock(mode === 'sheet');

		if (mode === 'popup' && anchor) {
			placePopup(anchor);
		} else {
			const card = node.querySelector('.event-calendar-overlay__card');
			card.style.top = '';
			card.style.left = '';
		}
	}

	function showItemPopup(item, pin) {
		if (activeItem && activeItem !== item) {
			activeItem.classList.remove('is-open');
		}
		activeItem = item;
		item.classList.add('is-open');
		const evt = readEvent(item);
		showOverlay([evt], {
			dateLabel: evt.date,
			mode: 'popup',
			anchor: item,
			pin,
		});
	}

	function openDaySheet(day) {
		const events = Array.from(day.querySelectorAll('.event-calendar__item')).map(readEvent);
		if (!events.length) {
			return;
		}
		if (activeDay && activeDay !== day) {
			activeDay.classList.remove('is-selected');
			activeDay.setAttribute('aria-expanded', 'false');
		}
		activeDay = day;
		day.classList.add('is-selected');
		day.setAttribute('aria-expanded', 'true');
		showOverlay(events, {
			dateLabel: day.getAttribute('data-date') || '',
			mode: 'sheet',
			pin: true,
		});
	}

	function toggleDaySheet(day) {
		if (activeDay === day && overlay && !overlay.hidden) {
			hideOverlay();
			return;
		}
		openDaySheet(day);
	}

	function applyMonthHtml(root, html) {
		const tmp = document.createElement('div');
		tmp.innerHTML = html;
		const nextCal = tmp.querySelector('.event-calendar') || tmp.firstElementChild;
		const currentCal = root.querySelector('.event-calendar');
		if (!nextCal) {
			root.innerHTML = html;
			return;
		}
		if (!currentCal) {
			root.insertBefore(nextCal, root.firstChild);
			return;
		}

		currentCal.setAttribute('data-month', nextCal.getAttribute('data-month') || '');

		const nextNav = nextCal.querySelector('.event-calendar__nav');
		const currentNav = currentCal.querySelector('.event-calendar__nav');
		if (nextNav && currentNav) {
			const nextLinks = nextNav.querySelectorAll('.event-calendar__nav-link');
			const currentLinks = currentNav.querySelectorAll('.event-calendar__nav-link');
			currentLinks.forEach((link, i) => {
				if (!nextLinks[i]) {
					return;
				}
				link.setAttribute('href', nextLinks[i].getAttribute('href') || '');
				link.setAttribute('data-month', nextLinks[i].getAttribute('data-month') || '');
			});
			const nextTitle = nextNav.querySelector('.event-calendar__title');
			const currentTitle = currentNav.querySelector('.event-calendar__title');
			if (nextTitle && currentTitle) {
				currentTitle.textContent = nextTitle.textContent;
			}
		}

		const nextStage = nextCal.querySelector('.event-calendar__stage');
		const currentStage = currentCal.querySelector('.event-calendar__stage');
		if (nextStage && currentStage) {
			currentStage.replaceWith(nextStage);
			return;
		}

		currentCal.replaceWith(nextCal);
	}

	async function loadMonth(root, month, options = {}) {
		const { pushUrl = true, direction = 0 } = options;
		if (!config.ajaxUrl || !month || root.classList.contains('is-loading')) {
			return;
		}

		hideOverlay();
		const prevMonth = currentMonth(root);
		if (prevMonth === month) {
			return;
		}

		const dir = direction || compareMonth(month, prevMonth);
		root.classList.add('is-loading');
		root.classList.toggle('is-to-prev', dir < 0);
		root.classList.toggle('is-to-next', dir > 0);
		root.setAttribute('aria-busy', 'true');

		if (!prefersReducedMotion()) {
			root.classList.add('is-leaving');
			await wait(FADE_MS);
		}

		const id = ++requestId;
		const body = new URLSearchParams();
		body.set('action', config.action || 'astrowave_event_calendar_month');
		body.set('nonce', config.nonce || '');
		body.set('cal_month', month);

		try {
			const res = await fetch(config.ajaxUrl, {
				method: 'POST',
				credentials: 'same-origin',
				headers: {
					'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
				},
				body: body.toString(),
			});
			const json = await res.json();
			if (id !== requestId) {
				return;
			}
			if (!json || !json.success || !json.data || !json.data.html) {
				throw new Error('invalid response');
			}

			applyMonthHtml(root, json.data.html);
			if (pushUrl) {
				updateUrl(json.data.month || month);
			}

			if (!prefersReducedMotion()) {
				root.classList.add('is-entering');
				root.classList.remove('is-leaving');
				requestAnimationFrame(() => {
					requestAnimationFrame(() => {
						root.classList.remove('is-entering', 'is-to-prev', 'is-to-next');
					});
				});
			} else {
				root.classList.remove('is-leaving', 'is-to-prev', 'is-to-next');
			}
		} catch (e) {
			root.classList.remove('is-leaving', 'is-to-prev', 'is-to-next');
		} finally {
			if (id === requestId) {
				root.classList.remove('is-loading');
				root.removeAttribute('aria-busy');
			}
		}
	}

	function onDocumentClick(event) {
		const closeBtn = event.target.closest('.event-calendar-overlay__close, [data-overlay-close]');
		if (closeBtn) {
			event.preventDefault();
			hideOverlay();
			return;
		}

		if (event.target.closest('.event-calendar-overlay__link')) {
			return;
		}

		const nav = event.target.closest('.event-calendar__nav-link');
		if (nav) {
			const root = nav.closest('.event-calendar-root');
			if (root) {
				event.preventDefault();
				const month = nav.getAttribute('data-month');
				if (month) {
					loadMonth(root, month, { pushUrl: true });
				}
			}
			return;
		}

		const calendarHit = event.target.closest('.event-calendar-root');
		if (calendarHit && isDayInteraction(calendarHit)) {
			const day = event.target.closest('.event-calendar__day--has-event');
			if (day) {
				event.preventDefault();
				toggleDaySheet(day);
			}
			return;
		}

		const item = event.target.closest('.event-calendar__item');
		if (item) {
			event.preventDefault();
			if (activeItem === item && pinned) {
				hideOverlay();
				return;
			}
			showItemPopup(item, true);
			return;
		}

		if (overlay && !overlay.hidden && !overlay.contains(event.target)) {
			hideOverlay();
		}
	}

	function onKeydown(event) {
		if (event.key === 'Escape') {
			hideOverlay();
			return;
		}

		const keyDay = event.target.closest('.event-calendar__day--has-event');
		if (keyDay && isDayInteraction(keyDay) && (event.key === 'Enter' || event.key === ' ')) {
			event.preventDefault();
			toggleDaySheet(keyDay);
			return;
		}

		if (isDayInteraction(event.target)) {
			return;
		}

		const item = event.target.closest('.event-calendar__item');
		if (item && (event.key === 'Enter' || event.key === ' ')) {
			event.preventDefault();
			if (activeItem === item && pinned) {
				hideOverlay();
			} else {
				showItemPopup(item, true);
			}
		}
	}

	function onPointerOver(event) {
		const item = event.target.closest('.event-calendar__item');
		if (!item || !canHoverFor(item) || pinned) {
			return;
		}
		if (hoverTimer) {
			window.clearTimeout(hoverTimer);
		}
		hoverTimer = window.setTimeout(() => {
			showItemPopup(item, false);
		}, 80);
	}

	function onPointerOut(event) {
		if (!canHoverFor(event.target) || pinned) {
			return;
		}
		const item = event.target.closest('.event-calendar__item');
		if (!item) {
			return;
		}
		const next = event.relatedTarget;
		if (next && (item.contains(next) || (overlay && overlay.contains(next)))) {
			return;
		}
		if (hoverTimer) {
			window.clearTimeout(hoverTimer);
			hoverTimer = null;
		}
		hoverTimer = window.setTimeout(() => {
			if (overlay && overlay.matches(':hover')) {
				return;
			}
			hideOverlay();
		}, 140);
	}

	function onPopState(event) {
		const root = document.querySelector('.event-calendar-root');
		if (!root) {
			return;
		}
		let month = event.state && event.state.calMonth;
		if (!month) {
			try {
				month = new URL(window.location.href).searchParams.get('cal_month');
			} catch (e) {
				month = null;
			}
		}
		if (month) {
			loadMonth(root, month, { pushUrl: false });
		}
	}

	function onScrollOrResize() {
		if (overlay && !overlay.hidden && overlay.classList.contains('is-popup') && activeItem && !isDayInteraction(activeItem)) {
			placePopup(activeItem);
		}
	}

	function onViewportChange() {
		hideOverlay();
	}

	document.addEventListener('click', onDocumentClick);
	document.addEventListener('keydown', onKeydown);
	document.addEventListener('pointerover', onPointerOver);
	document.addEventListener('pointerout', onPointerOut);
	window.addEventListener('popstate', onPopState);
	window.addEventListener('scroll', onScrollOrResize, true);
	window.addEventListener('resize', onScrollOrResize);
	if (typeof compactQuery.addEventListener === 'function') {
		compactQuery.addEventListener('change', onViewportChange);
	} else if (typeof compactQuery.addListener === 'function') {
		compactQuery.addListener(onViewportChange);
	}
})();

詳しくはAIに放り込んでください。

【補足】WordPressのディレクトリ外に出力

WordPressのディレクトリ外にあるページに表示させられるか。
しかもドメインが違う。

表示できます。ただし HTML に […] を書くだけでは動きません。PHP の do_shortcode() が必要です。

流れのまとめ

1行目: WPを読む

<head>: CSSを手動で出す

本文: do_shortcode でカレンダーHTML

</body>前: JS + Ajax設定を手動で出す(同一オリジン)

-----
サンプルページ
https://astrowave.jp/amnesia_record/event_calendar.php?cal_month=2026-08

前提(1行目)

<?php require_once($_SERVER['DOCUMENT_ROOT']."/neo/wp-load.php");?>

1. <head> … CSS を手動で出す

<?php
$astrowave_cal_base = '';
if ( defined( 'ASTROWAVE_SIMPLE_EVENT_CALENDAR_URL' ) ) {
	$astrowave_cal_base = ASTROWAVE_SIMPLE_EVENT_CALENDAR_URL;
} elseif ( ... ) {
	$astrowave_cal_base = plugins_url( '/', $astrowave_cal_file );
}
if ( $astrowave_cal_base ) {
	...
	printf( '<link rel="stylesheet" href="%s" ...>', ... 'css/event-calendar.css?ver=...' );
}
?>
処理意味
$astrowave_cal_baseプラグインの URL(CSS/JS の置き場)
defined( 'ASTROWAVE_...' )プラグインが読み込まれているか
<link ... event-calendar.css>wp_head() が無いので自分で CSS を出力

普通のテーマならプラグインが自動でやりますが、ルートではここが必要です。

2. 本文 … カレンダー本体(同じ)

<div class="stocks-event-calendar">
<?php
if ( shortcode_exists( 'astrowave_event_calendar' ) ) {
	echo do_shortcode( '[ astrowave_event_calendar ]' );
}
?>
</div>

3. </body> 直前 … JS と Ajax 設定を手動で出す

<?php
/**
 * neo 外のため wp_footer() が無い → カレンダー JS + Ajax 設定を直接出力
 * JS が無いと月送りが ?cal_month= のページ遷移になる
 */
if ( ! empty( $astrowave_cal_base ) ) {
	$astrowave_cal_ver = defined( 'ASTROWAVE_SIMPLE_EVENT_CALENDAR_VERSION' ) ? ASTROWAVE_SIMPLE_EVENT_CALENDAR_VERSION : '1.1.3';

	// admin_url() は neo.astrowave.jp を返すため、トップ(astrowave.jp)からは cross-origin になる。
	// 月送り Ajax は同一オリジンの /neo/wp-admin/admin-ajax.php を使う。
	$astrowave_cal_ajax_url = admin_url( 'admin-ajax.php' );
	if ( ! empty( $_SERVER['HTTP_HOST'] ) ) {
		$req_host  = strtolower( wp_unslash( $_SERVER['HTTP_HOST'] ) );
		$ajax_host = wp_parse_url( $astrowave_cal_ajax_url, PHP_URL_HOST );
		if ( $ajax_host && strtolower( $ajax_host ) !== $req_host ) {
			$astrowave_cal_ajax_url = ( is_ssl() ? 'https://' : 'http://' ) . $req_host . '/neo/wp-admin/admin-ajax.php';
		}
	}

	$astrowave_cal_ajax = array(
		'ajaxUrl' => $astrowave_cal_ajax_url,
		'nonce'   => wp_create_nonce( 'astrowave_event_calendar' ),
		'action'  => 'astrowave_event_calendar_month',
	);
	printf(
		'<script>window.astrowaveEventCalendar = %s;</script>' . "\n",
		wp_json_encode( $astrowave_cal_ajax )
	);
	printf(
		'<script src="%s" defer></script>' . "\n",
		esc_url( $astrowave_cal_base . 'js/event-calendar.js?ver=' . rawurlencode( (string) $astrowave_cal_ver ) )
	);
}
?>
処理意味
admin_url()本来の Ajax URL(neo.astrowave.jp になる)
ホスト比較して書き換えページは astrowave.jp なので 同一オリジン の /neo/wp-admin/admin-ajax.php に変更
nonce不正リクエスト防止用のワンタイムキー
actionサーバー側の「月送り処理」の名前
window.astrowaveEventCalendarJS が読む設定オブジェクト
event-calendar.js月送り・ポップアップ用スクリプト

ここが無いと、見た目だけ出て月送りが動かない/?cal_month= でページ全体が切り替わる、になります。

【AI】イラストを描いてもらった

Flow」で作成した画像です。誰でもgoogleアカウントでログインして使えます。

AIイラスト02

星間旅路のメロディ

「宇宙の静けさに包まれながら、漂流する過去の音楽を捜し求め、銀河の奥底でその旋律に耳を傾ける。」

「この電波はどこの星からきたのだろうか。」

きっとうまくいくさ。

どうしたんですか。