Romira's develop blog

Rustでブログサイトを作りました

Cover image of Rustでブログサイトを作りました

はじめに

はじめまして,Romiraです.普段は福岡の大学院生をしています.
今回は,Rustでブログサイトを作成したのでその紹介をします.

Rustでブログサイトを作ろうと思った理由

Rustでブログサイトを構築しようと思った理由は主に2つです.
まず,私はRustacean (Rustを書くプログラマー) なのでRustで何かを開発しようというのが根本の原動力です.
次にyewのversion=0.20でSSRモードが試験機能として実装されたからです。

yewとはこの後の章で説明しますが,Rust製のフロントエンドフレームワークです.

技術スタック

本ブログの主な技術スタックは以下の通りです.

  • アプリケーション
    • Rust
    • yew
    • axum
    • Tailwind CSS
    • Newt
  • インフラ
    • OCI
    • Cloudflare

このうちいくつかの技術について取り上げたいと思います。

yew

yew.png

yewとは,WebAssemblyによるマルチスレッドフロントエンドWebアプリを作成するためのモダンなRustフレームワークです,とあります.以下GitHubリポジトリより原文.

Yew is a modern Rust framework for creating multi-threaded front-end web apps with WebAssembly.

マルチスレッド?と引っかかったので調べたのですがWebAssemblyでマルチスレッドを可能にする動きがあるようです.

WebAssemblyでマルチスレッドによる並列処理を可能にする「wasi-threads」仕様の提案、ByteCode Allianceが明らかに

現在はシングルスレッド動作のようですね.

Rustでフロントエンドを書く,となればyewが第一候補に上がると思います.(他にはDioxusというフレームワークが最近活発に開発されているようですが,まだ触ったことがないので今回は割愛します.こちらもSSRに対応しているようです.)

yewの記述はReactによく似ています.例えば「10を上限としてボタンが押されたら表示されている数を1増やす」というCounterコンポーネントは以下のように書くことができます.

#[derive(PartialEq, Properties)]
pub struct CounterProps {
    limit: usize,
}

#[function_component]
pub fn Counter(props: &CounterProps) -> Html {
    let CounterProps { limit } = props;

    let counter = use_state(|| 0);

    let onclick = Callback::from({
        let counter = counter.clone();
        let limit = limit.clone();
        move |_| {
            if *counter < limit {
                counter.set(*counter + 1);
            }
        }
    });

    html! {
        <div>
            <p>{*counter}</p>
            <button onclick={onclick}>{"+1"}</button>
        </div>
    }
}

#[function_component]
fn App() -> Html {

    html! { <Counter limit={10} /> }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

実際に動かしてみたい方は
Playgroundがあるので試してみてください.

このyewというフレームワークですが,最新のバージョンでついにSSRに対応しました.(めでたい!)

SSRはSEO強い等のメリットがあるので今回のようなブログサイトには適しています.

基本的な記述方法は公式ドキュメントexamplesがあるので軽く紹介します.

use yew::prelude::*;
use yew::ServerRenderer;

#[function_component]
fn App() -> Html {
    html! {<div>{"Hello, World!"}</div>}
}

#[tokio::main(flavor = "current_thread")]
async fn no_main() {
    let renderer = ServerRenderer::<App>::new();

    let rendered = renderer.render().await;

    // Prints: <div>Hello, World!</div>
    println!("{}", rendered);
}

CounterコンポーネントとSSRのexampleの違いはrender()の結果が文字列で返ってくる点でしょうか.
注意したいのは,コード上ではわかりにくいですがランタイムの違いです.前者はWASMにビルドされた後にブラウザで実行されます.
後者は当然SSRなのでビルドされるのは実行ファイルです.

もちろんこのままではリクエストを受け付けていないのでactix-webaxumなどのWebフレームワークを用いてリクエストに応じたレンダリング結果をブラウザに返します.
今回は公式のexampleに則ってaxumを採用しました.

またHydrationさせるために必要なファイルもBuildする必要があります.
これがちょっと難ありで,という話は「開発中に困ったこと」の章で話したいと思います.

Tailwind CSS

tailwindcss.png

これについては選択肢がなかった,という一言に尽きます.
公式ドキュメントにはいくつかのフレームワークが挙げられているのですが,どれも更新が数ヶ月,数年前で現在のyewの最新バージョンに追従できていないので使えないというのが現実です.
これ以外のフレームワークだとjsを必要とするものやnpmのパッケージで提供されているものは導入が困難です.
その点,Tailwind CSSはCSSを読み込むだけで導入できるのでyewでの開発にも使うことができます.

Newt

newt.png

Newtとは,ヘッドレスCMSサービスの一つです.

  • 無料枠が豊富
  • 日本語対応

という2つを理由にこのサービスを採用することにしました.無料枠についてはこのような感じです.

newt無料枠.png

基本的に個人利用なら使い切れないんじゃないかなと思います.
モデル数が無制限なのは拡張性があって嬉しいですね.
この他にも画像をGoogle Cloud Storage or AWS S3に保存することができ,そうした場合,imageKit or imgixで画像最適化を行うことができます.
これは管理画面からポチポチ行うだけで設定できたのでそこまで難しくなかったです.

インフラ

今回の構成図です.

blog-romira-dev.drawio.png

アプリケーションは少し前に話題になったOracle Cloud InfrastructureのArm Ampere A1 Compute (高スペックなARM CPUが無料) にホスティングしています.
このインスタンスには個人開発した他のアプリケーション等もホスティングしているのでnginxを前段に置いています.

CDNにCloudflareを採用し,キャッシュさせています.
これによりAPIを叩く回数を削減し,Webページの高速化にも貢献しています.
またNewt側でコンテンツに更新があった場合はすぐに最新のデータを表示させたいのでWebhook通知機能を用いてGitHub Actionsに通知し,Purgeしています.

開発中に困ったこと

とにかく試験機能であったり,そもそもRustの中でもフロントエンドなんてほとんど情報がないので困ったことがいくつか出てきました.

  1. 記事ページ(このページ等)のテーマ切り替えボタンが効かない.
  2. OGP設定が反映されない
  3. デバッグがつらい

などです.

記事ページ(このページ等)のテーマ切り替えボタンが効かない.

このサイトにはライトテーマとダークテーマを用意していて,切り替えるボタンをヘッダーの右に設置しているのですが,押して頂けるとわかるように無反応です.
ちなみにトップページは正常に動作しています.

この原因はわかっていて,from_html_uncheckedというHTML文字列をyewのVNodeという型に変換する関数を実行しているからです.
これはSSRモードのHydration時に発生する問題で,WASMがpanic!を起こし処理が終わってしまうようです.
これに起因して切り替えるボタンが無効化されてしまっているのだと思います.

この問題はyewのIssueにも取り上げられており,Hydrationに対応させようという動きはあるようですが,どうやらあまり上手くいってないようです.

こちらでなんとかできないかと試行錯誤しましたが上手く行かなかったのでこのバグは一旦放置しています.

OGP設定が反映されない

curlで叩いてもブラウザで見てもMetaタグは設定されているのになぜか反映されないという現象が起きていました.
原因は単純でStreamで配信されていたから,なんですよね.
なぜStreamにしていたかというのはyewのexamples/ssr_routerをそのまま使っていたからという単純な理由なのですが,結構原因がわからずに余計な調査をしていました.
結果的にここを修正するだけで反映されました.

デバッグがつらい

これに関しては困ったことというより本当に辛いだけなのですが.

今回の開発環境ではコードの変更 → ブラウザでチェックの過程の間に

  1. tailwind.cssをビルドする
  2. Hydrationのためのビルドをする(trunk buildでWASMにビルド)
  3. 本体をビルドする(cargo build

1つ目についてはすぐ終わるので無視できます.
問題は2つ目と3つ目です.Rustのビルドは遅いことで有名ですが,そのビルドが2回走ります.とても長いです.
大体平均ですが,5~7秒ほどかかります.細かく頻繁に調整したいフロントエンド開発にとってはなかなか辛いものがあります.

まとめ

RustのフロントエンドフレームワークyewでSSRモードを使ってブログサイトを作成しました.
現状,Rustでフロントエンドとなると趣味の範囲を超えないですがRustが書けるということはRustaceanにとって一番のメリットなのでぜひ使ってみてほしいです(辛い話しかしていないような気がしますが・・・楽しいですよ).

このサイトのこれから

現状は特に記事を表示するというシンプルな機能しかないですが,記事が増えてきたら

  • カテゴリ別に表示する
  • ページネーションに対応する

あたりはやりたいかなと思います.どれも記事がないと意味がないので,これからこのサイトでブログ投稿していこうと思います.

参考文献