[Astro] WordPressエクスポート(WXR)を turndown でMarkdownに変換する
TechWordPress から 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