WordPress の記事を Markdown に変換した

09 May 2020
#WordPress#GatsbyJS#MySQL#Go

WordPress から GatsbyJS に記事を移行したときの手順を記します。

GatsbyJS は Markdown で書かれた記事からよしなにブログを生成してくれるので、WordPress の記事を Markdown に変換する必要がありました。 Markdown には HTML を書けるので記事の内容そのものは WordPress からコピペすれば良いのですが、記事のメタデータ(投稿日時やタグなど)も適切にコピペしないといけないのが面倒だったのでスクリプトを書きました。

前提

WordPress をバックアップするプラグイン BackWPup で作成されたデータベースのダンプファイル 4423_wp.sql が手元にあるとします。

1. データベースを復元する

手元の MySQL にデータをインポートします。 データベース名は wp としました。

$ mysql -uroot
mysql> create database wp
mysql> \q
$ mysql -uroot wp < 4423_wp.sql

テーブルはこんな感じです。

mysql> show tables;
+-----------------------+
| Tables_in_wp          |
+-----------------------+
| wp_commentmeta        |
| wp_comments           |
| wp_filemeta           |
| wp_links              |
| wp_options            |
| wp_postmeta           |
| wp_posts              |
| wp_term_relationships |
| wp_term_taxonomy      |
| wp_terms              |
| wp_usermeta           |
| wp_users              |
| wp_wpmm_subscribers   |
+-----------------------+

2. データベースから記事を書き出す

MySQL に接続して全ての記事を Markdown 形式で出力するスクリプトを Go で書きました。

package main

import (
	"database/sql"
	"fmt"
	"os"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
)

type Post struct {
	ID      uint64
	Date    time.Time
	Title   string
	Content string
	Tags    []string
}

func main() {
	db, err := sql.Open("mysql", "root:@/wp?parseTime=true")
	if err != nil {
		panic(err.Error())
	}
	defer db.Close()

	q := `SELECT ID, post_date, post_title, post_content 
		  FROM wp_posts 
		  WHERE post_status = 'publish' AND post_type = 'post'`
	rows, err := db.Query(q)
	if err != nil {
		panic(err.Error())
	}
	defer rows.Close()

	for rows.Next() {
		var post Post
		err := rows.Scan(&post.ID, &post.Date, &post.Title, &post.Content)
		if err != nil {
			panic(err.Error())
		}
		post.Tags = fetchTags(db, post.ID)
		writePost(&post)
	}
}

func fetchTags(db *sql.DB, postID uint64) []string {
	q := `SELECT t.name FROM wp_terms AS t 
		  JOIN wp_term_relationships AS r 
		  	ON r.object_id = %d 
		  JOIN wp_term_taxonomy AS tax
		  	ON tax.term_taxonomy_id = r.term_taxonomy_id
		  	AND t.term_id = tax.term_id`
	rows, err := db.Query(fmt.Sprintf(q, postID))
	if err != nil {
		panic(err.Error())
	}
	defer rows.Close()

	var tags []string
	for rows.Next() {
		var tag string
		rows.Scan(&tag)
		tags = append(tags, tag)
	}
	return tags
}

func writePost(post *Post) {
	dateJST := getDateJST(post)
	filename := fmt.Sprintf("%s-%d.md", dateJST.Format("20060102"), post.ID)

	file, err := os.Create("./posts/" + filename)
	if err != nil {
		panic(err.Error())
	}
	defer file.Close()

	file.WriteString("---\n")
	file.WriteString(fmt.Sprintf("title: \"%s\"\n", post.Title))
	file.WriteString(fmt.Sprintf("path: \"/%d\"\n", post.ID))
	file.WriteString(fmt.Sprintf("date: \"%s\"\n", dateJST.Format("2006-01-02")))
	file.WriteString(fmt.Sprintf("excerpt: \"%s\"\n", getExcerpt(post)))
	file.WriteString(fmt.Sprintf("tags: [%s]\n", getTags(post)))
	file.WriteString("---\n\n")
	file.WriteString(post.Content)
}

func getDateJST(post *Post) time.Time {
	return post.Date.In(time.FixedZone("Asia/Tokyo", 9*60*60))
}

func getExcerpt(post *Post) string {
	index := strings.Index(post.Content, "<!--more-->")
	if index >= 0 {
		subst := string(post.Content[0:index])
		return strings.TrimSpace(strings.Replace(subst, "\r\n", "", -1))
	}
	return post.Content
}

func getTags(post *Post) string {
	quotedTags := make([]string, len(post.Tags))
	for i, t := range post.Tags {
		quotedTags[i] = "\"" + t + "\""
	}
	return strings.Join(quotedTags, ", ")
}

このスクリプトは

---
title: "WordPress の記事を Markdown に変換した"
path: "/wp2md"
date: "2020-05-09"
excerpt: "WordPress から GatsbyJS に記事を移行したときの手順を記します。"
tags: ["WordPress", "GatsbyJS", "MySQL", "Go"]
---

記事の内容(HTML)

のようなフォーマットで 20200509-wp2md.md というファイルを出力します。

ただし、path の部分を上記のようないい感じの文字列にするのは大変なので(もともと WordPress をそういうパスに設定していないので)とりあえず記事の ID にしています。

おわりに

記事テーブルには改訂の履歴も記録されていたりして、データベースの仕様を把握する必要があったので、思ったより面倒でした。 記事が20件ぐらいしかないなら愚直にコピペしたほうがいいです。

4423.ch