2024年09月29日
HonoXの発表がされてから、いつかやろうと思っていた移行作業を本日に行った。
しかし振り返ってみるとこの日記サイト自体は2021年から記事があるが、色んな移行作業をしてきた気がする。
そういえば何らかの大きなアップデートをするたびに日記に残していた。
これを機に振り返ってみよう。
当初からしばらくNext.jsの12系で動かしていたのだった。
このタイミングで13系に切り替えたようだが、エントリを読むと、結構苦戦していたみたい。
2023年の9月にserverless daysのワークショップでHonoに触れて試しに作ってみた時のエントリ。
この時に行けるという手ごたえがあったのだった。
このくらいの頃からNext.jsが自分の手に負えなくなってきていて、移行を真剣に考え始めたような気がする。
実際にVercelで動いていたサイトをHono x Cloudflareに置き換えた際の記録。
この時はWorkersで動かしていた。
かなり恐縮ではあるものの、枠が空いていたので、Hono Advent Calendar 2023に登録してみた。
https://qiita.com/advent-calendar/2023/hono
クリスマスイブに投稿された記念的なエントリ。エモい。
WorkersからPagesへの置き換えた時の記録。
Hono自体はこの辺りで大きめのアップデートが入って確かこのくらいのタイミングでViteが組み込まれたような記憶がある。ここの置き換えは結構大変だった。だが、エントリに書いてある通りゆーすけべーさんの助言もあり何とか動かすことに成功をした。
こんな感じで紆余曲折を経てきた日記サイトだが、今回はHonoXベースに移行をすることにした。
まずはHonoXとは何なのかということをゆーすけべーさんのZennのエントリから引用をする。
HonoXとは一言で言うと「HonoとViteを組み合わせたメタフレームワーク」です。HonoX自体が機能を提供しないのが肝です。
もう少しだけ具体的に言います。HonoXで扱うのは「Honoのインスタンス」そのものです。つまりあなたがHonoXでアプリを作るということは「Honoのアプリを作る」ことになります。その証拠にエントリーポイントになる
app/server.ts
内で出てくるapp
はHonoのインスタンスなので、hono/dev
にあるヘルパー関数showRoutes()
がそのまま使えます。
難しいことは分からないが、HonoXでアプリを作りということはHonoでアプリを作ることと同義なので、そんなに移行は大変そうじゃないかも?という気がした。
そして実際に素人の自分でもかなりスムーズに移行ができたので、すげぇ、本当だ!って感じで驚いた。やり始めてから3時間くらいだったんじゃないだろうか。
さすがにすげぇだけだと話が終わってしまうので、ざっくりとどんな作業をしたのかということをまとめておく。
適宜githubのREADMEを参考にした。
https://github.com/honojs/honox
まずはベースとなる構成を参考にするために、npm create hono@latest
して適当なプロジェクトを作成する。
参考にして、vite.config.ts
を書き換える。
import build from '@hono/vite-build/cloudflare-pages'
import adapter from '@hono/vite-dev-server/cloudflare'
import honox from 'honox/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [honox({ devServer: { adapter } }), build()],
ssr: {
external: ['dayjs']
},
})
package.jsonを以下のようにしてyarn install
する。
{
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build --mode client && vite build",
"preview": "wrangler pages dev",
"deploy": "npm run build && wrangler pages deploy",
"sitemap": "scripts/gen_sitemap.py"
},
"dependencies": {
"dayjs": "^1.11.10",
"hono": "^4.6.3",
"honox": "^0.1.25",
"microcms-js-sdk": "^3.1.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240529.0",
"@hono/vite-build": "^1.0.0",
"@hono/vite-dev-server": "^0.15.1",
"vite": "^5.2.12",
"wrangler": "^3.57.2"
}
}
適当に作ったプロジェクトのapp
ディレクトリを丸ごとコピペして、今までsrc
ディレクトリにあった構成フォルダをコピペする。
※index.tsxとlayout.tsxはコピペしない。
これまでindex.tsx内でBindingsの定義をしていたが、HonoXではこのファイル内に定義をする。
自分の場合microCMSのリクエストで用いる環境変数を取得する必要があったので、受け取れるようにここで定義。
import { } from 'hono'
import type { Meta } from './types/meta'
type Head = {
meta: Meta
}
type Bindings = {
API_KEY: string
SERVICE_DOMAIN: string
}
declare module 'hono' {
interface Env {
Variables: {}
Bindings: Bindings
}
interface ContextRenderer {
(content: string | Promise<string>, head?: Head): Response | Promise<Response>
}
}
また、Head情報を定義することで_renderer.tsxで受け取ることができる仕組みらしい。
// routes/_renderer.tsx
export default jsxRenderer(({ children, meta }) => {
return (
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{meta?.title}</title>
<link rel="canonical" href={meta?.canonicalUrl} />
<meta name="description" content={meta?.description} />
<meta name="author" content={config.author} />
{/*<!-- OGP -->*/}
<meta property="og:title" content={meta?.title} />
<meta property="og:description" content={meta?.description} />
...
</head>
<body>
<Header></Header>
{children}
<Footer></Footer>
</body>
</html>
)
})
もともとのHonoプロジェクトではindex.tsx内でルーティングを定義していた。
HonoXではファイルベースのルーティングシステムとなる。
例えば記事詳細ページは/post/:slug
というルールでルーティングしていて、もともとはこういう書き方をしていた。
/**
* 記事詳細ページ
*/
app.get('/post/:slug', async (c) => {
const slug = c.req.param('slug')
const client = createClient({
serviceDomain: c.env.SERVICE_DOMAIN,
apiKey: c.env.API_KEY,
})
const listDetail = await client.getListDetail<Post>({ endpoint: 'post', contentId: slug })
const props = {
post: listDetail,
siteData: {
title: listDetail.title,
description: listDetail.description,
canonicalUrl: config.siteURL + `/post/${slug}`,
ogpType: "article" as const,
ogpImage: listDetail.thumbnail?.url,
ogpUrl: config.siteURL + `/post/${slug}`,
},
}
return c.html(<DetailContent {...props} />)
})
同じようなことをするにはroutes/post/[id].tsx
というファイルを作り以下のようにする。
import { createRoute } from 'honox/factory'
import { MicroCMSClient } from '../../libs/microcmsClient'
import type { Post } from '../../types/blog'
import type { Meta } from '../../types/meta'
import { config } from '../../settings/siteSettings'
import { DetailContent } from '../../components/DetailContent'
export default createRoute(async (c) => {
const { id } = c.req.param()
const serviceDomain = c.env.SERVICE_DOMAIN
const apiKey = c.env.API_KEY
const client = new MicroCMSClient(serviceDomain, apiKey)
const post = await client.getDetail<Post>('post', id)
if (!post) return
const contentUrl = config.siteURL + `/post/${id}`
const meta: Meta = {
title: post.title,
description: post.description,
canonicalUrl: contentUrl,
ogpType: 'article' as const,
ogpImage: post.thumbnail?.url,
ogpUrl: contentUrl
}
return c.render(
<>
<DetailContent post={post} />
</>, { meta }
)
})
HonoでComponent化していたtsxファイルはHonoXになっても何も変更せずに使える。
なので、一部データ取得で記述が変えるだけで今まで通り動くようになる。
この日記サイトについては、ページング、カテゴリ別の絞り込み、タグの絞り込みなどのルーティングがあるが、ほぼファイル構成を変えるだけで動くようになってしまった。
Honoがそのまま動く、の前評判通り構成をいじるだけで置き換えが完了、率直に言って感動をした。
githubに公開している。
https://github.com/qlitre/hono-qlitre-dialy
まだ理解が追い付いていない部分も多いが、とりあえず無事に移行ができたのでよかった。
雑感としてはファイルベースになったことで、コードの整理がしやすくなってブログなどの階層的なサイトを作るなら断然HonoXの方がいいな、と思ってしまう。それと、HonoXになったことでクライアントコンポーネントも作りやすくなったみたいなので、もう少しリアクティブな動きを取り入れたりなどもしやすそう。HonoXに切り替えたことで、またブログ作りが楽しくなりそうだ、という予感がする。ではでは。