Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

Next.jsのmetadata.titleがlayoutのtitle.templateと二重に表示される

公開 読了時間 約3分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

親layoutでtitle.templateを設定済みの配下では、子ページのmetadata.titleに同じ接尾辞を含む文字列を書くと、テンプレートの%sにその文字列全体が代入され、タイトルタグの接尾辞が二重に表示される。

結論

親layoutでtitle.templateを設定している配下では、子ページのmetadata.titleに同じ接尾辞を含む文字列をそのまま書くと、タイトルタグにその接尾辞が二重に表示される。 テンプレートは子ページが返した文字列を検査せず、%sへそのまま代入するだけだからだ。直し方は子ページのtitleから接尾辞を取り除き、付与そのものをlayout側のテンプレートに委ねること。

症状

Next.jsのApp Routerでは、親のlayout.tsxにmetadata.titleをオブジェクト形式で設定し、templateを使うと、配下の各ページが返したtitleの末尾に共通の接尾辞を自動で付けられる。

// app/layout.tsx
export const metadata: Metadata = {
  title: {
    default: "〇〇教室 東京",
    template: "%s | サイト名",
  },
  // ...
};

この状態で、採用ページ側のmetadata.titleに次のように書いていた。

// app/recruit/page.tsx(修正前)
export const metadata: Metadata = {
  title: "アルバイト募集(講師) | サイト名",
  description: "...",
};

このページだけブラウザのタブやGoogleの検索結果に表示されるタイトルが「アルバイト募集(講師) | サイト名 | サイト名」のように、同じ接尾辞が2回続いて表示される。

原因

template: "%s | サイト名"は、子ページが返したtitleの文字列をそのまま%sへ代入するだけの単純な置換で、代入する文字列の中身は一切見ていない。子ページのtitleに接尾辞を含めていなければ%sには接尾辞を含まない文字列だけが入り、結果として「本文 | サイト名」という1回だけの形になる。

しかし採用ページのtitleには、すでに| サイト名という接尾辞そのものの文字列が含まれていた。このため%sにはこの文字列全体がそのまま代入され、テンプレートが持つ| サイト名と合わせて、タイトル全体では接尾辞に相当する部分が2回現れる形になった。

テンプレートの仕組み自体は設計どおりに動いている。壊れていたのは「子ページ側は接尾辞を書かない」という前提が、このページ1つだけ守られていなかった点にある。

直す

子ページのtitleから接尾辞の部分を取り除き、接尾辞の付与をlayout側のtemplateだけに任せる。

// app/recruit/page.tsx(修正後)
export const metadata: Metadata = {
  title: "アルバイト募集(講師)",
  description: "...",
};

layout.tsx側のtemplate: "%s | サイト名"はそのままで、子ページが返す文字列から接尾辞を抜いただけで直る。templateを外したり書き換えたりする必要はない。テンプレートは正しく機能していたので、触るべきは常に「代入される側」である子ページのtitleのほうだった。

再発しない形にする

title.templateを使う設計は、「子ページは接尾辞を書かない」という前提を、コードの型やコメントでは強制できない。metadata.titleは単なる文字列なので、どんな内容を入れてもTypeScriptの型チェックは通ってしまう。

この種の事故を構造的に防ぐ方法は、子ページのtitleを書く段階でテンプレートの接尾辞を意識させないことに尽きる。レビューや執筆の際に「このページのタイトルは最終的にどう表示されるか」をプレビューで確認する、あるいは接尾辞を含む文字列が子ページ側に紛れ込んでいないかを目視で見る、という当たり前の確認が、テンプレートを設定した時点でこそ一度必要になる。

よくある質問

Q1なぜtitle.templateを設定しているのに接尾辞が二重に付くのですか?

親layoutのtitle.templateは%sへ子ページのmetadata.titleをそのまま代入するだけで、子ページ側の文字列から重複する部分を取り除く処理はしていません。子ページのtitleに接尾辞を含む文字列を書いてしまうと、その文字列ごと%sへ入り、テンプレートが元々持っている接尾辞と合わせて二重に表示されます。

Q2直し方はtitle.templateを外すことですか?

いいえ。テンプレート自体は正しく機能しているので外す必要はありません。直すのは子ページのmetadata.titleのほうで、接尾辞を含めずに書き、接尾辞の付与をlayout側のtemplateに委ねるだけで解消します。

Q3この問題は他のページにも起きますか?

親layoutでtitle.templateを設定しているサイト全体に共通する注意点です。子ページ側でtitleを個別に書くときに、テンプレートが付ける接尾辞と同じ文字列を含めてしまえば、どのページでも同じ形で二重に表示されます。

確認した環境

  • Next.js 15.4 / App Router
  • 2026-07-08 に修正

この記事の根拠

  • TypeScriptファイル 9〜9行目コミット 403e01a
  • TypeScriptファイル 9〜9行目コミット f3ed55e
  • TypeScriptファイル 16〜19行目コミット f3ed55e

本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。