Node.jsでWordPressに記事を投稿する方法

プログラミング関連

この記事でわかること

  • Node.jsのスクリプトからWordPressに記事を投稿する仕組み
  • WordPress REST APIとアプリケーションパスワードの使い方
  • Gutenbergブロック形式での投稿方法
  • カテゴリ・タグ・アイキャッチ画像・メタディスクリプションの自動設定

なぜNode.jsで投稿するのか

WordPressの管理画面を開かずに、コマンド一発で記事を投稿できる。 定型フォーマットの記事(俳句・川柳・日報など)は特に効果的で、投稿作業を自動化できる。


必要なもの

  • WordPress サイト(REST APIが有効であること)
  • Node.js(実行環境)
  • WordPress のアプリケーションパスワード

手順

1. アプリケーションパスワードの発行

WordPress管理画面 → ユーザー → プロフィール → 「アプリケーションパスワード」セクション

  • アプリ名を入力(例:Claude Code)
  • 「新しいアプリケーションパスワードを追加」をクリック
  • 表示されたパスワードをコピーして保存(再表示されないので注意)

2. 認証情報を .env ファイルに保存

WP_USER=WordPressのユーザー名
WP_APP_PASSWORD=発行したアプリケーションパスワード

3. 投稿スクリプト(Node.js)の仕組み

認証

ユーザー名とアプリケーションパスワードをBase64エンコードして Authorization: Basic xxxxxx ヘッダーに付与する。

REST APIエンドポイント

POST https://サイトURL/wp-json/wp/v2/posts

投稿データの構成

{
  "title": "記事タイトル",
  "content": "本文(Gutenbergブロック形式)",
  "status": "publish",
  "categories": [カテゴリID],
  "tags": [タグID],
  "featured_media": メディアID,
  "excerpt": "メタディスクリプションに使う文章"
}

4. Gutenbergブロック形式で投稿する

クラシックエディタではなくブロックエディタとして認識させるには、 本文をブロックコメントで囲む必要がある。

<!-- wp:paragraph -->
<p>本文テキスト</p>
<!-- /wp:paragraph -->

グループブロック(枠線付き)の例:

<!-- wp:group {"style":{"border":{"width":"2px","style":"double","color":"#000000"}}} -->
<div class="wp-block-group" style="border-color:#000000;border-style:double;border-width:2px">
  <!-- wp:paragraph -->
  <p>枠の中のテキスト</p>
  <!-- /wp:paragraph -->
</div>
<!-- /wp:group -->

5. タグIDの取得・自動作成

タグが存在しない場合は自動で作成する仕組みを組み込める。

GET /wp-json/wp/v2/tags?search=タグ名
→ 存在すればIDを取得
→ 存在しなければ POST /wp-json/wp/v2/tags で作成

6. アイキャッチ画像の設定

あらかじめWordPressにアップロード済みの画像をURLで検索してIDを取得する。

GET /wp-json/wp/v2/media?search=ファイル名
→ source_url が一致するものの ID を使用

7. メタディスクリプションの設定(Cocoonテーマ)

Cocoonテーマは excerpt(抜粋)フィールドをメタディスクリプションとして使用する。 Yoast SEO等のプラグインが不要な場合はこの方法が有効。

"excerpt": "メタディスクリプションにしたいテキスト"

スクリプト全文

post_article.js

const https = require('https');
const path = require('path');
const fs = require('fs');
// 認証情報の読み込み
function loadCredentials() {
  const envPath = path.join(__dirname, '.env');
  if (!fs.existsSync(envPath)) {
    console.error('エラー: .env ファイルが見つかりません。');
    process.exit(1);
  }
  const lines = fs.readFileSync(envPath, 'utf8').split('\n');
  const env = {};
  for (const line of lines) {
    const m = line.match(/^([^=]+)=(.+)$/);
    if (m) env[m[1].trim()] = m[2].trim();
  }
  if (!env.WP_USER || !env.WP_APP_PASSWORD) {
    console.error('エラー: .env に WP_USER と WP_APP_PASSWORD を設定してください。');
    process.exit(1);
  }
  return env;
}
function apiRequest(method, endpoint, body, auth) {
  return new Promise((resolve, reject) => {
    const token = Buffer.from(`${auth.WP_USER}:${auth.WP_APP_PASSWORD}`).toString('base64');
    const data = body ? JSON.stringify(body) : null;
    const options = {
      hostname: 'あなたのサイトURL',
      path: `/wp-json/wp/v2/${endpoint}`,
      method,
      headers: {
        'Authorization': `Basic ${token}`,
        'Content-Type': 'application/json',
        ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {})
      }
    };
    const req = https.request(options, (res) => {
      let buf = '';
      res.on('data', chunk => buf += chunk);
      res.on('end', () => {
        try {
          const json = JSON.parse(buf);
          if (res.statusCode >= 400) reject(new Error(json.message || `HTTP ${res.statusCode}`));
          else resolve(json);
        } catch (e) {
          reject(new Error('レスポンスのパースに失敗: ' + buf.slice(0, 200)));
        }
      });
    });
    req.on('error', reject);
    if (data) req.write(data);
    req.end();
  });
}
// Markdownファイルのパースと変換
function parseMarkdown(filePath) {
  const raw = fs.readFileSync(filePath, 'utf8');
  let title = '';
  let body = raw;
  const frontmatterMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
  if (frontmatterMatch) {
    const meta = frontmatterMatch[1];
    body = frontmatterMatch[2].trim();
    const titleMatch = meta.match(/^title:\s*(.+)$/m);
    if (titleMatch) title = titleMatch[1].trim();
  }
  if (!title) {
    const h1Match = body.match(/^#\s+(.+)$/m);
    if (h1Match) {
      title = h1Match[1].trim();
      body = body.replace(/^#\s+.+\n?/, '').trim();
    }
  }
  if (!title) {
    console.error('エラー: タイトルが見つかりません。');
    process.exit(1);
  }
  const content = convertToGutenberg(body);
  return { title, content, excerpt: body.replace(/[#*`|>\-_\[\]()]/g, '').slice(0, 200).trim() };
}
// MarkdownをGutenbergブロック形式に変換
function convertToGutenberg(md) {
  const lines = md.split('\n');
  const blocks = [];
  let i = 0;
  while (i < lines.length) {
    const line = lines[i];
    if (line.startsWith('```')) {
      const lang = line.slice(3).trim();
      let code = '';
      i++;
      while (i < lines.length && !lines[i].startsWith('```')) { code += lines[i] + '\n'; i++; }
      const escaped = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
      blocks.push(`<!-- wp:code -->\n<pre class="wp-block-code"><code${lang ? ` class="language-${lang}"` : ''}>${escaped.trimEnd()}</code></pre>\n<!-- /wp:code -->`);
      i++; continue;
    }
    const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
    if (headingMatch) {
      const level = headingMatch[1].length;
      blocks.push(`<!-- wp:heading {"level":${level}} -->\n<h${level} class="wp-block-heading">${inlineConvert(headingMatch[2])}</h${level}>\n<!-- /wp:heading -->`);
      i++; continue;
    }
    if (line.match(/^[-*]\s+/)) {
      let items = [];
      while (i < lines.length && lines[i].match(/^[-*]\s+/)) { items.push('<li>' + inlineConvert(lines[i].replace(/^[-*]\s+/, '')) + '</li>'); i++; }
      blocks.push(`<!-- wp:list -->\n<ul class="wp-block-list">${items.join('')}</ul>\n<!-- /wp:list -->`); continue;
    }
    if (line.match(/^---+$/)) {
      blocks.push(`<!-- wp:separator -->\n<hr class="wp-block-separator"/>\n<!-- /wp:separator -->`);
      i++; continue;
    }
    if (line.trim() === '') { i++; continue; }
    let para = '';
    while (i < lines.length && lines[i].trim() !== '' && !lines[i].startsWith('#') && !lines[i].startsWith('```') && !lines[i].match(/^[-*]\s+/) && !lines[i].match(/^---+$/)) {
      para += (para ? ' ' : '') + lines[i].trim(); i++;
    }
    if (para) blocks.push(`<!-- wp:paragraph -->\n<p>${inlineConvert(para)}</p>\n<!-- /wp:paragraph -->`);
  }
  return blocks.join('\n\n');
}
function inlineConvert(text) {
  return text
    .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
    .replace(/\*(.+?)\*/g, '<em>$1</em>')
    .replace(/`(.+?)`/g, '<code>$1</code>')
    .replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>');
}
function parseArgs(args) {
  const result = {};
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--file' || args[i] === '-f') result.file = args[++i];
    else if (args[i] === '--draft') result.draft = true;
  }
  return result;
}
async function main() {
  const args = parseArgs(process.argv.slice(2));
  if (!args.file) { console.log('使い方: node post_article.js --file "記事.md" [--draft]'); process.exit(1); }
  if (!fs.existsSync(args.file)) { console.error('エラー: ファイルが見つかりません: ' + args.file); process.exit(1); }
  const auth = loadCredentials();
  const { title, content, excerpt } = parseMarkdown(args.file);
  const status = args.draft ? 'draft' : 'publish';
  console.log(`タイトル: ${title} / ステータス: ${status}`);
  try {
    const post = await apiRequest('POST', 'posts', { title, content, status, excerpt }, auth);
    console.log('✅ 投稿完了! URL: ' + post.link);
  } catch (err) {
    console.error('エラー:', err.message);
    process.exit(1);
  }
}
main();

実際の使い方(コマンド例)

下書きの場合

node post_article.js --file "記事ファイル.md" --draft

公開の場合

node post_article.js --file "記事ファイル.md"

ハマりやすいポイント

問題原因解決策
クラシックエディタで保存されるブロックコメントがない で囲む
カテゴリが反映されないカテゴリIDが間違っているREST APIでカテゴリ一覧を確認する
メタディスクリプションが入らないYoast SEOフィールドを使っているテーマに合わせたフィールドを使う
readline エラー対話形式はClaude Codeで動かないCLIコマンド引数形式に変更する

まとめ

WordPress REST API + Node.js + アプリケーションパスワードの組み合わせで、 Node.jsから直接WordPressへの記事投稿が可能になる。 定型フォーマットの記事(俳句・川柳・日報など)の投稿自動化に特に有効。

コメント

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