HonoXで画像配信パターンの検証

はじまりはここから
ゆーすけベーさんのCloudflare画像配信パターンについての記事。
HonoでやってるcloudflareのR2、Workers配信の配信パターンを
HonoXでやるならどうなるんだろうと思って、HonoXをまだ使いこなせていない分際ですが
やってみたくなった。
一旦導入
とりあえずはプロジェクトを作成していく。
npm create hono@latest

R2のバケットを作成する
何やら開発環境ではふたつのバケットが必要なようなので以下を2回実行。
片方にpreview的なサフィックスをつけた。
npx wrangler r2 bucket create <YOUR_BUCKET_NAME>
紐付ける
不要なコメントアウトを削除しつつ、R2の作成したバケットとの紐付けを行なっていく。
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "<YOUR_PROJECT_NAME>",
"compatibility_date": "2025-02-08",
"pages_build_output_dir": "./dist",
"compatibility_flags": [
"nodejs_compat"
],
"r2_buckets": [
{
"binding": "<YOUR_BINDING_NAME>",
"bucket_name": "<YOUR_BUCKET_NAME>",
"preview_bucket_name": "<YOUR_PREVIEW_BUCKET_NAME>"
}
]
}

とりあえず実現したいこと
画像へのGETリクエストがあった時にR2から配信したい。
import { css } from 'hono/css'
import { createRoute } from 'honox/factory'
import Counter from '../islands/counter'
const className = css`
font-family: sans-serif;
`
export default createRoute((c) => {
const name = c.req.query('name') ?? 'Hono'
return c.render(
<div class={className}>
<h1>Hello, {name}!</h1>
<Counter />
{/* ↓追加↓ */}
<img src="/images/xxx.jpeg" alt="" />
</div>,
{ title: name }
)
})

見つけた方法
APIのエンドポイントを作成できるらしいので一旦作ってみる
一旦作成
GETとPUTの処理は一旦元記事から拝借して作成。
// app/routes/about/index.ts
import { Hono } from 'hono'
// TypeScriptの型を指定
type Bindings = {
<YOUR_BINDING_NAME>: R2Bucket
}
const app = new Hono<{
Bindings: Bindings
}>()
app.get('/:filename', async(c) => {
const object = await c.env.<YOUR_BINDING_NAME>.get(c.req.param('filename'))
if (!object) {
return c.notFound()
}
const body = await object.arrayBuffer()
return c.body(body, 200, {
'Content-Type': object.httpMetadata?.contentType ?? 'image/jpeg'
})
})
app.put('/upload', async(c)=>{
console.log(await c.env.<YOUR_BINDING_NAME>.list())
// フォームリクエストから画像と名前を取得
const { file, name } = await c.req.parseBody<{ file: File; name: string }>()
// R2バケットに置く
const result = await c.env.<YOUR_BINDING_NAME>.put(name, file, {
httpMetadata: {
// メタデータでContent Typeを指定
contentType: file.type
}
})
return c.json(result)
})
export default app
POSTMANを使用してPUTリクエストを送信する
VSCodeのPOSTMAN拡張機能を使用して適当な画像ファイルを送信してみる。
配信できたっぽい
ひとまず配信はできたっぽい。
次はキャッシュの設定をしていく。

キャッシュを効かせる
Workers内ではサービスワーカーにあるCache相当のAPIが実装されており、それを使うことでレスポンスにキャッシュを効かせることができます。
ということで元記事から拝借したものを入れてみる。
// workers-typeに存在していたcachesをひとまずインポート
import { caches } from '@cloudflare/workers-types/experimental'
// 中略
app.get('*', async (c, next) => {
// URLをキーにする
const cacheKey = c.req.url
// キャッシュオブジェクト
const cache = caches.default
// キャッシュされたレスポンスを取得
const cachedResponse = await cache.match(cacheKey)
// もしキャッシュが存在したら返却
if (cachedResponse) {
return cachedResponse
}
await next()
if (!c.res.ok) {
return
}
// Cache-Controlヘッダの値によってキャッシュする時間を設定
c.header('Cache-Control', 's-maxage=60')
// waitUntilを使って非同期でキャッシュをセット
const res = c.res.clone()
c.executionCtx.waitUntil(cache.put(cacheKey, res))
})
// 中略
エラーとなる
そううまくはいかないですよね。
ここで一旦お手洗いに行きたくなったので終了。
Cannot read properties of undefined (reading 'default')
experimentalだからローカル環境ではまだ使用できない説もある。

ちょっと理解
workers.devとlocalhostでは動作しなくて、
カスタムドメインをR2に割り当てる必要があるのか
Custom domains
When a custom domain is connected to your bucket, the contents of your bucket will be made publicly accessible through that domain. Websites connected can also benefit from Cloudflare features such as bot management, Access, and Cache.

そもそもPagesにデプロイされるけど・・・
Workers的な処理ができるのかというところを疑問に思ったけど。
HonoXのビルドのエントリーポイントは_workers.jsになるらしいから
おそらくAdvanced modeなるものでPages Functionが動作するらしいから大丈夫でしょう
Cloudflare PagesでR2を使用する
Pages FunctionでR2を使用している例もあるから大丈夫だろうと思う。
あとはカスタムドメインでキャッシュが使えるか

期間が少し空いたけど続けてはいます
再度確認してみる
experimentalでインポートは有効じゃないけど、デプロイはできるのかなと検証してみた。
入れたのは以下のコード
app.get('*', async (c, next) => {
// URLをキーにする
const cacheKey = c.req.url
// キャッシュオブジェクト
const cache = caches.default
// キャッシュされたレスポンスを取得
const cachedResponse = await cache.match(cacheKey)
// もしキャッシュが存在したら返却
if (cachedResponse) {
return cachedResponse
}
await next()
if (!c.res.ok) {
return
}
// Cache-Controlヘッダの値によってキャッシュする時間を設定
c.header('Cache-Control', 's-maxage=3600')
// waitUntilを使って非同期でキャッシュをセット
const res = c.res.clone()
c.executionCtx.waitUntil(cache.put(cacheKey, res))
})

ページ確認
設定したCache-Controlもついているし、
Cf-Cache-StatusもHITになっているから大丈夫そう