VSCodeからWordPressへ!Markdown記事を自動投稿するシステムを作った話

Development
スポンサーリンク
スポンサーリンク

はじめに:なぜこのシステムを作ったのか

ブログを書くとき、皆さんはどんなエディタを使っていますか?

WordPress標準のGutenbergエディタも便利ですが、長文を書いているとこんな不満が出てきませんか?

  • ブラウザがクラッシュしたら書いた内容が消える不安
  • 画像管理が煩雑で、同じ画像が重複してアップロードされる
  • Markdown記法で書けないもどかしさ
  • バージョン管理ができない

私もまさにこの悩みを抱えていました。そこで、VSCodeでMarkdownを書いて、ワンコマンドでWordPressに投稿できるシステムを作ることにしました。

この記事では、その開発の経緯と、遭遇した問題、そして最終的に完成したシステムについてご紹介します。

 

理想の執筆環境とは

まず、私が目指した理想の執筆環境はこんな感じです:

求めていた機能

1. VSCodeでMarkdownを書きたい

  • 慣れ親しんだエディタで執筆
  • リアルタイムプレビュー(Ctrl+Shift+V)
  • Gitでバージョン管理

2. 画像管理を自動化したい

  • ローカルの画像を自動アップロード
  • 画像パスを自動で置換
  • 同じ画像の重複を避ける

3. ワンコマンドで投稿したい

npm run publish

これだけで完了!

4. 既存記事の更新も自動化したい

  • スラッグで既存記事を検索
  • 見つかったら自動更新
  • 見つからなければ新規投稿

 

開発の流れ:試行錯誤の連続

Phase 1:まずは基本機能

最初は、シンプルに「MarkdownをWordPressに投稿する」ことを目指しました。

使用した技術:

  • Node.js
  • WordPress REST API
  • marked(Markdown → HTML変換)
  • gray-matter(Front Matter解析)

基本的な処理フロー:

1. post.md を読み込み
2. Front Matter解析(タイトル、スラッグなど)
3. Markdown → HTML変換
4. WordPress REST APIで投稿

最初のコード(簡略版):

const marked = require('marked');
const matter = require('gray-matter');
const axios = require('axios');

// Markdownファイル読み込み
const fileContent = fs.readFileSync('post.md', 'utf-8');
const { data: frontMatter, content: markdown } = matter(fileContent);

// HTML変換
const htmlContent = marked(markdown);

// WordPress投稿
await axios.post(
  `${WP_URL}/wp-json/wp/v2/posts`,
  {
    title: frontMatter.title,
    content: htmlContent,
    slug: frontMatter.slug,
    status: 'draft'
  },
  { auth: { username: WP_USER, password: WP_APP_PASSWORD } }
);

この時点では、「動く!」という感動がありました。VSCodeで書いた記事が本当にWordPressに投稿されたのです。

 

Phase 2:画像アップロードの実装

次に、画像の自動アップロードに取り組みました。

課題:

  • Markdown内の相対パス(images/screenshot.png)を、WordPressのURLに置換する必要がある

解決策:

  1. images/ フォルダ内の画像を全てアップロード
  2. アップロード後のURLを取得
  3. Markdown内のパスを置換

実装:

// 画像アップロード
const imageMap = {};

for (const imageFile of imageFiles) {
  const imagePath = path.join(imagesDir, imageFile);
  
  const form = new FormData();
  form.append('file', fs.createReadStream(imagePath));
  
  const response = await axios.post(
    `${WP_URL}/wp-json/wp/v2/media`,
    form,
    { auth: { username: WP_USER, password: WP_APP_PASSWORD } }
  );
  
  // ローカルパス → WordPressのURL
  imageMap[`images/${imageFile}`] = response.data.source_url;
}

// Markdown内のパスを置換
let updatedMarkdown = markdown;
for (const [localPath, wpUrl] of Object.entries(imageMap)) {
  updatedMarkdown = updatedMarkdown.replace(
    new RegExp(`\\(${localPath}\\)`, 'g'),
    `(${wpUrl})`
  );
}

これで、画像が自動でアップロードされ、記事内のリンクも正しく置換されるようになりました!

 

Phase 3:WP Githuber MDプラグインとの出会い

ここで、WP Githuber MDという素晴らしいプラグインを発見しました。

このプラグインを使うと、WordPress側でもMarkdown記法で編集できます。つまり:

VSCodeでMarkdown編集
  ↓
WordPressに投稿(Markdownのまま)
  ↓
WordPress側でもMarkdown編集可能!

試してみた結果:

最初は「Markdown → HTML変換」していましたが、WP Githuber MDを使えばMarkdownをそのまま投稿できるはず...

しかし、ここで大きな問題が発生しました。

 

遭遇した問題たち

問題1:Markdownがそのまま表示される

症状:


## 見出し

本文...

このMarkdownをそのまま送信すると、WordPress側で:

# タイトル

## 見出し

本文...

Markdownの記号がそのまま表示されてしまいました。

原因:

  • WordPress REST APIは、デフォルトでMarkdownを認識しない
  • WP Githuber MDのメタフィールドを設定する必要がある

解決策:

// メタフィールドを追加
const postData = {
  title: frontMatter.title,
  content: markdown,  // HTMLではなくMarkdown
  meta: {
    _is_githuber_markdown: '1',
    _is_githuber_markdown_enabled: 'yes'
  }
};

これで、Markdownがそのまま送信され、WordPress側で正しく表示されるようになりました!

 

問題2:既存記事がHTMLの場合

次の問題:

既存記事(すでにHTMLで保存されている)を、Markdownで更新しようとすると...

症状:

既存記事(HTML形式)
  ↓
Markdownを送信
  ↓
WordPress: 「これはテキストだ」
  ↓

## 見出し

で保存される

つまり、一度HTMLとして保存された記事は、Markdownとして認識されない!

試行錯誤:

  1. WordPress管理画面で手動変換 → 面倒
  2. 記事を削除して再投稿 → 一時的に記事が消える
  3. モード切替可能にする → これだ!

 

問題3:画像が増え続ける

最大の問題:

記事を更新するたびに、画像が新規アップロードされる:

1回目: featured.jpg
2回目: featured-2.jpg  ← 新規
3回目: featured-3.jpg  ← どんどん増える

WordPress Media Libraryが肥大化!

この問題の解決が、今回の開発で最も時間をかけた部分でした。

 

最大の挑戦:画像管理の最適化

解決策の検討

方法1:画像を再利用

  • ファイル名で検索 → 見つかったら再利用
  • 問題: 内容が変わっても検出できない

方法2:削除して再アップロード

  • 毎回削除 → 再アップロード
  • 問題: 遅い、無駄

方法3:ハッシュベース(採用!)

  • 画像の内容からハッシュを計算
  • ハッシュが一致 → 再利用
  • ハッシュが異なる → 削除して再アップロード

ハッシュベースの実装

1. ハッシュ計算:

const crypto = require('crypto');

function calculateFileHash(filePath) {
  const fileBuffer = fs.readFileSync(filePath);
  const hashSum = crypto.createHash('md5');
  hashSum.update(fileBuffer);
  const hex = hashSum.digest('hex');
  return hex.substring(0, 6);  // 最初の6桁
}

2. ファイル名にハッシュを含める:

元のファイル名: featured.jpg
  ↓
ハッシュ計算: a3f2b1
  ↓
新しいファイル名: article-slug-featured-a3f2b1.jpg

3. 既存画像を検索:

async function findExistingImageByPattern(slug, originalFileName) {
  const searchPattern = `${slug}-${originalFileName}`;
  
  const response = await axios.get(
    `${WP_URL}/wp-json/wp/v2/media`,
    {
      auth: { username: WP_USER, password: WP_APP_PASSWORD },
      params: { search: searchPattern }
    }
  );
  
  // パターンに一致する画像を探す
  for (const media of response.data) {
    const fileName = path.basename(media.source_url);
    if (fileName.startsWith(searchPattern)) {
      return media;
    }
  }
  
  return null;
}

4. 統合処理:

async function uploadOrReuseImage(imagePath, fileName, slug) {
  // ハッシュ計算
  const hash = calculateFileHash(imagePath);
  const newFileName = `${slug}-${fileName}-${hash}.jpg`;
  
  // 既存画像を検索
  const existingImage = await findExistingImageByPattern(slug, fileName);
  
  if (existingImage) {
    const existingFileName = path.basename(existingImage.source_url);
    
    if (existingFileName.includes(`-${hash}`)) {
      // ハッシュ一致 → 再利用
      console.log(`♻️  ${fileName} (変更なし)`);
      return { id: existingImage.id, url: existingImage.source_url };
    } else {
      // ハッシュ不一致 → 削除
      console.log(`🗑️  古い画像を削除`);
      await deleteImage(existingImage.id);
    }
  }
  
  // 新規アップロード
  console.log(`✅ ${fileName} → ${newFileName}`);
  return await uploadImage(imagePath, newFileName);
}

結果:

1回目実行:
  ✅ featured.jpg → article-featured-a3f2b1.jpg

2回目実行(画像変更なし):
  ♻️  featured.jpg (変更なし)  ← 再利用!

3回目実行(画像を更新):
  🗑️  古い画像を削除: article-featured-a3f2b1.jpg
  ✅ featured.jpg → article-featured-9e8f7d.jpg  ← 新しいハッシュ

完璧に動作しました!🎉

 

完成したシステム

最終的なアーキテクチャ

VSCode
  ↓ Markdown記法で執筆
posts/
├── article-name/
│   ├── post.md
│   ├── featured.jpg
│   └── images/
│       └── screenshot.png
  ↓ npm run publish
Node.jsスクリプト
  ├─ Front Matter解析
  ├─ H1削除
  ├─ 画像アップロード(ハッシュ管理)
  ├─ パス置換
  ├─ HTML変換(HTMLモード)
  └─ 既存記事検索・更新
  ↓ WordPress REST API
WordPress
  ↓ 表示
ブログ記事

主要機能

1. 2つのモード

# HTMLモード(推奨)
mode: html
→ スクリプト側でHTML変換
→ Gutenbergエディタ

# Markdownモード
mode: markdown
→ Markdownをそのまま送信
→ WP Githuber MDエディタ

2. 既存記事の自動更新

// 既存記事を検索
const existingPost = await findExistingPost(frontMatter.slug);

if (existingPost) {
  // 更新
  await axios.post(`/wp-json/wp/v2/posts/${existingPost.id}`, data);
} else {
  // 新規投稿
  await axios.post(`/wp-json/wp/v2/posts`, data);
}

3. ハッシュベースの画像管理

画像変更なし → 再利用 ♻️
画像変更あり → 削除 🗑️ → 再アップロード ✅

4. タイトル重複の自動回避

// 最初のH1を自動削除
updatedMarkdown = removeFirstH1(updatedMarkdown);

 

実際の運用フロー

日常的な記事投稿

# 1. 記事作成
code posts/new-article/post.md

# 2. 画像配置
posts/new-article/
├── post.md
├── featured.jpg
└── images/
    └── screenshot.png

# 3. プレビュー確認
Ctrl+Shift+V

# 4. 投稿
npm run publish-single new-article

# 5. WordPress管理画面で確認
→ 公開

所要時間:わずか数秒!

既存記事の更新

# 1. VSCodeで編集
code posts/existing-article/post.md

# 2. 画像も更新(上書き保存)
posts/existing-article/images/screenshot.png

# 3. 投稿
npm run test-local

# 4. 出力確認
📸 画像アップロード中...
  🗑️  古い画像を削除: article-screenshot-a3f2b1.png
  ✅ screenshot.png → article-screenshot-9e8f7d.png
📝 既存記事を更新します(ID: 123)
✅ 更新成功!

画像の更新も自動検出!

 

得られたメリット

1. 執筆効率の向上

Before:

WordPress Gutenbergで執筆
  ↓ 画像をドラッグ&ドロップ
  ↓ ブラウザがクラッシュしないか不安
  ↓ バージョン管理なし

After:

VSCodeで執筆
  ↓ Ctrl+Shift+V でプレビュー
  ↓ Gitでバージョン管理
  ↓ npm run publish
  ↓ 完了!

2. 画像管理の最適化

Before:

同じ画像が何度もアップロードされる
→ Media Libraryが肥大化
→ 管理が大変

After:

画像の内容が変わっていなければ再利用
→ Media Libraryがクリーン
→ 無駄なアップロードなし

3. 既存記事の更新が楽

Before:

WordPress管理画面を開く
→ 記事を探す
→ 編集
→ 保存

After:

VSCodeで編集
→ npm run publish
→ 完了!

4. バージョン管理

Gitで管理:

git commit -m "記事追加:○○について"
git push

履歴が残る:

  • いつ何を変更したか
  • 過去のバージョンに戻せる
  • チーム開発も可能

 

今後の展望

GitHub Actions連携

次のステップは、GitHub Actionsでの自動投稿です:

name: Publish to WordPress

on:
  push:
    branches: [main]
    paths: ['posts/**']

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm install
      - env:
          WP_URL: ${{ secrets.WP_URL }}
          WP_USER: ${{ secrets.WP_USER }}
          WP_APP_PASSWORD: ${{ secrets.WP_APP_PASSWORD }}
        run: npm run publish

運用フロー:

VSCodeで記事作成
  ↓
git push
  ↓
GitHub Actions自動実行
  ↓
WordPressに自動投稿

完全自動化!

複数サイト対応

複数のWordPressサイトへの投稿も検討中:

npm run publish-site1
npm run publish-site2

バルク投稿

過去記事の一括移行:

npm run publish  # 全記事を投稿

 

まとめ

開発を通じて学んだこと

1. 試行錯誤の重要性

最初から完璧なシステムは作れません。問題に遭遇するたびに、解決策を考え、実装し、改善する。この繰り返しが重要でした。

2. ユーザー体験の大切さ

技術的に可能でも、運用が面倒では意味がありません。「ファイル名を変える」という一手間を省くために、ハッシュベースの仕組みを実装しました。

3. ドキュメントの価値

試行錯誤の過程を記録しておくことで、後から見返したときに「なぜこうしたのか」が分かります。

このシステムを作って良かったこと

  • 執筆が楽しくなった
  • バージョン管理ができるようになった
  • 画像管理のストレスがなくなった
  • 記事の更新が簡単になった

そして何より、このシステム自体がブログ記事になりました!

 

技術スタック

最後に、使用した技術をまとめておきます:

言語・ランタイム:

  • Node.js

ライブラリ:

  • axios - HTTP通信
  • marked - Markdown → HTML変換
  • gray-matter - Front Matter解析
  • form-data - ファイルアップロード
  • dotenv - 環境変数管理
  • crypto - ハッシュ計算

WordPress側:

  • WordPress REST API
  • WP Githuber MD(オプション)

エディタ:

  • VSCode

バージョン管理:

  • Git / GitHub

 

おわりに

VSCodeでブログを書きたい、画像管理を自動化したい、という単純な動機から始まったこのプロジェクト。

試行錯誤を繰り返しながら、最終的には想像以上に完成度の高いシステムになりました。

もし同じような悩みを抱えている方がいたら、この記事が参考になれば嬉しいです。

そして、「自分でツールを作る」という選択肢を持つことで、日々の作業がもっと楽しくなるかもしれません。

Happy Blogging! 🚀


参考リンク

最終更新: 2026-01-11

コメント