[Astro] WordPressエクスポート(WXR)を turndown でMarkdownに変換する

Tech
[Astro] WordPressエクスポート(WXR)を turndown でMarkdownに変換する

WordPress から Astro へ移行するとき、一番のヤマは既存記事の移行です。 WordPress の本文は HTML、Astro(Content Collections)の記事は Markdown なので、変換が必要になります。

ここでは、WordPress のエクスポート(WXR = XML)を読み込み、HTML 本文を Markdown に変換して YYYY-MM-DD-slug.md を吐き出すところまでをやります。

使うもの

  • fast-xml-parser — WXR(XML)のパース
  • turndown — HTML → Markdown 変換
  • turndown-plugin-gfm — テーブルや打ち消し線など GFM 対応
npm i -D fast-xml-parser turndown turndown-plugin-gfm

WXR を書き出す

WordPress 管理画面の ツール > エクスポート から「投稿」を選んでダウンロードします。 xxxxx.WordPress.YYYY-MM-DD.xml の中に、各記事が <item> として入っています。本文は content:encoded に HTML で入っています。

<item>
  <title><![CDATA[記事タイトル]]></title>
  <wp:post_id>519</wp:post_id>
  <wp:post_date><![CDATA[2021-11-14 10:00:00]]></wp:post_date>
  <wp:post_name><![CDATA[apache-force-https]]></wp:post_name>
  <wp:status><![CDATA[publish]]></wp:status>
  <wp:post_type><![CDATA[post]]></wp:post_type>
  <category domain="category" nicename="tech"><![CDATA[Tech]]></category>
  <content:encoded><![CDATA[<p>本文のHTML...</p>]]></content:encoded>
</item>

変換スクリプト

公開済み(status === 'publish')の投稿だけを対象に、HTML を Markdown へ変換しながらファイルを生成します。

import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { XMLParser } from 'fast-xml-parser';
import TurndownService from 'turndown';
import { gfm } from 'turndown-plugin-gfm';

// 1) WXR を読み込む
const xml = readFileSync('export.xml', 'utf-8');
const parser = new XMLParser({ ignoreAttributes: false, cdataPropName: '__cdata' });
const data = parser.parse(xml);

// channel.item は単数だと配列にならないので正規化
const items = [].concat(data.rss.channel.item ?? []);

// 2) HTML -> Markdown 変換器
const td = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
td.use(gfm);

mkdirSync('src/content/blog', { recursive: true });

for (const item of items) {
  if (item['wp:post_type'] !== 'post') continue;
  if (item['wp:status'] !== 'publish') continue;

  const title = String(item.title ?? '').trim();
  const date = String(item['wp:post_date'] ?? '').slice(0, 10); // YYYY-MM-DD
  const slug = String(item['wp:post_name'] ?? '');
  const html = item['content:encoded'] ?? '';

  const markdown = td.turndown(String(html));

  // 3) frontmatter を付けて書き出す
  const fm = [
    '---',
    `title: ${JSON.stringify(title)}`,
    `pubDate: ${date}`,
    'category: tech', // カテゴリは後で振り分ける
    'tags: []',
    'thumbnail: /images/no-image.png',
    '---',
    '',
  ].join('\n');

  const fileName = `src/content/blog/${date}-${slug}.md`;
  writeFileSync(fileName, fm + markdown + '\n', 'utf-8');
  console.log('wrote', fileName);
}

実行します。

node convert.mjs

ハマりどころ・メモ

  • item は単数だと配列にならない。fast-xml-parser は要素が1つだと配列ではなくオブジェクトを返すので、[].concat(...) で必ず配列化しておくと安全。
  • コードブロックは WordPress 側のプラグイン次第で <pre> だったり独自タグだったりするので、変換後に手直しが要ることが多い。technical なブログだと特にここは目視で確認推奨。
  • 画像は本文に絶対URLで入っていることが多い。/images/... にダウンロードして置き換えるか、そのまま外部参照にするか方針を決めておく。
  • slug が日本語だった場合は、wp:post_name が URL エンコードされた文字列になっていることがある。英語スラッグを振り直すなら、ファイル名だけ自分で決めてしまうのも手。
  • カテゴリ(<category domain="category">)も拾えるが、移行を機に整理したいなら一旦 tech などに寄せておいて後で振り分けるのが楽だった。

最初に変換でざっくり Markdown 化 → あとは手元で目視チェック、という流れが結局いちばん早かったです。

Astro, WordPress, Node.js