HonoX(Hono + Vite)+ Cloudflare Workersにポートフォリオを移行しました
はじめに
Yusuke WadaさんのHonoは、超有名なので説明不要ですよね。先日のState of JavaScript 2025でも、満足度90%超えで"Tier S"入りしていました。
ただ、かく言う私は使ったことがない。
そこで、ちょうどポートフォリオには少し不満があったこともあり、
Next.js + OpenNext + Cloudflare Workers から
HonoX(Hono + Vite)+ Cloudflare Workers へ移行しました。
この記事は諸々の話と、これからHonoXつかってみたい人の手助けになればと思います。
移行前に感じていた課題
もともとの構成は気に入っていましたが、日常的な運用で不満がありました。
- Next.jsは高機能で強い反面、やや機能過多で開発中に重いと感じる
- Cloudflare workersへのデプロイのため、OpenNextを使っていた。相当ありがったが、正直「もう少し安定してほしいな」と感じる瞬間があった
ちなみにOpenNextは、Next.jsをさまざまな環境で動かすためのアダプターです。
OpenNext aims to support all Next.js features and is widely deployed in production across multiple platforms.
なぜHonoXなのか
HonoXとは
Yusuke Wadaさんが、HonoXについてという記事でおっしゃっているように、
HonoXとは一言で言うと「HonoとViteを組み合わせたメタフレームワーク」
ということです。
Reactに対するNext.jsのようなもんですね。ですが全然違うのは、Viteの設定がそのまま使えたり、コア部分はHonoママなので見通しが良く、ViteやHonoを知っていれば、そのまま知識を活かせます。
さらに、HonoXはよくあるファイルベースルーティングが使えたり、インタラクティブな部分は、"Islandsアーキテクチャ"でそこだけクライアント側でレンダリングできます。
一方で、現時点でアルファのため、破壊的変更リスクは前提として受け入れる必要があります。まあ、自分のポートフォリオなら自己責任なんでね。
HonoX is a simple and fast meta-framework for creating full-stack websites or Web APIs - (formerly Sonik). It stands on the shoulders of giants; built on Hono, Vite, and UI libraries.
Note: HonoX is currently in the "alpha stage". Breaking changes are introduced without following semantic versioning.
選んだ理由
まとめると以下3点です。
- Next.js経験があれば入りやすい(ファイルルーティングできる)
- Cloudflare Workers運用を継続できる(デプロイ先を変えなくてよい)
- 学習コストが読める(構成がシンプルで把握しやすい)
実際に移行してやったこと
移行のためのスピード重視で、機能追加はほとんどしていません。かつ後々のために、運用しやすさを優先しました。
ソースコードはこちら → portfolio-v3 | github
デザイン
- ダークモードのみ
- ボタンやリンクは装飾を最小限
- アニメーションは基本なし
「読むこと」に集中できる設計を優先しました。と、言うとカッコいいですが、単純にデザインに時間をかけたくないだけです。
shadcn/uiのようなUIライブラリも特に使っていません。この程度であれば、ちょこちょこっとTailwind CSSで書けば済みます。
記事管理
もともと記事はMarkdownで、それをパースする構成でした。今回も、同様にライブラリを入れてパースしています。
ただし、以前はmdファイルとそれに使われる画像は、プロジェクトのソースとしてgithubで管理していましたが、今回はCloudflare R2にアップロードして、そこから取得する構成に変更しました。
こうしておけば、"記事を更新するたびgithubにコミット" が不要で、R2にアップロードするだけで済みます。
サイトはCloudflare Workersにデプロイするので、R2のバケットをバインディングすれば、アクセスも楽勝です。(キャッシュを効かせるのに、ちょっと苦労しました。AIとの二人三脚でなんとかした感じです)
唯一の機能
流石になにかしらの機能は追加したいと思い、タグ絞り込み機能を追加しました。
記事の frontmatter に tags を配列で指定しておき、サーバー側で ?tag= クエリパラメータで絞り込みます。
// routes/posts/index.tsx
export default createRoute(async (c) => {
// URLクエリから tag パラメータを取得(例: /posts?tag=React)
const tag = c.req.query("tag") || null;
// ページタイトルとメタ情報を動的に設定
const title = tag ? `Posts: ${tag} | Poko Hanada` : "Posts | Poko Hanada";
const description = tag ? `記事一覧(${tag}タグ)` : "全記事一覧";
return c.render(
<div>
<title>{title}</title>
<meta name="description" content={description} />
{/* 中略 */}
<Section heading="Posts">
{/* tag プロパティを TagFilterとPostList に渡す */}
<TagFilter tag={tag} />
<PostList
bucket={c.env.POSTS_BUCKET}
tag={tag}
cacheOptions={{ ctx: c.executionCtx, request: c.req.raw }}
/>
</Section>
</div>,
);
});
記事一覧を表示する post-list.tsx コンポーネントでは、受け取ったタグのクエリをもとにフィルタリングしています。
// features/post-list.tsx
export const PostList = async ({
bucket,
tag,
cacheOptions,
}) => {
// バケットを受け取りR2から記事を取得
const postsResult = await getAllPosts(bucket, 100, cacheOptions);
// 中略
return (
<ul class="list-none px-0">
{sortedMetadataList
{/* ここでタグフィルタリング。tag指定があればそれを持つ記事だけに絞る */}
.filter((meta) => (tag ? meta.tags?.includes(tag) : true))
.map((meta) => (
<ListContent
key={meta.slug}
href={`/posts/${meta.slug}`}
title={meta.title}
>
<div class="mt-1">
{meta.tags?.map((t) => (
<Button size="sm" href={`/posts?tag=${t}`}>
{t}
</Button>
))}
</div>
</ListContent>
))}
</ul>
);
};
UIでは tag-filter.tsx Island コンポーネントで、選択中のタグをバッジ表示し、クリアボタンで /posts に戻れるようにしています。
// islands/tag-filter.tsx
<div class="flex items-center gap-2 rounded-full border px-3 py-1">
<span># {selectedTag}</span>
<button onClick={() => window.location.assign("/posts")}>✕</button>
</div>
良かった点
- 学習コストは小さめで、スムーズに移行できた。
- SSRとはいえ、非常に軽い。開発中もサクサク動いてストレスがない。
- 必要十分な機能はしっかり揃っている。
参考にした記事
HonoX自体の公式ドキュメントは多分まだなさそうですが、以下の記事がとても参考になりました。
Yusuke Wadaさんの記事
azukiazusaさんの記事
ググればたくさん出てきます。
まとめ
今回の移行は、「最新技術を触ってみたい」だけでなく、軽快に開発できる環境を作りたいという目的も大きかったです。 しばらくはHonoXで運用しつつ、変化を追いながら、シンプルさを維持して改善していきます。
