3 min read

ブログをGhostへ移行した

背景

今までWordPressを使ってブログを運用していたが、WPは公式にMarkdownに対応しておらず、別途プラグインを導入しないと実現できなかった。

別途導入する必要があることや、PHPなこと、管理画面UIが古臭いことが気になっており、前々から良いCMSはないかと探していた。
CMSの条件として

  • セルフホスト可能
  • Markdown公式サポート
  • 動作が軽量

といった点で考えていた。

しばらく探してもなさそうだったので、諦めて自作を開始したが認証系の実装で手こずった。
それでもなんとかあとは管理画面UIを作るだけ、というところになって一気にやる気が失せた。

ロジックは書けてもUIはそんなゴリゴリ書けん。
あ〜もうなんかだめだ〜〜〜となり、再度CMSを探す旅に出た。

そこで見つけたのがGhost
ものすごくおしゃれで今風なUI
それにMarkdownも公式サポート

これは試してみるしかないと思い、早速ローカルに建ててみた。
公式からDockerコンテナが配布されているので構築はめちゃくちゃ簡単。ほんとうに誰でもできるレベル。

あっという間に構築でき、すぐ使えるようになった。
試しに記事を書いてみても書きやすいし、なによりやっぱりMarkdownは強い。

ブログCMSをGhostにすることに決めた。

テーマ製作

なるべくWPで使っているUIを崩さずにそのまま、だけど軽量に、これを目指して自作することにした。
業務でCSSはかじっているのでなんとなくなら書ける。

Ghostが利用しているテンプレートエンジンのHandlebarsとスタイルとしてSCSSを使ってがんばって作った。

GitHub - sugtao4423/tao-blog-theme: https://blog.sugtao4423.xyz/
https://blog.sugtao4423.xyz/. Contribute to sugtao4423/tao-blog-theme development by creating an account on GitHub.

Ghost、なにげに良いのは上のように /bookmark でブックマークカードを作れること。
もちろんこれも自作のスタイルをあてている。

なおGhostくんは自動でminifyしてくれたりはしないので、そこらへんは自前でやる必要がある。
上のリポジトリの package.json を参考にでもどうぞ。

そんなこんなで全く新しいブログシステムでありながら以前のようなUIでの提供?をすることができた。

Ghostくん、末永くよろしくお願いいたします。

余談: 記事移行やリダイレクト

WPからの記事の移行は公式から移行用プラグインが配布されていたので使ってみた。
が、どうも投稿日時や更新日時などの日時系をローカルのタイムゾーンから取ってきてしまう模様。
つまり post_date_gmt を取ってUTCから算出してほしいのに post_date のJSTを取ってそれをUTCとみなしてしまっていた。

それだと時刻がバグりまくるのでう〜んだった。
それだけに修正を加えるスクリプトを作ってもよかったのだが、それならもういっそのこと全部自分でやろうと思い、お得意のPHPでスクリプトを書いて記事の移行をした。
ただタグ(カテゴリ)等は移行されないのでそこだけは手動でやった。
あとHTMLからのMarkdown化もほぼ手作業。
結構時間かかったがテーマ製作に比べたら全然だったはず。

リダイレクトに関してもPHPでスクリプトを書いた。
Ghostにはリダイレクトのリストを渡すと自動でそれらを適用してくれる機能があるのだが、うちではリバースプロキシとしてnginxを使っているのでそっち側で処理させてオーバーヘッドをなくした。

リダイレクト一覧を出力するのに使ったスクリプト

簡単に言えばWPとGhostのDBに接続、記事タイトルが一致するもののslugをnginx conf用に整形して出力という感じ。

<?php

declare(strict_types=1);

/* Nginx configuration
location / {
    proxy_pass http://ghost/;

    if ( $arg_p = "10" ) { return 301 /hoge-id/; }
    location = /archives/10 { return 301 /hoge-id/; }
}
*/

$WP_DB_HOST = '10.0.0.24';
$WP_DB_NAME = 'WordPress';
$WP_DB_USER = 'wordpress';
$WP_DB_PASS = 'example';

$GHOST_DB_HOST = '127.0.0.1';
$GHOST_DB_NAME = 'ghost';
$GHOST_DB_USER = 'root';
$GHOST_DB_PASS = 'example';

$wpPdo = new PDO("mysql:host=$WP_DB_HOST;dbname=$WP_DB_NAME", $WP_DB_USER, $WP_DB_PASS);
$wpPdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$stmt = $wpPdo->prepare('SELECT * FROM wp_posts WHERE post_type = "post" AND post_status = "publish" ORDER BY post_date_gmt ASC');
$stmt->execute();
$wpPosts = $stmt->fetchAll(PDO::FETCH_ASSOC);

$ghostPdo = new PDO("mysql:host=$GHOST_DB_HOST;dbname=$GHOST_DB_NAME", $GHOST_DB_USER, $GHOST_DB_PASS);
$wpPdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$pArgRedirects = [];
$archiveRedirects = [];

foreach ($wpPosts as $post) {
    $postId = $post['ID'];
    $postTitle = $post['post_title'];

    $stmt = $ghostPdo->prepare('SELECT * FROM posts WHERE title = :title');
    $stmt->execute(['title' => $postTitle]);
    $ghostPost = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$ghostPost) {
        echo "No ghost post found for $postTitle\n";
        continue;
    }
    $ghostSlug = $ghostPost['slug'];

    $pArgRedirects[] = [
        'oldPostId' => $postId,
        'newUrl' => "/$ghostSlug/",
    ];

    $archiveRedirects[] = [
        'oldPath' => "/archives/$postId",
        'newUrl' => "/$ghostSlug/",
    ];
}

$pArgString = '';
foreach ($pArgRedirects as $redirect) {
    $pArgString .= "if ( \$arg_p = \"{$redirect['oldPostId']}\" ) { return 301 {$redirect['newUrl']}; }\n";
}
file_put_contents(__DIR__ . '/p-arg-redirects.conf', $pArgString);


$slashString = '';
foreach ($archiveRedirects as $redirect) {
    $slashString .= "location = {$redirect['oldPath']} { return 301 {$redirect['newUrl']}; }\n";
}
file_put_contents(__DIR__ . '/archive-redirects.conf', $slashString);

ただnginxもGhostもUnicode文字をうまくパースできず一つのパーマリンクが404になってしまった。
これに関してはURLにエンコード文字を使っていたのが悪いと思ってるので割り切れた。

  • /?p=1: 301 redirect
  • /archives/1: 301 redirect
  • /%yyyy%/%mm%/%postname%: 404

最後にパフォーマンスの比較を貼って〆

追記

以下、Google PageSpeed InsightsのMobile、PC両方での話。
記載しているスコアは両方で同じだった。

05/30 ユーザー補助72から97

画像だけのリンクやボタンに aria-label 属性を追加して97まで上昇した。

06/07 パフォーマンス89から100

「次世代フォーマットでの画像の配信」項目を極力減らした。
具体的にはAVIFとWebPの二種類の画像を作成して srcset image-set() に記述した。(ヘッダー背景とプロフィール画像)
上記ツイートのパフォーマンスでは93になっていたが、それから何回計測をしても80台だったので93はたまたまだった模様。