RustとDioxusでブログサイトを自作した話
はじめに
はじめまして、せをと申します。 趣味でプログラミングをしています。
私は、以前からブログを書いてみたいと思っていました。 しかし、ネタが無くてずっと書けずにいました。 そこで、ブログサイトを自作してそれについて書けばよいのではないか、と思いました。
今回はRustとDioxusで自作したブログサイトを紹介します。
注意
Web技術について詳しいわけではないため、間違いがある可能性があります。 参考にする際は注意してください。
概要
技術スタック
Dioxus
DioxusはRustのフロントエンドフレームワークです。
Rustのフロントエンドフレームワークの選択肢は他にもYew、Leptosなどが挙げられます。 どれもReactのような書き方ができます。 その中でも、Dioxusを選んだ理由は2つあります。
理由1: 汎用性が高い
DioxusはWebだけでなく、デスクトップアプリやモバイルアプリも対応しています。 今回はWebのみですが、今後、なにかアプリを作る際の選択肢に入れることができます。
また、WebについてもCSR、SSR、SSGに対応しており、他2つと比較して最も汎用性が高いと言えます。 ただし、SSGに対応しているのはv0.6.*のみで、今回使用したv0.7.9では未対応です。
理由2: 読みやすい
Yewはこのようにマクロの中にHTMLを記述する書き方で、Leptosもこれに似た書き方です。
#[component]
fn App() -> Html {
let counter = use_state(|| 0);
let onclick = {
let counter = counter.clone();
move |_| {
let value = *counter + 1;
counter.set(value);
}
};
html! {
<div>
<button {onclick}>{ "+1" }</button>
<p>{ *counter }</p>
</div>
}
}
それに対し、DioxusはこのようにタグをRust構造体のように記述できます。
pub fn App() -> Element {
let mut count = use_signal(|| 0);
rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
}
}
これは人によって好みが分かれると思いますが、私は、Dioxusの書き方のほうが好きです。 理由はタグを閉じるときにタグ名が不要なので、タグが増えたときにごちゃごちゃせず読みやすいからです。
SSGとSSRについて
SSG(Static Site Generation)とは、HTMLファイルをすべて事前に生成しておいて、クライアントに配信する技術です。 サーバーはリクエストに対応するHTMLファイルをクライアントに返すだけなので低いサーバー負荷で高速なWebサイトを構築できます。
SSR(Server Side Rendering)とは、サーバーがリクエストを受け取ると、リクエストに対応したHTMLを生成して返す技術です。 リクエスト毎に動的にHTMLを生成するため、SSGよりも柔軟なWebアプリを構築できます。
作成したブログサイト自体はSSGを使用していますが、ビルドにSSRを使用しています。
ディレクトリ構成
.
├── assets/
│ └── posts/ # ブログ記事をMarkdownで配置
├── build.sh # ビルドスクリプト
├── public/ # ビルド時にtarget/dx/blog/release/web/にコピーされる
│ └── assets/
│ └── images/ # 画像を配置
├── src/
│ ├── components/ # Dioxusコンポーネント
│ ├── lib.rs
│ ├── main.rs # エントリーポイント
│ ├── posts.rs # MarkdownをHTMLに変換
│ └── route.rs # ルートハンドラ
└── target/dx/blog/release/web/ # ビルド成果物
├── public/ # クライアントに配信するHTMLなど
└── server # サーバーの実行ファイル
ビルドの流れ
後で詳しく説明しますが、Dioxusの特性上、このような流れにすることで静的サイトとしてビルドできます。
- SSRのサーバーとしてビルド
- サーバーを起動
- 起動すると
assets/posts/のMarkdownを読み込み、HTMLに変換してサーバー内で保持
- 起動すると
curlコマンドでサーバーのすべてのページに対してリクエストを送信- サーバーはHTMLを返すと同時にHTMLファイルを
public/以下に保存
- サーバーはHTMLを返すと同時にHTMLファイルを
- サーバー終了
詰まったところ
記事のスタイリング
全体のスタイリングにはTailwind CSSを使用しました。 Tailwind CSSは事前に定義されたクラスを使用してスタイリングを行います。
Dioxusでのセットアップ方法は公式ドキュメントのTailwindに書いてあります。
とても便利なのですが、ブラウザの標準スタイルが解除されてしまいます。 今回のブログサイトの場合、Markdownの記事をHTMLに変換して埋め込むため、クラスを指定できず、スタイリングされていない記事が表示されます。

解決策として、記事で使われるすべてのタグに対してCSSでスタイリングをしようとしましたが、量が多いためとても面倒です。
そこで、他の解決策を調べてみるとTailwind CSS Typographyという公式のプラグインを見つけました。
このプラグインはproseクラスを指定したタグの内部をいい感じにスタイリングしてくれます。
記事を包んでいる<article>タグにproseクラスを指定し、ブログ全体のスタイリングに合わせてカスタマイズしました。

v0.7.9ではSSG未対応
Dioxus公式ドキュメントのStatic Site GenerationにはSSGを使用する方法がありますが、実際にやってみるとできません。
dx bundle --web --ssg --release
このコマンドを実行するとtarget/dx/blog/release/web/public/にすべてのページのHTMLが生成されるはずですがルートのindex.htmlしか生成されませんでした。
調べてみると、v0.6からv0.7で大規模なリファクタリングが行われた関係で、この機能が無効化されているようです。
今後のアップデートで修正されると思いますが、SSGを使用するには一時的な代替案が必要です。
DioxusのSSGがやっていることは簡単なことでした。 Static Site Generationにはこのように書かれています。(日本語訳)
Dioxus SSGは、アプリをローカルで実行し、アプリからサイトマップを取得した後、curlリクエストを手動で実行してサイトをインデックス化します。 サイトがSSGを使用するように設定されている場合、各ページのHTMLがファイルシステム上にキャッシュされます。
この処理をbuild.shに記述しました。
dx bundle --web --release # SSRとしてビルド
./target/dx/blog/release/web/server & # サーバーを起動
SERVER_PID=$!
sleep 2 # 起動するまで待機
ROUTES=$(curl -s -X POST http://localhost:8080/api/static_routes \ # 全ページのルーティングを取得
-H "Content-Type: application/json" -d '{}')
echo "$ROUTES" | tr -d '[]"' | tr ',' '\n' | while read route; do
curl -s -H "Accept: text/html" "http://localhost:8080$route" > /dev/null # 各ページに対してリクエストを送信
echo "Rendered: $route"
done
kill $SERVER_PID # サーバー終了
このスクリプトを実行すれば、target/dx/blog/release/web/public/に静的サイトが生成されます。
デプロイ
GithubにプッシュするとGithub Actionsが動き、Cloudflare Pagesにデプロイするようにしました。
最初にこのようなワークフローを単純に作成しました。
- Rustをインストール
- dioxus-cliをインストール
- ビルド
- デプロイ
しかし、いくつか問題がありました。
dioxus-cliのインストールが遅い
cargo install dioxus-cliでインストールできますが、ソースコードからビルドするためかなり時間がかかります。
そこで、このようにcargo-binstallを使用することでバイナリをダウンロードでき、ビルドを避けられます。
steps:
- uses: cargo-bins/cargo-binstall@main # cargo-binstallをインストール
- run: cargo binstall dioxus-cli@0.7.9 # cargo-binstallを使用してdioxus-cliをインストール
ビルドが遅い
dioxus-cliほどではありませんが、ブログサイト自体もビルドに時間がかかります。
こちらはSwatinem/rust-cacheを使用してビルド成果物をキャッシュすることで2回目以降のビルド時間を大幅に短縮できます。
デプロイ時にエラーが発生する
Cloudflare Pagesへのデプロイはcloudflare/wrangler-actionを使用しました。
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy target/dx/blog/release/web/public/ --project-name=blog
しかし、pnpmが見つからないというエラーが発生しました。
Error: Unable to locate executable file: pnpm. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.
Error: 🚨 Action failed
pnpmをインストールしても別のエラーが発生しました。 試行錯誤した結果、cloudflare/wrangler-actionを使用せずにnpmでwranglerをインストールして直接実行することでうまくいきました。
最終的にできたワークフローはこちらです。
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: Setup cache
uses: Swatinem/rust-cache@v2
with:
cache-bin: "false"
cache-on-failure: "true"
- name: Setup binstall
uses: cargo-bins/cargo-binstall@main
- name: Install dioxus-cli
run: cargo binstall dioxus-cli@0.7.9
- name: Build
run: ./build.sh
- name: Install wrangler
run: npm install -g wrangler
- name: Deploy to Cloudflare Pages
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: wrangler pages deploy target/dx/blog/release/web/public --project-name=blog
まとめ
RustでのWeb開発はもっと苦戦するかと思いましたが、簡単なブログサイト程度であれば意外と簡単だと感じました。 一番の難点はやはり情報の少なさだと思います。 この点についてはエコシステムが拡大するにつれて情報も増えていくと思うので今後に期待したいです。
また、自分で公開するまでできたことはWeb開発について知ることができ、よい経験になりました。 私の中でWeb開発をするハードルがかなり低くなったと感じます。
今後は記事を増やしながら、コードブロックのハイライト、数式の描画、タグ検索、全文検索といった機能も追加していきたいと考えています。