長らく放置していたブログプラットホームのリポジトリがあったんだけど、最近のご時世もあって日々の変化が少ないので、日記でも書こうかなと思って開発再開してリリースまでしました、という話です。技術的な面について書きます。
構成
まずはざっくりイメージ画像。
やってることは大したことないのにサービス分割しちゃってますが、ログインして記事を書いたりするところをDashboard、記事が公に表示されるところをFrontと呼んでいます。両者がJSON APIをコールしてHTMLを表示(FrontだけちゃんとSSR)しています。
RenderというPaaSを使っていて、Renderには外からアクセスできるWeb ServiceとアクセスできないPrivate Serviceという概念があります。APIとRedisはDashboardとFrontから触れればいいのでPrivateにしています。なのでブラウザからAPI叩きたいときはそれぞれがプロキシしています。
リポジトリ
いわゆるモノレポ。1つでやっています。昔はたくさんリポジトリを作るのが趣味だったんですが、あっちこっちへプルリクを出して疲弊したので1つで収める努力をしています。Renderのアプリケーションはリポジトリ単位で設定するのですが、ビルドコマンドなどを調整するとモノレポでもいけます。masterブランチが更新されるとデプロイされるので、一部のアプリケーションにしか変更がないのに全部デプロイされてしまう問題はあるのですが、困ってないので問題ではないということにしています。lintなどcommitにフックして何かしたいときは、各アプリケーションごとに何か入れるのではなく、pre-commitを使っています。CIはGitHub Actionsでやっていて、変更してないコードのテストが走るのは無駄なので以下の条件を付けています。
name: Run dashboard Tests on: push: paths: - dashboard/** - .github/**
ただしこれだと、pushしたときの最新のcommitで条件に当てはまるファイルを編集していないとテストが走ってくれないようなので真似しないでください。push:
をpull_request:
にすればいいのかなーくらいで止まっているので後でドキュメント読んで直します。
フレームワーク
APIはElixir/Phoenix、DashboardとFrontはSvelte/Sapperです。最近routifyというものがあるのを知ったので、もしかしたらDashboardがFrontどっちか載せ替えて遊んでみるかも。
認証
Frontが出す情報は誰にでも見れるものだけなので、そこのAPIは認証は不要にしています。Dashboardが叩いているAPIは権限によってできることできないことがある(他人の記事は編集できないとか)ので、JWTのトークンが必要になっています。ブラウザにトークンは返したくなかったので、DashboardにログインするときにAPIから発行されたトークンはセッションに入れ、Dashboardへのログイン状態はhttpOnlyのクッキーで管理しています。続くリクエストをAPIにプロキシするときにセッションからトークンを持ってきて、リクエストヘッダにそっと乗せてあげています。
あと、いまのところユーザは自分だけで、それでいいかなと思っているので新規登録はできないようになっています。
テーマ
まだデフォルトの白いテーマしか作ってないですが、記事を表示するときの文字色とか背景色とか余白とかをコントロールできるように、細かくCSSのカスタムプロパティを用意しています。DevTools開くとなんか見えるはず。ReactだとProvider使ってやったりするらしいですが、SvelteだとそこまでCSS in JSって感じじゃないし中途半端にそういうことすると辛くなりそうだったので、JavaScript界では極力なにもしない方針で。
もうちょい細かい話を書くと、ブログの設定に応じてカスタムプロパティ定義のCSSを返すエンドポイントを用意して、Sapperのtemplate.html
にそれを読むlinkタグを追加しています。このエンドポイントはAPIを叩いてテーマ名からCSSファイル読んで中身を返しているだけ。最終的なHTMLは↓のような感じ。
<head> <!-- カスタムプロパティ定義だけのCSS --> <link rel="stylesheet" href="/themes/style.css"> <!-- カスタムプロパティを使うCSSたち --> <link rel="stylesheet" href="client/main.2468218770.css"> <link rel="stylesheet" href="client/index.e7c4f362.css"> <link rel="stylesheet" href="client/PostList.1a162f09.css"> </head>
本当は<svelte:head>
でheadタグの中に流し込めればエンドポイント用意する必要もないしリクエスト増やさなくて済んだのですが、<svelte:head>
で入れるとカスタムプロパティを使っているCSSたちより下に書かれてしまい、一瞬スタイル崩れが見えてしまったのでこういう実装にしています。何か案が思いついたら変えます。
記事の構造
ちょっとした工夫ポイントとして、記事の構造があります。最初にあんまやりたくないなと思っていたのはMarkdownで、理由はコードブロックとか多くひとは使わないようなものも付いてきちゃうから。あと方言が多い。とはいえWYSIWYGも嫌で、contenteditable
を駆使するのもちょっとやったらエグい辛くてやめました。その辺りで、例えば[見出し, 段落, 画像, 段落, 見出し, 段落]
みたいに、文書を構成する要素の配列で記事を表現したらいいんでないかという考えに至り、調べたら世にブロックエディタという概念があるようで、100番煎じだけどつまり実績あるってことじゃんとなって採用に至りました。ブロックエディタのJSライブラリもあったけど、スマホから触ったらカーソルが荒ぶって飛んでいってしまったので自作の方針で。いまは見出しと段落とリンクしかないけど、記事書く画面はこんな感じ。右の︙から要素を追加したり順番を変えたりできます。
DB上ではJSONBのカラムに↓のようなJSONを突っ込んでるだけで、アプリケーション層で余計なキーを弾いたり構造のバリデーションをがんばったりしています。children
が入れ子になるケースはまだないですが、実装も大変そうだし使い勝手も悪くなりそうなのでやりたくないなという気持ちだけあります。
%{ "type" => "root", "children" => [ %{ "type" => "paragraph", "value" => "これは段落です。\nはい。" }, %{ "type" => "link", "value" => %{ "title" => "Link", "url" => "https://example.com" } } ] }
感想
なんか後で直しますばっかりだった。あと、モノリスにしなかったばかりに、Renderの請求がそこそこのお値段になってしまいました。