HonoX(Hono + Vite)+ Cloudflare Workersにポートフォリオを移行しました

2026/2/17

はじめに

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公式リポジトリ

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点です。

  1. Next.js経験があれば入りやすい(ファイルルーティングできる)
  2. Cloudflare Workers運用を継続できる(デプロイ先を変えなくてよい)
  3. 学習コストが読める(構成がシンプルで把握しやすい)

実際に移行してやったこと

移行のためのスピード重視で、機能追加はほとんどしていません。かつ後々のために、運用しやすさを優先しました。

ソースコードはこちら → 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で運用しつつ、変化を追いながら、シンプルさを維持して改善していきます。


Recent Posts