はてなブログの記事を Hugo に移行するツールを作ってみた
2020-12-22はてなブログで書いていたブログを Hugo で作り直したいと思い、 Go の勉強も兼ねて簡単な移行ツールを作ってみました。
目次
概要
はてなブログに投稿していた記事を Hugo 用の Markdown ファイルとしてエクスポートするツールを作りました。
経緯
このブログとは別に、趣味のバイクに関する記事をはてなブログで書いています。(最後の更新は一年以上前ですが…)
はてなブログに移行する前は WordPress で書いていたため、独自ドメインをそのまま使うためにはてなブログ Pro を契約して使っています。そのはてなブログ Pro の更新が来年の 1月末ということで、それまでに移行したいなと思っていたのが経緯です。
AtomPub を利用して記事を参照する
はてなブログには AtomPub を利用して記事の参照、投稿、編集、削除を行うことができます。
AtomPub を利用するには OAuth 認証、WSSE認証、Basic認証 のいずれかの方法で認証を行う必要がありますが、今回は Basic 認証で利用してみます。 Basic 認証の ID は はてなID で、 パスワードは API キー を使用します。この API キー については、はてなブログ管理画面の 設定 > 詳細設定 の AtomPub APIキー で確認することができます。
AtomPub の詳しい仕様については下記ページに書かれているので、ここでは記事の参照についてのみ触れます。
参照用エンドポイントにアクセスして XML を取得
記事一覧を参照するには、下記のエンドポイントにアクセスします。
https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry/
このエンドポイントについては、はてなブログ管理画面の 設定 > 詳細設定 にある AtomPub ルートエンドポイント で確認できます。
レスポンスは ContentType: application/atom+xml
で返却されます。
今回は Basic 認証を用いてアクセスするので、 Go では次のようにして XML を取得します。(import
文は省略してます)
func main() {
xmlData, err := getXML(next)
if err != nil {
fmt.Println(err)
break
}
}
func getXML(url string) (string, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
var basicAuthZ string = generateBasicAuthZ()
req.Header.Set("Authorization", "Basic "+basicAuthZ)
client := new(http.Client)
res, err := client.Do(req)
if err != nil {
return "", err
}
if res.StatusCode != 200 {
return "", fmt.Errorf("Failed to get xml. [%s]", res.Status)
}
body, _ := ioutil.ReadAll(res.Body)
defer res.Body.Close()
return string(body), nil
}
func generateBasicAuthZ() string {
raw := hatenaId + ":" + hatenaAPIKey
bytes := []byte(raw)
return base64.StdEncoding.EncodeToString(bytes)
}
取得できる XML の内容は下記のようになっています。
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app">
<link rel="first" href="{参照用のルートエンドポイント}" />
<link rel="next" href="{次のページの参照用エンドポイント}" />
<title>{ブログタイトル}</title>
<subtitle>{ブログのサブタイトル}</subtitle>
<link rel="alternate" href="{ブログの URL}"/>
<updated>2019-06-14T17:41:59+09:00</updated>
<author>
<name>{はてな ID}</name>
</author>
<generator uri="https://blog.hatena.ne.jp/" version="4d0fe3f3cd3000000000020000000000">Hatena::Blog</generator>
<id>hatenablog://blog/00000000000000000000</id>
<entry>
...
</entry>
<entry>
...
</entry>
</feed>
また、 <entry>
の中は下記のようになっています。
<entry>
<id>tag:blog.hatena.ne.jp,2013:blog-{はてな ID}-00000000000000000000-00000000000000000</id>
<link rel="edit" href="{個別記事を参照するエンドポイント}"/>
<link rel="alternate" type="text/html" href="{記事の URL}"/>
<author><name>{はてな ID}</name></author>
<title>{記事タイトル}</title>
<updated>2020-12-22T00:03:46+09:00</updated>
<published>2020-12-21T21:00:58+09:00</published>
<app:edited>2020-12-22T00:03:46+09:00</app:edited>
<summary type="text">{記事の概要}</summary>
<content type="text/x-markdown">
...
</content>
<hatena:formatted-content type="text/html" xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#">
...
</hatena:formatted-content>
<category term="{カテゴリ名1}" />
<category term="{カテゴリ名2}" />
<app:control>
<app:draft>yes</app:draft>
</app:control>
</entry>
これを Go の構造体に落とし込みます。
Go で XML をパースする
今回は XML をパースするために、 encoding/xml
パッケージを使用します。
パースする前に、 XML の構造に合わせて次のような構造体を定義します。
type Atom struct {
Title string `xml:"title"`
SubTitle string `xml:"subtitle"`
Links []Link `xml:"link"`
Entries []Entry `xml:"entry"`
}
type Entry struct {
Links []Link `xml:"link"`
Author string `xml:"author>name"`
Title string `xml:"title"`
Published string `xml:"updated"`
Content string `xml:"content"`
Summary string `xml:"summary"`
Draft string `xml:"control>draft"`
Categories []Category `xml:"category"`
}
type Category struct {
Term string `xml:"term,attr"`
}
type Link struct {
Rel string `xml:"rel,attr"`
Href string `xml:"href,attr"`
}
xml:"title"
のようにタグを指定することで、 XML 内の要素と関連付けることができます。また、 xml:"term,attr"
のようにタグを指定することで、 XML の各要素の属性と関連付けることができます。
実際にパースする部分は下記のとおりです。
func main() {
// ...
atom := Atom{}
xerr := xml.Unmarshal([]byte(xmlData), &atom)
if xerr != nil {
fmt.Println(xerr.Error())
break
}
fmt.Println(atom.Title)
fmt.Println(atom.SubTitle)
fmt.Println("-----")
for _, entry := range atom.Entries {
fmt.Println(entry.Title)
}
}
上記のような実装で、 XML の情報を構造体に落とし込むことができました。
みちのえきまにあ
SC59 CBR1000RRで道の駅巡りをしているまったりツーリングライダーのブログ
-----
test
泊まりのツーリングで初めて車体トラブルが発生した話
SSTR 2019 に参加してきました!
.
.
.
あとは、これらのデータをゴニョゴニョやって、下記のような Hugo 用の Markdown ファイルを生成します。
---
title: "%s"
date: %s
draft: %s
author: ["%s"]
categories: [%s]
archives: ["%s", "%s"]
description: "%s"
url: "/%s"
---
%s
完成したもの
特徴
README
にも書いていますが、特徴を簡単に書いておきます。
- はてなブログのすべての記法 (Markdown/はてな記法/見たまま) に対応しています
- はてな記法で書かれた記事の下記の要素は、それぞれ HTML に変換されます
- リンク文字列
- はてなブログカード
- Twitter 埋め込み
- はてなフォトライフの画像埋め込み
- 下書きの記事は
draft: true
として生成されます - Permalink は
url: "/entry/..."
として生成されます
特に、各記事の URL を Permalink として生成しておくことで、 URL をそのまま引き継ぐことができるようになってます。
ただし注意点としては、Markdown ファイルとして生成するものの、中身は純粋な Markdown ではなく HTML も混じった状態で生成されます。そのため、 Hugo の設定ファイル config.toml
に下記の記述を追加し、記事内で HTML 及び JavaScript のコードを生で記述しても解釈されるようにする必要があります。
[markup]
[markup.goldmark]
[markup.goldmark.renderer]
unsafe = true
また、上記のはてな記法以外のタグ (Amazon の商品リンクなど) は変換されないので、手で直す必要があります。(今後対応するかもしれません)
使い方
使い方は簡単で、上のリポジトリを clone してもらって docker build
して docker run
するだけです。
詳しくは README
を読んでください。
まとめ
はてなブログで書いた記事を Hugo に移行してみた話でした。
Go の勉強も兼ねて作ってみたツールなので、書き方おかしいとかコード汚いとか全然動かないとかあれば issue、PR いただけると嬉しいです。
comments powered by Disqus