2026年5月22日

2026年5月22日

MarkdownをHTMLに変換してWordPressに投稿するCLIツールの作り方(WP-CLIカスタムコマンド)

Markdownで書いた原稿をコマンド1つでHTMLに変換し、WordPressの投稿として保存したい場合は、WP-CLIのカスタムコマンドとして実装するのが素直な方法です。この記事では、まず既存のツールだけで済ませる最小構成を示し、そのうえでフロントマター対応・既存投稿の更新・ドライランを備えた wp md import コマンドを、PHPのMarkdownライブラリ(league/commonmark)を使って作る手順を説明します。

GitHubへのpushをきっかけに自動で反映したい場合は、GitHub ActionsとREST APIを使う別記事の方法が向いています。本記事はサーバー上(またはローカル開発環境)で手元のファイルを取り込む使い方に絞っています。

まずは既存ツールだけで試す

WP-CLIの wp post create は、ファイルを引数に渡すとその内容を本文として投稿を作成できます。Markdownの変換に汎用の変換ツール(Pandocなど)を使えば、コードを書かずに一連の流れを試せます。

# Markdown を HTML に変換(GitHub 風の記法として読み込む)
pandoc -f gfm -t html hello.md -o hello.html

# 下書きとして投稿を作成し、作成された投稿IDだけを表示
wp post create hello.html --post_title="はじめての投稿" --post_status=draft --porcelain

更新するときは wp post update 123 hello.html のように投稿IDとファイルを渡します。数本を取り込むだけならこれで十分です。

ただし、この方法ではタイトルやスラッグを毎回コマンドの引数で渡す必要があり、どのファイルがどの投稿に対応するかも自分で覚えておかなければなりません。原稿が増えてくると手間と取り違えが増えるため、次の節からはそれを1つのコマンドにまとめます。

作るコマンドの仕様

完成形は次のように使えるコマンドです。

wp md import posts/*.md --user=editor-name
wp md import posts/hello.md --dry-run
  • Markdownファイル先頭のYAMLフロントマターから、タイトル・スラッグ・ステータスを読む
  • スラッグが同じ投稿があれば更新、無ければ新規作成する
  • --dry-run を付けると保存せず、変換後のHTMLの冒頭と処理内容だけを表示する
  • フロントマターにステータスが無ければ下書きにする

原稿は例えば次のように書きます。

---
title: CLIから投稿するテスト
slug: cli-import-test
status: draft
---

## 見出し

本文を **Markdown** で書きます。

準備:プラグインの雛形とライブラリの導入

コマンドはテーマではなく、小さなプラグインとして作ります。テーマを切り替えてもコマンドが使えるようにするためです。wp-content/plugins/md-import/ を作り、その中でComposerを使ってライブラリを入れます。

cd wp-content/plugins/md-import
composer require league/commonmark symfony/yaml

league/commonmarkはCommonMark仕様とGitHub風の拡張(表・打ち消し線・タスクリストなど)に対応したPHPのMarkdownパーサーです。フロントマターの読み取りには同ライブラリの拡張機能を使い、YAMLの解析にsymfony/yamlを使います。各ライブラリが要求するPHPのバージョンは更新で変わるため、サーバーのPHPバージョンが要件を満たしているかをComposerの出力や各ライブラリのドキュメントで確認してください(2026年10月現在)。

本番サーバーにComposerが無い場合は、ローカルで vendor/ まで作ってからプラグインごとアップロードする方法でもかまいません。

コマンドの実装

wp-content/plugins/md-import/md-import.php に次の内容を置き、管理画面かWP-CLI(wp plugin activate md-import)で有効化します。作業はまずステージング環境で行い、本番で使う前にデータベースのバックアップを取ってください。

<?php
/**
 * Plugin Name: Markdown Import Command
 * Description: Markdown を HTML に変換して投稿を作成・更新する WP-CLI コマンド。
 */

use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\FrontMatter\FrontMatterExtension;
use League\CommonMark\Extension\FrontMatter\Output\RenderedContentWithFrontMatter;
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
use League\CommonMark\MarkdownConverter;

if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
	return;
}

require_once __DIR__ . '/vendor/autoload.php';

class My_Markdown_Import_Command {

	const ALLOWED_STATUSES = array( 'draft', 'pending', 'private', 'publish' );

	/**
	 * Markdown ファイルを HTML に変換して投稿として保存します。
	 *
	 * ## OPTIONS
	 *
	 * <file>...
	 * : 取り込む Markdown ファイル。
	 *
	 * [--dry-run]
	 * : 保存せずに処理内容だけを表示します。
	 *
	 * ## EXAMPLES
	 *
	 *     wp md import posts/hello.md --user=editor-name
	 *
	 * @when after_wp_load
	 */
	public function import( $args, $assoc_args ) {
		$dry_run   = \WP_CLI\Utils\get_flag_value( $assoc_args, 'dry-run', false );
		$converter = $this->converter();
		$failed    = 0;

		foreach ( $args as $file ) {
			if ( ! is_readable( $file ) ) {
				WP_CLI::warning( "読み込めません: {$file}" );
				$failed++;
				continue;
			}

			$result = $converter->convert( file_get_contents( $file ) );
			$meta   = ( $result instanceof RenderedContentWithFrontMatter )
				? (array) $result->getFrontMatter()
				: array();
			$html   = $result->getContent();

			$basename = pathinfo( $file, PATHINFO_FILENAME );
			$title    = isset( $meta['title'] ) ? sanitize_text_field( $meta['title'] ) : $basename;
			$slug     = sanitize_title( isset( $meta['slug'] ) ? $meta['slug'] : $basename );
			$status   = ( isset( $meta['status'] ) && in_array( $meta['status'], self::ALLOWED_STATUSES, true ) )
				? $meta['status']
				: 'draft';

			$existing = get_posts( array(
				'name'           => $slug,
				'post_type'      => 'post',
				'post_status'    => 'any',
				'fields'         => 'ids',
				'posts_per_page' => 1,
				'no_found_rows'  => true,
			) );
			$post_id  = $existing ? (int) $existing[0] : 0;

			if ( $dry_run ) {
				WP_CLI::log( sprintf(
					'[dry-run] %s → %s(slug: %s, status: %s)',
					$file,
					$post_id ? "投稿ID {$post_id} を更新" : '新規作成',
					$slug,
					$status
				) );
				WP_CLI::log( mb_substr( wp_strip_all_tags( $html ), 0, 80 ) );
				continue;
			}

			$postarr = array(
				'post_type'    => 'post',
				'post_title'   => $title,
				'post_name'    => $slug,
				'post_content' => $html,
				'post_status'  => $status,
			);
			if ( $post_id ) {
				$postarr['ID'] = $post_id;
				$saved = wp_update_post( wp_slash( $postarr ), true );
			} else {
				$saved = wp_insert_post( wp_slash( $postarr ), true );
			}

			if ( is_wp_error( $saved ) ) {
				WP_CLI::warning( "{$file}: " . $saved->get_error_message() );
				$failed++;
				continue;
			}
			WP_CLI::success( sprintf( '%s → 投稿ID %d(%s)', $file, $saved, $post_id ? '更新' : '作成' ) );
		}

		if ( $failed ) {
			WP_CLI::error( "{$failed} 件のファイルで失敗しました。" );
		}
	}

	private function converter() {
		$environment = new Environment( array(
			'html_input'         => 'strip',
			'allow_unsafe_links' => false,
		) );
		$environment->addExtension( new CommonMarkCoreExtension() );
		$environment->addExtension( new GithubFlavoredMarkdownExtension() );
		$environment->addExtension( new FrontMatterExtension() );

		return new MarkdownConverter( $environment );
	}
}

WP_CLI::add_command( 'md', 'My_Markdown_Import_Command' );

有効化したら wp help md import を実行してください。docコメントに書いたオプションと使用例がヘルプとして表示されれば、コマンドの登録はできています。WP-CLIはこのdocコメントの ## OPTIONS を読んで引数を検証するため、書式(コロンや空行の位置)を崩すと引数が正しく認識されません。

実装のポイントと、なぜそうしているか

WP-CLIからだけ読み込む

ファイル冒頭で WP_CLI 定数を確認し、通常のページ表示や管理画面ではライブラリを読み込まないようにしています。サイトの表示速度に影響を与えず、Webからこのコードが実行されることもありません。

変換時に生のHTMLとJavaScriptリンクを取り除く

league/commonmarkの設定で html_input を strip にすると、Markdown内に直接書かれたHTMLタグは出力から取り除かれます。allow_unsafe_links を false にすると、javascript: などの危険なスキームのリンクも無効になります。原稿の書き手が自分だけで、Markdownの中にHTMLを書きたい場合は allow に変えることもできますが、複数人の原稿を取り込むなら既定の安全側のままにしておくのが無難です。

実行ユーザーを指定する

WP-CLIはそのままでは「ログインしていないユーザー」として動きます。この状態で投稿を作ると投稿者が空になるため、--user=ユーザー名 で投稿者を指定してください。また、WordPressは保存時にユーザーの権限に応じてHTMLを絞り込みます(kses)。unfiltered_html 権限を持たないユーザーで実行すると、変換後のHTMLもさらに安全側に整えられます。

値をそろえてから保存する

フロントマターの値は外部入力として扱い、タイトルは sanitize_text_field()、スラッグは sanitize_title() で整え、ステータスは許可した値以外を下書きにしています。wp_insert_post() と wp_update_post() は受け取ったデータからスラッシュを取り除くので、本文中のバックスラッシュ(コード例など)が消えないよう wp_slash() を通しています。

既存投稿の照合はスラッグで行う

post_status に any を指定して、公開済みだけでなく下書きや予約投稿も照合の対象にしています(ゴミ箱の投稿は対象外です)。ファイル名やフロントマターのスラッグを後から変えると、別の投稿として新規作成される点に注意してください。

つまずきやすい点

  • Class "League\CommonMark\..." not found:vendor/autoload.php が無いか、別の場所でComposerを実行しています。プラグインのディレクトリ内に vendor/ があるかを確認します
  • フロントマターがそのまま本文に出る:フロントマター拡張が読み込まれていないか、ファイル先頭の --- の前に空行やBOMが入っています
  • ブロックエディターで「クラシック」ブロックとして開く:変換したHTMLにはブロックの区切りコメントが無いためです。表示には影響しませんが、ブロック単位で編集したい場合はエディターのブロックへの変換機能を使います
  • 画像が表示されない:原稿内の相対パスはWordPress上で解決できません。先に wp media import で画像を取り込み、そのURLを原稿に書くか、コマンドに画像の取り込みとURL置き換えの処理を追加します

まとめ

  • 数本の取り込みなら、Pandocなどの変換ツールと wp post create <ファイル> の組み合わせで足ります
  • 繰り返し使うなら、WP-CLIのカスタムコマンドにまとめると、フロントマターの読み取りや既存投稿の更新を自動化できます
  • 変換にはleague/commonmarkを使い、生のHTMLと危険なリンクを取り除く設定にしておきます
  • 実行時は --user で投稿者を指定し、--dry-run で内容を確かめてから保存します
  • 作業はステージングで試し、本番で使う前にバックアップを取ります

お気軽にご相談ください

お見積りへ お問い合わせへ
▲