12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Next.js × Cloudflare】結婚式のアルバムに入りきらなかった写真をWebアルバムにした話

Last updated at Posted at 2023-12-11

今年の5月に結婚式をしました :tada:

結婚式の前には、Web招待状も作り、やりきった気持ちでいましたが、気づけばもう年末が近づいているではないですか

年末、実家に帰ったら

:woman_tone1: アルバムは?

と母親に聞かれるに決まっております。
ということで、重い腰を上げてアルバム作りに勤しみました。

とりあえず、紙媒体のアルバムも作るとして、せっかくなので、入りきらない写真たちはWebアルバムにしてみようかなという企画でございます。

どんなものを作ったか

結婚式の写真は流石に恥ずかしいので、我が家の猫に出場してもらいます。

gallery.gif

画像を30枚ずつフェッチする形にしていますが、結婚式の画像はある程度良い画質のもので提供したいので、データフェッチが終わるまではブラーを表示するようにしています。

あとは、そこまで凝ったことはしていませんが、30枚を読み込んだあと、普通にボタンを押してもらうとさらに読み込むような形にしています。

使用技術

  • Next.js v14.0.3
  • Cloudflare Pages
  • Cloudflare Images

フロントエンドには慣れているので、Next.jsを使用しました。
あとは、Next.jsをどうホスティングするかですが、できる限り料金を下げたかったことと、画像数が1000枚を超えそうだったので、Vercelではなく、Cloudflareでホスティングすることにしました。

工夫ポイント

1. Cloudflare Pagesへのデプロイ

以下のライブラリを用いました。

概ね以下のドキュメントの通りなので、大きなつまりポイントなくリリースすることができました。

2. Cloudflare Imagesでの画像最適化

Cloudflare Imagesは勝手にWebPへの変換や、リサイズを行なってくれるため、とても便利です。

Cloudflare Imagesにアップロードした画像は以下のようなAPIで一覧が取得できるため、Next.jsのサーバーサイドでデータを一覧データを30件ずつ取得するようにしました。

  const { searchParams } = new URL(request.url)
  const continuationToken = searchParams.get('continuation_token')
  const perPage = searchParams.get('per_page')
  const params = {
    per_page: perPage || '30',
    ...(continuationToken && { continuation_token: continuationToken }),
    sort_order: 'desc',
  }
  const res = await fetch(
    `${process.env['CLOUDFLARE_URL']}/images/v2?${new URLSearchParams(params)}`,
    {
      headers: { Authorization: `Bearer ${process.env['CLOUDFLARE_API_KEY']}` },
      cache: 'no-cache',
    }
  )
  if (!res.ok) {
    throw new Error('Failed to fetch image urls')
  }
  const json = await res.json()
  let parsed
  try {
    parsed = Schema.parse(json)
  } catch (err) {
    return NextResponse.json({}, { status: 404 })
  }

一気に10000件まで取得できるようですが、後続の処理でブラー画像をフェッチするために、サブリクエストを発行する必要があり、Cloudflare Workersの制限に引っかかってしまうため、今回は30件ずつ取得する形で実装しています。

フロントエンド側はLoaderを設定し、next/imageでの最適化は行わず、Cloudflare側に画像の最適化を直接してもらうようにしています。

const loader: ImageLoader = ({ src }) => {
  return `https://imagedelivery.net/##account_identifier##${src}`
}

...
  <div className="grid gap-4">
    {items.map((image) => {
      return (
        <Image
          key={image.id}
          className="h-auto max-w-full rounded-lg"
          src={`/${image.id}/public`}
          loader={loader}
          width={1366}
          height={768}
          alt={String(image.id)}
        />
      )
    })}
  </div>

本当は、レイアウトシフトを防ぐため、ちゃんとwidth、heightを指定する必要があります。
今回、そこまでAPIを作り込めず、レイアウトシフトが発生しておりますが、またどこかで修正しようと思います。

3. ブラー画像の表示

今回、画像が読み込めるようになるまでの間、ブラーのかかった画像を表示したかったので、Variantsという機能を使って、ブラー画像を生成するようにしました。

管理画面上で設定できますし、どうせブラーのかかった状態で表示するので、とても小さい画像で生成するようにしました。

スクリーンショット 2023-12-11 23.15.40.png

また、next/imageでは、ブラー画像URLをbase64にエンコードしたデータとして渡す必要があるため、Cloudflareから画像の一覧をフェッチしたあとに、ブラー画像をPromise.allで取得、base64のURLに変換しています。

const cache = new Map<Props, string>()

async function getBase64ImageUrl(image: Props): Promise<string> {
  let url = cache.get(image)
  if (url) {
    return url
  }
  const response = await fetch(
    `https://imagedelivery.net/##account_identifier##/${image.id}/blur`
  )
  const buffer = await response.arrayBuffer()

  url = `data:image/jpeg;base64,${Buffer.from(buffer).toString('base64')}`
  cache.set(image, url)
  return url
}

...

const blurImagePromises = parsed.result.images.map((image) => {
  return getBase64ImageUrl({ id: image.id })
})

const imagesWithBlurDataUrls = await Promise.all(blurImagePromises)

あとは、この生成したURLをblurDataURLオプションとして渡し、placeholderオプションをblurに設定することで、画像取得中、ブラー画像を表示してくれます。

...
  <div className="grid gap-4">
    {items.map((image) => {
      return (
        <Image
          key={image.id}
          className="h-auto max-w-full rounded-lg"
          src={`/${image.id}/public`}
          blurDataURL={image.blurImageUrl}
          placeholder="blur"
          loader={loader}
          width={1366}
          height={768}
          alt={String(image.id)}
        />
      )
    })}
  </div>

まとめ

いかがでしたでしょうか?

今回はCloudflare PagesでNext.jsをホスティングし、Cloudflare Imagesを用い、画像のリサイズやブラー処理を行いました。

クライアントからの情報に応じて、自動的に画像をリサイズしたりする場合は、Cloudflare Image Resizingだったり、Flexible Variantsとかを使うとできたりしそうなので、またそれも使ってみたいなと思います。

ただ、その前にレイアウトシフトを無くしたり、無限スクロールを実装したりと年末までにもう少しブラッシュアップしようかと思うので、また気合があれば記事を書こうかと思います。

12
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?