Closed3

【調査中】App Router で取得したデータによる Cache-Control の制御ができない?

Yo IwamotoYo Iwamoto

やりたいこと

fetch したデータに応じて、Response Header の Cache-Control を切り替える。

例えば記事投稿サービスで、公開範囲が「全員に公開」または「会員のみに公開」の2種類がある記事データがあったとします。

types/Post.ts
export type Post = {
  ...
  visibility: 'public' | 'users-only';
}

キャッシュ戦略は色々あると思いますが、ここではシンプルに以下を前提とします。

  • 経路上に CDN
  • 「全員に公開」の記事については30秒間キャッシュさせる
  • 「会員のみに公開」の記事についてはキャッシュさせない

また、前提条件ではありませんが自分の気持ちとして、self-host で Next.js を使う前提で、可能な限り Next.js サーバー側にキャッシュを握られたくないというスタンスがあります。
Fastly などの CDN を活用する場合、オリジンとの多段キャッシュが生じてしまい管理が複雑化することを避けたいというのが理由です。

Yo IwamotoYo Iwamoto

pages での実装

pages/posts/[id]/index.tsx
import { getPost } from '@/queries/getPost';
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next';

export const getServerSideProps = (async ({ res, params }) => {
  // get path parameter
  const id = params?.id;
  if (typeof id !== 'string') return { notFound: true };

  // fetch data
  const post = await getPost(id);

  // set Cache-Control header depends on visibility
  res.setHeader(
    'Cache-Control',
    post.visibility === 'users-only'
      ? 'private, no-store, max-age=0, must-revalidate'
      : 'public, max-age=30',
  );

  return { props: { post } };
}) satisfies GetServerSideProps;

type PageProps = InferGetServerSidePropsType<typeof getServerSideProps>;

export default function Page({ post }: PageProps) {
  return <h1>{post.title}</h1>;
}

pages では上記のように、gSSP で取得したデータの visibility を見て、レスポンスヘッダの Cache-Control を切り替えるということができました。

'public' の記事ページ

❯ curl -IsS http://localhost:3000/posts/1 | grep cache-control                      
cache-control: public, max-age=30

'users-only' の記事ページ

❯ curl -IsS http://localhost:3000/posts/2 | grep cache-control
cache-control: private, no-store, max-age=0, must-revalidate

データに応じて確かに Cache-Control の値が変わっています。

Yo IwamotoYo Iwamoto

App Router の場合

App Router は、pages のページ単位のリクエストとはモデルが違い、データ取得は基本的に Server Component (以後 SC)に結びついていて、SC 単位で柔軟にキャッシュ制御を行います。
そのため、SC でのデータ取得のキャッシュをレスポンスヘッダの Cache-Control で制御する、のようなことはできません。

Middleware で対処

SC はコンポーネントレベルですが、Middleware なら、App Router であってもリクエストごとのレスポンスヘッダに触れることができます。
そのため、例えば安易に、記事情報を取得して visibility によってヘッダーを変える、という処理を Middleware に実装する場合、以下のように書けます。

middleware.ts
import { getPost } from './queries/getPost';
import { NextResponse } from 'next/server';
import type { NextMiddleware } from 'next/server';

export const config = {
  matcher: '/posts/:path*',
};

const middlewareFn = (async (req) => {
  const res = NextResponse.next();

  const matches = req.nextUrl.pathname.match(/\/posts\/([^?]+)/);
  if (matches === null) throw new Error();

  const postId = matches[1];
  const post = await getPost(postId);

  res.headers.set(
    'Cache-Control',
    post.visibility === 'users-only'
      ? 'private, no-store, max-age=0, must-revalidate'
      : 'public, max-age=30',
  );

  return res;
}) satisfies NextMiddleware;

export default middlewareFn;
このスクラップは2023/06/21にクローズされました