さようなら@gridsome/source-wordpress。こんにちはMarkdown。

ようやく記事のソースをWordPressからMarkdownにできました。作業の振り返りです。
私の場合はWordPressをかなりシンプルな状態で使っていたのでなんとかなりましたが、ゴリゴリにいろいろな機能を使って記事を書いてる場合はこれだけでは済まないと思います。

記事のMarkdown化

WordPressの記事をMarkdown化するツールはいくつかあったのですが、今回はwordpress-to-hugo-exporterを使用しました。
多少改造したものの、ほぼ正しくMarkdown化できて、かつ、必要な情報をFront Matterに出力できたので大変ありがたかったです。

以下のツールについては残念ながら私のWordPressの記事とは相性が悪かったようで、うまくいきませんでした。

wordpress-to-hugo-exporterの改造

Hugoで使う分には問題ないのですが、Gridsome(の自作のテーマ)で使おうとすると情報が足りないため、Front Matterに出力する内容を変更しました。

function convert_meta を修正します。
https://github.com/SchumacherFM/wordpress-to-hugo-exporter/blob/4d5ded8fa7c9e66a6b0e273bc5242bb1ab9765b0/hugo-export.php#L121
どうせあとで他の箇所も正規表現で置換することになると思っていたので、結構適当です。

       $output = array(
            'title' => html_entity_decode(get_the_title($post), ENT_QUOTES | ENT_XML1, 'UTF-8'),
            'author' => get_userdata($post->post_author)->display_name,
            'type' => get_post_type($post),
            'date' => $this->_getPostDateAsIso($post),
            'excerpt' => get_the_excerpt($post), // excerpt が設定されていない場合は、記事の先頭を抜粋
            'postid' => get_the_ID($post), // 自分のブログでは WordPress の投稿IDをURLに使っていたため、取得が必要
        );
        if (false === empty($post->post_excerpt)) {
            $output['excerpt'] = $post->post_excerpt;
        }
        $output['published'] = true; // source-filesystem では published が記事発行フラグになる
        if (in_array($post->post_status, array('draft', 'private'))) {
             // false にするとキー自体が出力されないので適当な文字にしておく。あとから一括で置換
            $output['published'] = 'ITS_FALSE';
        }
        if ($post->post_status == 'private') {
            $output['published'] = 'ITS_FALSE';
        }

こんな感じのFront Matterになりました。

---
title: WebARENA Indigoの新インスタンスのネットワークが遅い。
author: りゅー
type: post
date: 2021-05-01T15:58:30+00:00
excerpt: WebARENA Indigoでインスタンスの在庫が追加されたのですが、使い物にならないので別のVPSを検討したほうがいいよという記事です。
postid: 1706
published: true
url: /archives/1706
categories:
  - uncategorized

---

Markdownに変換しきれなかった部分を正規表現で置換

出力されたmdの内容を見て、一部はhtmlのままだったため自分で修正しました。
VSCodeだと置換対象と置換後の状態がわかりやすいので、以下のように単純に置換できるものに関しては、すべてVSCodeを使って置換しました。

  • Prosm.jsのコードブロック <pre><code class="language-言語"> -> ```言語
  • コードブロック(<pre><code>)内の文字実体参照・数値文字参照をデコード
  • Front Matterの published: ITS_FALSE -> published: false
  • <figure><figcaption> -> ![Some Text][数字]

    • 補足:wordpress-to-hugo-exporterは、リンクを定義参照リンクの形式で変換します。以下は書式の例です。
    ![画像テキスト][1]
     [1]: https://retrorocket.biz/images/dummy.png

正規表現を使いたくない・正規表現ではどうにもならない場合、wordpress-to-hugo-exporterが同梱しているmarkdownifyを改造する必要があります。私の場合はそこまでする必要はありませんでした。
強いて言うなら、後述するgridsome-remark-figure-captionと定義参照リンクの相性が悪かったので、そこの出力は直せばよかったですね。

gridsome-remark-figure-captionの導入

このままだと、今までfigcaptionに設定していたテキストがimgタグのaltになってしまうため、gridsome-remark-figure-caption![Some Text][数字]<figure><figcaption>にtransformできるようにしました。
gridsome-remark-figure-caption - Gridsome

ただ、このプラグインとwordpress-to-hugo-exporterで出力したmdとの食い合わせが悪く、画像の表示に定義参照リンクを使っているとfigurteタグにtransformできないため、この部分は諦めてpython3で使い捨てのスクリプトを書きました。

from glob import glob
import re

"""
![Some Text][1] を
![Some Text](https://example.com/img.png)
に変換する
"""

for file in glob('./*.md'):
  f = open(file, 'r')

  data = f.read()
  m = re.findall(r' \[(\d+)\]: (http.+)', data)
  dst = data
  for img_nums in m: #((1, http://...))
    pat = r'(!\[.+\])\[%s\]' % img_nums[0]
    rep = r"\1(%s)" % img_nums[1]
    dst = re.sub(pat, rep, dst)
  with open('./convert/'+ file, 'w') as ff: ## convert フォルダに変換結果を出力
    print(dst, file=ff)

  f.close()

wordpress-to-hugo-exporterが削除してしまったhtmlタグの復旧

iframeタグは変換時に削除してしまうようなので、WordPressの全文検索からタグの使用箇所を調べて、消されている箇所に人力で付け足しました。
他にも削除されたタグがあるかもしれないのですが、Markdown化できないタグなら消えてたほうがいいだろうということで、これ以上は調べませんでした。

Gridsomeのテーマの修正

GraphQLのクエリについては、もともとWordPressへの依存が出ないように設定していたのでほとんど書き換えなしで済みました。
一番問題だったのはdate formatの扱いで、Gridsomeは(今の所)タイムゾーンを一切無視して全てGMTで扱ってくるため、LuxonaddSchemaResolversを使って、日本時間を返すfield dateWithOffset を作りました。
なお、@gridsome/source-wordpressの場合、WordPressに設定したタイムゾーンの時刻を返す date とGMTで時刻を返す dateGmt の両方があり、このあたりを考えなくても良い作りになっていました。

タイムゾーンを無視する問題についてはIssueに上がっています。
GraphQL date format doesn't respect timezones · Issue #1244 · gridsome/gridsome
これどうにかしてほしいですね…。

さらに、addSchemaResolversで追加した項目についてはfilterが使えないというバグがあります。
graphql: Filters fail for fields added via Data Store API · Issue #1196 · gridsome/gridsome

ということで、中でDate fieldsにfilterを使っている Gridsomeで月別アーカイブ機能を作成する。 の方法は使えなくなりました。だめじゃん。
月別アーカイブは別の方法で生成するように修正したため、そのうち記事を書こうと思います。

余談

GraphQLでタイムゾーンを扱うライブラリはいくつかあるのですが、どちらもうまいこと動作させられなかったので、今回は導入していません。

記事用ディレクトリのGit submodule化

このブログはテーマを
retrorocket/gridsome-blog: Gridsomeで構築したブログ
のリポジトリで管理しています。
記事もこのリポジトリで管理してしまうと、publised: false に設定している下書き記事(とメモで残している画像)が外から見えてしまうのと、記事のコミットログとテーマの修正用のコミットログが混ざるため、記事はプライベートリポジトリに逃がすことにしました。
contentディレクトリがサブモジュールになっています。

[submodule "content"]
	path = content
	url = git@github.com:retrorocket/blog-contents.git

Hugoはテーマがサブモジュールになっていますが、Gridsomeの場合はどう管理するのが正解なのでしょうね。

Git submodule化した記事リポジトリへのpush時にビルドできるようにする

記事のビルドはテーマ用リポジトリ(に設定したGitHub ActionsのWorkflow)で行う必要があるため、記事リポジトリへのpush時にテーマ用リポジトリのWorkflowが呼び出されるようにします。
もともと、WordPressで記事を書いていたときから、Webhook経由でWorkflowを呼び出せるようにしていたため、それを再利用します。

ビルド完了までの流れ

  1. 記事リポジトリのWorkflowにon: pushを設定し、push時にrepository dispatch eventでGitHub Actionsのワークフローを呼び出す

    # 記事リポジトリの .github/workflows
    name: Fire repository dispatch event
    
    on:
      push:
        branches:
          - master
    
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - run: |
              curl -v -H "Authorization: token ${{ secrets.GitHubのPersonal Access Token }}" -H "Accept: application/vnd.github.everest-preview+json" "https://api.github.com/repos/retrorocket/gridsome-blog/dispatches" -d '{"event_type": "build"}'
  2. テーマリポジトリ側のワークフローが呼び出されるので、サブモジュールをcloneしてビルド

    # テーマリポジトリの .github/workflows
    name: webhook-trigger
    
    on:
      workflow_dispatch:
      repository_dispatch:
        types:
          - build
      push:
        branches:
          - master
    
    jobs:
      build:
        name: build
        runs-on: ubuntu-latest
        steps:
          - name: Checkout
            uses: actions/checkout@v2
            # ここで `submodules: true` にした状態で、`token` を secrets.GITHUB_TOKEN 以外に設定すると、
            # .gitmodulesを更新したときに発生するpushイベントが再帰してしまう。
            # そのため、サブモジュールは別途個別にcloneする
          - name: Checkout other repo
            uses: actions/checkout@v2
            with:
              repository: retrorocket/blog-contents
              path: content
              token: ${{ secrets.記事リポジトリにアクセス権があるToken }}
          # ビルド部分は略
          - name: Push changes
            continue-on-error: true
            uses: ad-m/github-push-action@master
            with:
              github_token: ${{ secrets.GITHUB_TOKEN }}
              branch: ${{ github.ref }}

はまったところ

通常、リポジトリの${{ secrets.GITHUB_TOKEN }}を使った場合、GitHub ActionsのWorkflowは再帰で呼び出されないようになっています。
ワークフローで認証する - GitHub Docs

リポジトリのGITHUB_TOKENを使ってGitHub Actions アプリケーションの代わりにタスクを実行した場合、そのGITHUB_TOKENによって生じたイベントは、新たなワークフローの実行を生じさせません。 これによって、予想外の再帰的なワークフローの実行が生じないようになります。

おそらく actions/checkout@v2 の仕様で、 submodules: true にして tokensecrets.GITHUB_TOKEN 以外に設定すると、親リポジトリのgit pushに使用されるトークンがGITHUB_TOKENから上書きされます。
そのため、.gitmodulesの更新時に発生するgit pushで on: push イベントが再帰してしまい、ビルドが2回呼び出されることになるため、actions/checkoutを2回呼び出して回避しています。
ちなみに、この問題の解決がMarkdown化全体の作業の中で一番時間がかかりました。

移行前と移行後のソースの差分

Comparing source-wordpress...source-filesystem · retrorocket/gridsome-blog
削除された部分が多くて良いですね。Table of Contentsの生成部分等で無駄な処理が必要なくなったので、見通しが良くなりました。

所感

8年近く使ったWordPressくんともお別れの運びとなりました。いままで本当にありがとう…!
WordPressだと記事プレビューがめんどくさかったのですが、Markdown化したことでlocalhost:8080からリアルタイムプレビューできるようになりましたし、WordPress用のサーバも必要なくなったので移行できてよかったです。なにより記事をGitで管理できるのが良いですね。
問題点としては、自分の予想に反してGridsomeの開発が停滞気味で、ここ半年issueも放置されていてv1まで到達できるのかかなり怪しくなってきたのと、細かいところでバグが多いのでGatsbyに乗り換えたい気持ちになったことでしょうか。WordPressからGridsomeは大変だったけど、GridsomeからGatsbyならそんなにパワーを使わなくて済みそうな気はします。