🐱

[Next.js + App Router]DBの値から動的にmetadataを設定する

2023/07/14に公開

前書き

  • App Routerで、DBの値からmetadataを設定する書き方について、有用な記事を見つけられなかったので、自分なりのベストプラクティスを記します。

結論

  • ORMにprismaを使用しています
src/app/todo/[id]/page.tsx
import { Todo } from '@/components/pages/Todo';
import { prisma } from '@/lib/prisma';
import { Metadata } from 'next';
import { cache } from 'react';

type Props = {
  params: {
    id: string;
  };
};

const getTodo = cache(async (id: number) => {
  const todo = await prisma.todo.findUniqueOrThrow({ where: { id } });
  return todo;
});

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const todo = await getTodo(Number(params.id));
  return { title: todo.name };
}

const TodoPage = async ({ params }: Props) => {
  const todo = await getTodo(Number(params.id));
  return <Todo todo={todo} />;
};

export default TodoPage;

Point

解説

Pages Router(従来)では

以下のように直感的に書ける。

import Head from 'next/head'

const TodoPage = async ({ params }: Props) => {
  const todo = await getTodo(Number(params.id));
  return (
    <>
      <Head>
        <title>{todo.name}</title>
      </Head>
      <Todo todo={todo} />
    </>;
  );
};

export default TodoPage;

App Routerでは

generateMetadataを使用して、シンプルに書くと以下のようになる。

const getTodo = async (id: number) => {
  const todo = await prisma.todo.findUniqueOrThrow({ where: { id } });
  return todo;
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const todo = await getTodo(Number(params.id));
  return { title: todo.name };
}

const TodoPage = async ({ params }: Props) => {
  const todo = await getTodo(Number(params.id));
  return <Todo todo={todo} />;
};

但し、この書き方だと、クエリを2回発行してしまいよろしくない。

実際に発行されたクエリ↓

- wait compiling /todo/[id]/page (client and server)...
- event compiled client and server successfully in 1166 ms (432 modules)
- wait compiling...
- event compiled successfully in 91 ms (211 modules)
prisma:info Starting a postgresql pool with 17 connections.
# 1回目
prisma:query SELECT "public"."Todo"."id", "public"."Todo"."name", "public"."Todo"."created_at", "public"."Todo"."updated_at" FROM "public"."Todo" WHERE ("public"."Todo"."id" = $1 AND 1=1) LIMIT $2 OFFSET $3
# 2回目
prisma:query SELECT "public"."Todo"."id", "public"."Todo"."name", "public"."Todo"."created_at", "public"."Todo"."updated_at" FROM "public"."Todo" WHERE ("public"."Todo"."id" = $1 AND 1=1) LIMIT $2 OFFSET $3
- wait compiling /favicon.ico/route (client and server)...

cache関数を使う

// new!!
const getTodo = cache(async (id: number) => {
  const todo = await prisma.todo.findUniqueOrThrow({ where: { id } });
  return todo;
});

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const todo = await getTodo(Number(params.id));
  return { title: todo.name };
}

const TodoPage = async ({ params }: Props) => {
  const todo = await getTodo(Number(params.id));
  return <Todo todo={todo} />;
};

実際に発行されたクエリ↓

- wait compiling /todo/[id]/page (client and server)...
- event compiled client and server successfully in 1127 ms (432 modules)
- wait compiling...
- event compiled successfully in 87 ms (211 modules)
prisma:info Starting a postgresql pool with 17 connections.
// 1回のみ👏
prisma:query SELECT "public"."Todo"."id", "public"."Todo"."name", "public"."Todo"."created_at", "public"."Todo"."updated_at" FROM "public"."Todo" WHERE ("public"."Todo"."id" = $1 AND 1=1) LIMIT $2 OFFSET $3
- wait compiling /favicon.ico/route (client and server)...

後書き

  • DBの値からtitleを生成したいケースは割とあるかなと思い、記事にしてみました。
  • ご指摘、ご提案などございましたらお気軽にコメントお願いします🙏

Discussion