本サイトで採用している2023年7月時点での構成と問題点
- フロントエンド:Next.js 13.4.7(App Directory使用)
- バックエンド(ヘッドレスCMS):Notion API
- デプロイ先:Vercel
microCMSもAPIの候補に挙がりましたが、Notion APIを使用したブログ構築が比較的体験としてよかったので、Notion APIをバックエンド(ヘッドレスCMS)として採用しブログ構築を開始しました。
ただ、ここで問題になったのが、Notion APIから取得した画像が期限付きURLで且つ、その期限が1時間ということでした。
期限付きURLに対しての対策
主に下記の対策が候補として挙がると思います。
1, 画像を表示させるページをSSR(サーバーサイドレンダリング)で実装する
本サイトはパフォーマンスの観点から基本的にはSG(静的生成)で構築することを前提としていました。
そのため、この方法は却下しました。
2, ISRの採用
今後デプロイ先をAWSに移行することを予定しているのと、ISRによって画像を再度取得するタイミングによっては画像の取得に失敗して表示されないケースが考えられます。なのでこの方法も却下しました。
3, 画像だけ別途S3などのストレージにアップロードその画像データを使用
説明を省きますが、これは論外だったので却下しました。
その他、色々と対策はあるとは思いますが、SGを維持しつつ、NotionのGUIの更新だけで済むようにしたいということを踏まえ、Next.jsのApp Routerの機能を使い、画像取得先URLにキャッシュ制御を設定することで対応をしました。
ただし、最後に書きますがこの方法落とし穴があります。
実装方法
通常ですと、Notion APIで取得した画像URLをimageタグもしくはImageコンポーネントに設定すると思います。
通常の実装例<Link href='任意のパス'>
<Image
src='期限付き画像URL'
alt=''
style={{ objectFit: 'cover' }}
width={400}
height={225}
quality={30}
priority
/>
</Link>このような実装から下記の追加実装していきます。
- 画像取得用のプロキシーをApp Routerで作成
- 画像取得用のcomponentを作成
画像取得用のプロキシーをApp Routerで作成
src/app/api/imageProxy/route.ts
import axios from 'axios';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest, res: NextResponse) {
const imageUrl = req.nextUrl.searchParams.get('imageUrl');
if (!imageUrl || typeof imageUrl !== 'string') {
return NextResponse.json({ error: 'imageUrl query parameter is require' }, { status: 400 });
}
try {
const response = await axios.get(imageUrl, {
// 画像データをバイナリ形式に変換
responseType: 'arraybuffer'
});
const headers = new Headers();
headers.set('Content-Type', response.headers['content-type']);
// キャッシュを1年間保持
headers.set('Cache-Control', 'public, max-age=31536000');
return new NextResponse(response.data, { status: 200, headers });
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch image' }, { status: 500 });
}
}- imageUrlクエリパラメーターが存在する場合、axios.getを使用してそのURLから画像を取得します。レスポンスタイプはjson形式ではなく、arraybufferを指定しています。
- 画像の取得に成功した場合、その画像データをレスポンスとして返します。レスポンスヘッダには、取得した画像のContent-Typeと、キャッシュ制御の設定(最大1年間キャッシュを保持)が含まれます。※キャッシュ保持期間は任意です。
- その他、エラーハンドリング処理を実装しています。
このコードでは、Cache-Controlヘッダーの値にpubicを指定しています。publicを指定することにより、ブラウザ(プライベートキャッシュ)だけでなく、CDNによってもキャッシュできることを示します。
そのため、画像データはブラウザとCDNの両方にキャッシュされ、約1年間はキャッシュから取得されます。
画像取得用のcomponentを作成
/src/components/imageproxy.tsx
import axios from 'axios';
import Image from 'next/image';
import { NextRequest, NextResponse } from 'next/server';
import { FC } from 'react';
// 型定義の詳細については省略します
import type { ImageProxyType } from '@/types/types';
// propsの詳細については省略します
const ImageProxy: FC<ImageProxyType> = ({
className,
imageUrl,
alt,
objectFitStyle,
width,
height,
quality
}) => {
const proxyUrl =
imageUrl === '/noimage.png'
? '/noimage.png'
: `/api/imageProxy?imageUrl=${encodeURIComponent(imageUrl)}`;
return (
<Image
className={className}
src={proxyUrl}
alt={alt || ''}
style={objectFitStyle ? { objectFit: objectFitStyle } : undefined}
width={width}
height={height}
quality={quality || 50}
priority
/>
);
};
export { ImageProxy };- imageUrlクエリパラメーターを付与し、App Routerで作成したimageProxy側のURLをimageUrl変数に格納します。
- imageUrl変数をImageコンポーネントのsrcに設定します。
- その他、エラーハンドリング処理を実装しています。
画像表示用のコンポーネントを使用
<Link href='任意のパス'>
{/* // 作成したコンポーネントを使用 */}
<ImageProxy
imageUrl='期限付き画像URL'
alt=''
objectFitStyle={'cover'}
width={400}
height={225}
quality={30}
/>
</Link>あとは、作成したコンポーネント使って画像を表示させるだけです。
ブラウザの検証ツールで確認
実際にvercelにデプロイをし、動作確認をします。

ブラウザキャッシュから画像を取得していることを確認します。

cache-controlの設定が反映されていることを確認します。

Vercel側のCDNでHITしていることを確認します。
落とし穴
CDNの設定によりますが、基本的にはCDNは、一般的に最初のユーザーが特定のコンテンツにアクセスした時点でそのコンテンツをキャッシュします。そのため、初めてのユーザーアクセスがなければ、そのコンテンツはCDNのエッジサーバーにキャッシュされません。
つまり、今回の方法を採用するとなった場合は、SG生成から1時間以内にCDNキャッシュを生成するために各ページにアクセスしないといけません。
非常に厄介です。また改めて対策を考え、このサイトで公開する予定です。