どうも。Reoです。
ブログをWordPressからFreshに移行した際に、WordPressの記事を全てマークダウン形式のファイルに変換をしました。今回は、そのときに使ったツールの紹介をしていこうと思います。
使ったツール
素晴らしいツールの開発をありがとうございます。
まずはそのまま使ってみる
WordPressの記事をMarkdown形式に一括出力する方法の記事を参考に、wordpress-export-to-markdownを そのまま使ってみます。
WordPressの記事をxmlでエクスポートする
マークダウンに変換したい対象のWordPressのダッシュボードを開き、ツール
> エクスポート
より すべてのコンテンツ
を選択し、エクスポートファイルをダウンロードしましょう。
このエクスポート機能はWordPressに標準搭載されています。
wordpress.2024-11-01.xml
といったファイルがダウンロードされました。
Node.jsをインストールする
Node.jsのインストールは以下の記事を見て nvm
を使ってインストールしました。
nvm(Node Version Manager)を使ってNode.jsをインストールする手順
ツールを使ってみる
wordpress.2024-11-01.xml
ファイル- Node.js が動く環境
この2つを用意してwordpress-export-to-markdownを使う準備ができました。
wordpress.2024-11-01.xml
は export.xml
という名前にリネームし、デスクトップ上に wp-to-md
というディレクトリを作り、その中にいれました。
ターミナルを開き、npx wordpress-export-to-markdown
を実行してみましょう。
$ cd ~/Desktop/wp-to-md
$ npx wordpress-export-to-markdown
実行したら以下の項目に回答していきましょう。
- Path to WordPress export file? (WordPressのXMLファイル名。)
- Path to output folder? (出力先フォルダー名。デフォルトはoutput)
- Create year folders? (投稿年のフォルダーを作る?)
- Create month folders? (投稿月のフォルダーを作る?)
- Create a folder for each post? (投稿ごとにフォルダーを作る?)
- Prefix post folders/files with date? (ファイル名の先頭に投稿日時をつける?)
- Save images attached to posts? (添付画像を保存する?)
- Save images scraped from post body content? (imgタグで挿入した画像を保存する?)
- Include custom post types and pages? (カスタム投稿タイプやページも含める?)
回答が終わったら、マークダウンへの変換が始まります。
ファイルを確認する
output
ディレクトリを確認してみましょう。先ほどの場合では、~/Desktop/wp-to-md/output
にできています。
ここまで非常に簡単で、すごく感動した記憶があります。本当にありがたいツールです...
Obsidian で確認してみる
私の目標は、Obsidian上で記事を書くことでした。(# ブログをWordPressからFreshに移行した過程の紹介②【技術選定と記事の移行】)
何が足りていないのかを確認するために、Obsidianの保管庫として output
ディレクトリを開いてみました。
足りない部分を書き出してみる
- 追記データがない
- 自作のプラグインでカスタムフィールドとして実装していたので要対応
- コメントがない
- 投稿に紐づいて
comments
フォルダに欲しい。
- 投稿に紐づいて
slug
が欲しい- メタデータに新しい項目を追加する必要がある。
- 画像のsrcを一括で変換したい
- がんばりました。
- 参考リンクがない
- 1と同様にカスタムフィールドだったので要対応
- アプリのプライバシーポリシーがWordPress上にあるのでそれの移行が必要
- 手動で対応。
- app-ads.txt も移行しないといけない
- 手動で対応。Freshの
static
フォルダにぶち込んでいます。
- 手動で対応。Freshの
実際にコンテンツのみを移行させても、 ads.txt
や app-ads.txt
といったファイルをWordPressの外に配置していた場合には、コンテンツには含まれていないので注意が必要です。
ツールをカスタマイズ
目標は、先ほど書き出した足りない部分の全てを満たすことです。
今回の記事では、wordpress-export-to-markdownをカスタマイズする方法を順に紹介していきます。
- マークダウンのメタ情報に
slug
等の追加項目を用意する方法 - 画像の
src
を一括で置き換える方法 - コメントの書き出し方法
- カスタムフィールドで作られた追記を別ファイルで保存する方法
- 参考リンク(カスタムフィールド)があった場合にcontentの最後に追記する方法
プライベートフォーク
自作プラグインのカスタムフィールドの値をエクスポートしたかったので、特に他の人には需要がないだろう...と思い、プライベートでフォークしました。
--mirror
だと全てのブランチが取れるけど、単に clone
してリモートを変更してあげるだけでもよかったかもしれないですね。
# 作業ディレクトリに移動
cd ~/Tools
git clone --mirror https://github.com/lonekorean/wordpress-export-to-markdown.git
git push --mirror git@github.com:自分のユーザ名/wordpress-export-to-markdown.git
# 作業ディレクトリに移動
cd ~/Tools/wordpress-export-to-markdown
GitHub の Public リポジトリを Private に Fork する
クローンができたら、実際に実行してみましょう。
# 作業ディレクトリに移動
cd ~/Tools/wordpress-export-to-markdown
# 実行
npm install && node index.js
これにより、先ほど npx wordpress-export-to-markdown
で実行した処理と同じ処理が実行されます。
コードを理解する
まずはざっくりとコード全体を読んでみましょう。
npm install && node index.js
で index.js
が呼ばれているので、これがメインファイルです。
wizard
, parser
, writer
の3つが使われています。
それぞれを順に呼び出して、処理を行っています。
// ターミナル上からユーザーが設定値を入力できるようにする
const config = await wizard.getConfig(process.argv);
// XML形式のファイルをマークダウンファイルに翻訳する
const posts = await parser.parseFilePromise(config)
// 設定に応じてフォルダに書き込みを行う
await writer.writeFilesPromise(posts, config);
ユーザーインプットで設定値を変更する wizard
については触りませんでした。
wizard.js
を見ると、先ほど実行した時に聞かれた質問が設定されています。(年月フォルダを作るかどうかなど)
また、src/settings.js
の中には、frontmatter_fields
という設定があり、これらにマークダウンのメタ情報がかかれているのがわかります。
exports.frontmatter_fields = [
'title',
'date',
'categories',
'tags',
'coverImage'
];
さらにこれらに対応したファイルが src/frontmatter/title.js
といった風に用意されていることがわかります。
マークダウンのメタ情報に追加項目を用意する
なるべく簡単なものからやっていきましょう。
マークダウンのメタ情報を以下のようにしていこうと思います。
slug
と publishedDate
と updatedDate
を追加します。またdate
は削除します。
src/settings.js
に定義
①src/settings.js
の中にあるfrontmatter_fields
の中にキーの設定を行います。
この中に書いたものは、上の画像のようにメタ情報のキーとして利用されます。
exports.frontmatter_fields = [
'title',
'slug', // 追加
'publishedDate', // 追加
'updatedDate', // 追加
'categories',
'tags',
'coverImage'
];
src/frontmatter/
にファイルを追加
② 続いて、src/frontmatter/
以下に各キーと同名のファイルを作成します。
スラッグはすでに用意されていますね。
src/frontmatter/slug.js
// 投稿の slug を取得する
module.exports = (post) => {
return post.meta.slug;
};
publishedDate.js
と updatedDate.js
はそれぞれ、date.js
をコピーして作成しました。
またその他のファイルを作成したい場合には src/frontmatter/example.js
をコピーして作ると良いです。(このファイルを用意してくれているのもとてもすごい...)
実行してみる
変更をしたら保存をして、再度実行してみましょう。
# 作業ディレクトリに移動
cd ~/Tools/wordpress-export-to-markdown
# 実行
npm install && node index.js
これらのファイルのメタ情報に先ほど追加した情報があれば成功です。
src
を一括で置き換える
画像の 続いて、画像の src
を一括で置き換えます。これは、Freshで使いやすくするために利用します。
<img>
タグの親の<a>
を置き換える
WordPressで書いていた記事には以下のような画像タグが含まれていました。
<a href="https://uruly.xyz/wp-content/uploads/2016/11/hoge.png"><img class="aligncenter size-large wp-image-832" src="https://uruly.xyz/wp-content/uploads/2016/11/hoge.png" alt="サンプル" width="1024" height="437" /></a>
この<img src="">
部分は自動で ポスト名/images/
に入るようになっています。(wizardでの設定による)
a
タグの href
は変わっていなかったので同時に変換するようにします。これは画像をどこに置いているかによってsrc
の値も変わります。(本当は画像サーバーを持っていたほうがいいんだろうな...)
src/translator.js
の function getPostContent(postData, turndownService, config)
に追記します。
function getPostContent(postData, turndownService, config) {
let content = postData.encoded[0];
// 追加
let slug = getPostSlug(postData);
if (config.saveScrapedImages) {
// writeImageFile() will save all content images to a relative /images
// folder so update references in post content to match
content = content.replace(/(<img[^>]*src=").*?([^/"]+\.(?:gif|jpe?g|png))("[^>]*>)/gi, '$1images/$2$3');
// ここにaタグの置き換えも追加
content = content.replace(/(<a[^>]*href=").*?([^/"]+\.(?:gif|jpe?g|png))("[^>]*>)/gi, `$1images\/${slug}\/$2$3`);
}
// 以下略
}
/// 投稿のslugを返す
function getPostSlug(postData) {
return decodeURIComponent(postData.post_name[0]);
}
リンクに投稿slugを利用したので1つ関数を追加しました。
これにより以下のように変換できました。
// 変換前
<a href="https://uruly.xyz/wp-content/uploads/2016/11/hoge.png"><img class="aligncenter size-large wp-image-832" src="https://uruly.xyz/wp-content/uploads/2016/11/hoge.png" alt="サンプル" width="1024" height="437" /></a>
// 変換後
<a href="images/post_slug/hoge.png"><img src="images/hoge.png" alt="サンプル" width="1024" height="437 /></a>
getPostContent(postData, turndownService, config)
では、まだhtmlタグの状態で置き換えているだけなので、置き換え先はhtmlタグになります。
caption を変換
WordPressには標準で [caption]
というショートコードがあります。これがそのまま変換されずにマークダウンに書き出されていたので、それも修正します。
上記の getPostContent(postData, turndownService, config)
に追記します。
function getPostContent(postData, turndownService, config) {
let content = postData.encoded[0];
// 追加
let slug = getPostSlug(postData);
if (config.saveScrapedImages) {
// writeImageFile() will save all content images to a relative /images
// folder so update references in post content to match
content = content.replace(/(<img[^>]*src=").*?([^/"]+\.(?:gif|jpe?g|png))("[^>]*>)/gi, '$1images/$2$3');
// ここにaタグの置き換えも追加
content = content.replace(/(<a[^>]*href=").*?([^/"]+\.(?:gif|jpe?g|png))("[^>]*>)/gi, `$1images\/${slug}\/$2$3`);
// [caption] ショートコードの変換
content = content.replace(/\[caption[^\]]*\](<a[^>]*href=")([^"]+)("><img[^>]*src=")([^"]+)"[^>]*alt="([^"]*)".*?\>(.*?)\[\/caption\]/gi,
(match, linkStart, linkUrl, imgStart, imgUrl, altText, captionText) => {
return `<figure>
${linkStart}${linkUrl}${imgStart}${imgUrl}" alt="${altText}"></a>
<figcaption>${captionText.trim()}</figcaption>
</figure>`;
});
}
// 以下略
}
これにより [caption]
を<figure>
タグと<figcaption>
の形に変換しています。
雑ですがこんな感じに変換処理を追記する形で頑張りました。
コメントを取得
これは相当重かったです。がんばりました。
欲しい形
コメントファイルは以下のような形のマークダウンファイルにします。
タイトルは2018-11-20-03-11.md
といった日付にしました。
またコメントは各投稿のフォルダ内のcomments
フォルダに入るようにします。
この場合は 【swift-3】calayerを用いて図形を移動・拡大縮小してみた
というフォルダに入ります。
---
slug: "【swift-3】calayerを用いて図形を移動・拡大縮小してみた"
commentAuthor: "Reo(管理人)"
commentEmail: "めーるあどれす"
commentDate: "2018-11-19T18:11:12.000Z"
---
コメントありがとうございます。お役に立てたようで嬉しいです!
コピーする
それぞれファイルをコピーしてコメント用にファイルを作りました。
index.js
をコピーしてcomments.js
をつくるparser.js
をコピーしてcommentsParser.js
をつくるwriter.js
をコピーしてcommentsWriter.js
をつくるsettings.js
をコピーしてcommentSettings.js
をつくる
comments.js をつくる
comments.js
ではそれぞれおきかえたファイルを利用するように修正します。
const parser = require('./src/commentsParser');
const writer = require('./src/commentsWriter');
コメントをマークダウンにする時には以下のように実行します。
npm install && node comments.js
commentSettings.js でメタ情報を決める
欲しい形をもとに、各キーを作ります。
exports.frontmatter_fields = [
'slug',
'commentAuthor',
'commentURL',
'commentEmail',
'commentDate'
];
これらに応じたファイルを src/frontmatter
以下に用意します。
slug
は先ほどのものを利用します。
src/frontmatter/commentAuthor.js
module.exports = (post) => {
return post.meta.commentAuthor;
};
src/frontmatter/commentURL.js
module.exports = (post) => {
return post.meta.commentURL;
};
src/frontmatter/commentEmail.js
module.exports = (post) => {
return post.meta.commentEmail;
};
src/frontmatter/commentDate.js
module.exports = (post) => {
return post.meta.commentDate;
};
次で紹介するcommentsParser
にて post.meta
にコメントの情報が含むようにしているので、簡単ですね。
commentsParser.js を編集する
commentsParser.js
をコメント用に編集します。
設定ファイルを先ほど編集したcommentSettings.js
を使うように指定します。
// commentSettingsをつかう
const settings = require('./commentSettings');
続いて、collectPosts
関数を編集して collectComments
にします。
function collectComments(channelData, postTypes, config) {
const turndownService = translator.initTurndownService();
let allPosts = [];
postTypes.forEach(postType => {
const postsForType = getItemsOfType(channelData, postType)
.filter(postData => postData.status[0] !== 'trash' && postData.status[0] !== 'draft')
// コメントを含むもののみをフィルタリング
.filter(postData => postData.comment)
// pingbackを除く
.filter(postData => postData.comment.some(comment => comment.comment_type[0] !== 'pingback'))
.flatMap(postData =>
{
const comments = postData.comment.flatMap(comment => {
return {
// frontmatter で使われる生のpostData
data: postData,
meta: {
id: getPostId(postData),
date: getPostDate(postData),
slug: getPostSlug(postData),
type: postType,
commentAuthor: getCommentAuthor(comment),
commentDate: getCommentDate(comment),
commentURL: getCommentURL(comment),
commentEmail: getCommentEmail(comment),
imageUrls: []
},
// マークダウンに変換済みのcontent
content: translator.getCommentContent(postData, getCommentContent(comment), turndownService, config)
}
});
return comments;
}
);
if (postTypes.length > 1) {
console.log(`${postsForType.length} "${postType}" posts found.`);
}
allPosts.push(...postsForType);
});
if (postTypes.length === 1) {
console.log(allPosts.length + ' posts found.');
}
return allPosts;
}
postData.comment
の中にコメントの情報が入っています。pingback
を除いたコメントを変換します。
postData.comment
の中身は以下のようになっています。pingbackのものでわかりづらくて失礼...
/*
postData.comment の中身
comment_id: [ '173' ],
comment_author: [ '2019年を振り返ってみるぞい | うるおいらんど' ],
comment_author_email: [ '' ],
comment_author_url: [ 'https://uruly.xyz/2019-end/' ],
comment_author_IP: [ '183.90.228.27' ],
comment_date: [ '2019-12-31 03:24:42' ],
comment_date_gmt: [ '2019-12-30 18:24:42' ],
comment_content: [ '[…] さてさて、2019年の抱負を年初めに書いたので、抱負を達成できたか見ていきます。 […]' ],
comment_approved: [ '1' ],
comment_type: [ 'pingback' ],
comment_parent: [ '0' ],
comment_user_id: [ '0' ],
commentmeta: [ [Object], [Object], [Object] ]
*/
meta
には、frontmatter
からアクセスしたい情報を入れておきます。
function getCommentAuthor(comment) {
return comment.comment_author[0];
}
function getCommentURL(comment) {
return comment.comment_author_url[0];
}
function getCommentDate(comment) {
const dateText = comment.comment_date[0];
const dateTime = luxon.DateTime.fromFormat(dateText, 'yyyy-MM-dd HH:mm:ss', { zone: 'utc' });
if (settings.custom_date_formatting) {
return dateTime.toFormat(settings.custom_date_formatting);
} else if (settings.include_time_with_date) {
return dateTime.toISO();
} else {
return dateTime.toISODate();
}
}
function getCommentEmail(comment) {
return comment.comment_author_email[0];
}
これらはfrontmatter
のファイル側に定義するでもいいかもしれませんね...
commentsWriter.js を編集する
最後に commentsWriter.js を編集します。
書き換えるのはほんの一部です。function getPostPath(post, config)
の中身を編集して、コメントを書き出す場所を指定します。
function getPostPath(post, config) {
let dt;
if (settings.custom_date_formatting) {
// 1. この日付を `post.meta.date`にする
dt = luxon.DateTime.fromFormat(post.meta.date, settings.custom_date_formatting);
} else {
dt = luxon.DateTime.fromISO(post.meta.date);
}
// start with base output dir
const pathSegments = [config.output];
// create segment for post type if we're dealing with more than just "post"
if (config.includeOtherTypes) {
pathSegments.push(post.meta.type);
}
if (config.yearFolders) {
pathSegments.push(dt.toFormat('yyyy'));
}
if (config.monthFolders) {
pathSegments.push(dt.toFormat('LL'));
}
// create slug fragment, possibly date prefixed
let slugFragment = post.meta.slug;
if (config.prefixDate) {
slugFragment = dt.toFormat('yyyy-LL-dd') + '-' + slugFragment;
}
// use slug fragment as folder or filename as specified
const commentDate = luxon.DateTime.fromISO(post.meta.commentDate);
// 2. comments ディレクトリを追加する
pathSegments.push(slugFragment, 'comments');
// 3. コメントされた日付をファイル名にする
pathSegments.push(`${commentDate.toFormat('yyyy-LL-dd-hh-mm')}.md`);
return path.join(...pathSegments);
}
3箇所書き換えました。
これにより各投稿のディレクトリ以下にcomments
ディレクトリができるようになりました。
└── post
├── slug1
│ ├── index.md
│ ├── images
│ | └── hoge.png
| └── comments
| ├── 2024-11-10-10-40.md
| └── 2024-11-11-03-20.md
不要なコードを削除する
コメント内には画像がなかったので、commentsParser.js
内の collectAttachedImages
などの関数は削除しています。
最後にnpm install && node comments.js
をしました。npm install && node index.js
で output
ディレクトリがある状態で実行すると、いい感じにマージされます。
追記を別ファイルで保存する
WordPressで記事を書いている時に、追記を書くためのフィールドを自作プラグインで用意していました。
カスタムフィールドで作られた追記を additions.md
という別ファイルにして保存していきます。
欲しい形
先ほどのコメントと同様にadditions
フォルダを作ってその中に入れます。
投稿のslug/additions/2018-04-13.md
---
slug: "【swift-3】calayerを用いて図形を移動・拡大縮小してみた"
additionDate: "2018-04-13"
additionTitle: "gistにあげました〜"
---
一応Swift4に対応したものをgistにあげました。
<script src="https://gist.github.com/uruly/43dfda5790fb1bf37b6f15077026f4b4.js"></script>
と言っても@objcをつけただけなんですけどね。 あとdelegateのところをextensionに移しただけです。
Swift3からSwift4へは簡単に移行できて楽チン。
コメントと同様に
コメントと同様に parser
, writer
, settings
ファイルをコピーして実装しました。
だいぶ同じなので、ポイントだけざっくりと書いておきます。
exports.frontmatter_fields = [
'slug',
'additionDate',
'additionTitle'
];
カスタムフィールドは少し探すのが大変です。
src/frontmatter/additonDate.js
const luxon = require('luxon');
const settings = require('../additionSettings');
// 追記の日時
module.exports = (post) => {
const additionDateMeta = post.data.postmeta.find(meta => meta.meta_key[0] === 'addition_time');
// 見つかった場合は meta_value を返す。見つからない場合は空文字を返す。
if (additionDateMeta) {
const dateText = additionDateMeta.meta_value[0];
const date = new Date(dateText); // Date オブジェクトを作成
const dateTime = luxon.DateTime.fromJSDate(date, { zone: 'utc' }); // DateオブジェクトをDateTimeに変換
if (settings.custom_date_formatting) {
return dateTime.toFormat(settings.custom_date_formatting); // カスタムフォーマットで返す
} else if (settings.include_time_with_date) {
return dateTime.toISO(); // ISO形式で返す
} else {
return dateTime.toISODate(); // 日付のみのISO形式で返す
}
} else {
return '';
}
};
src/frontmatter/additonTitle.js
// 追記のタイトル
module.exports = (post) => {
const additionTitleMeta = post.data.postmeta.find(meta => meta.meta_key[0] === 'addition_title');
// 見つかった場合は meta_value を返す。見つからない場合は空文字を返す。
return additionTitleMeta ? additionTitleMeta.meta_value[0] : '';
};
src/additionParser.js
で追記を集めます。
function collectAdditions(channelData, postTypes, config) {
// this is passed into getPostContent() for the markdown conversion
const turndownService = translator.initTurndownService();
let allPosts = [];
postTypes.forEach(postType => {
const postsForType = getItemsOfType(channelData, postType)
.filter(postData => postData.status[0] !== 'trash' && postData.status[0] !== 'draft')
// 追記を含むもののみをフィルタリング
.filter(postData => postData.postmeta.some(meta => meta.meta_key[0] === 'addition_notes'))
.map(postData => ({
// raw post data, used by frontmatter getters
data: postData,
// meta data isn't written to file, but is used to help with other things
meta: {
id: getPostId(postData),
date: getPostDate(postData),
slug: getPostSlug(postData),
additionDate: getAdditionDate(postData),
type: postType,
imageUrls: [] // possibly set later in mergeImagesIntoPosts()
},
// contents of the post in markdown
content: translator.getAdditionContent(postData, getAdditionContent(postData), turndownService, config)
}));
if (postTypes.length > 1) {
console.log(`${postsForType.length} "${postType}" posts found.`);
}
allPosts.push(...postsForType);
});
if (postTypes.length === 1) {
console.log(allPosts.length + ' posts found.');
}
return allPosts;
}
content もカスタムフィールドになっているので、以下のように取得しています。
function getAdditionContent(postData) {
// postmeta の中から meta_key が 'addition_notes' である要素を探す
const additionNotesMeta = postData.postmeta.find(meta => meta.meta_key[0] === 'addition_notes');
// 見つかった場合は meta_value を返す。見つからない場合は空文字を返す。
return additionNotesMeta ? additionNotesMeta.meta_value[0] : '';
}
あとはコメントと同じ感じで書き出しまで行いました。
参考リンクをコンテンツの最後に追記
WordPressで記事を書いている時に、reference_list
というカスタムフィールドを作っていました。これは <a href=""></a>
を羅列すると、参考リンクとして表示されるというプラグインでした。(自作プラグイン)
追記のように別ファイルにしても良かったんですが、そういう運用をもうしないかなと思い、単にコンテンツの最後に ## 参考リンク
というセクションで表示させるように変更しました。
マークダウンに変換前のcontentに追加
src/translator.js
内の function getPostContent(postData, turndownService, config)
内に、参考リンクのセクションを追加していきます。
これは先ほど、画像の変換処理を書いた場所です。
function getPostContent(postData, turndownService, config) {
let content = postData.encoded[0];
let slug = getPostSlug(postData);
const referenceListMeta = postData.postmeta.find(meta => meta.meta_key[0] === 'reference_list');
if (referenceListMeta) {
const referenceTitle = '\n<h2>参考リンク</h2>\n';
const referenceLink = referenceListMeta.meta_value[0].split('\n');
content += referenceTitle;
content += '<ul>'
referenceLink.forEach(element => {
content += '<li>' + element + '</li>';
});
content += '</ul>'
}
// 先述の画像変換処理
}
この関数内ではhtmlで content
を扱っているので、htmlタグに変換します。
おわりに
素晴らしいツールのおかげで、このブログをマークダウン化することができました。
改めて wordpress-export-to-markdownに感謝いたします。
とても読みやすいコードだったのでカスタマイズもしやすかったです。
コメントや追記など、自分が大事にしたいものも無事に持ってくることができました。
以前、全てのSwiftの記事に追記を書く!ということをやったのが無駄にならなくてよかったです。追記は Swift3からSwift4対応をしたものが多かったので、今はもうそれも古いんですけどね...(さらにはgistが動いていないのでそこは手動で直さないと👊)
ざっくりとした記事になってしまいましたが、この記事が何かの役に立つと幸いです。
👋
コメントはありません。
現在コメントフォームは工事中です。