すべての記事

エンジニアリング

Next.jsでローカライズされたコンテンツを取得する

キャッシュと再検証を備えた、公開ページAPI向けの小さなクライアント。

Huy Phạm

· 5分で読めます

tixigen で管理しているページの文言は、公開ページAPIを通じてどのフロントエンドからでも取得できます。この記事では、Next.js の App Router で市場とロケールに応じたフィールドを取得し、キャッシュと再検証で「速さ」と「新しさ」を両立させる小さなクライアントの作り方を紹介します。対象は、ヘッドレス構成のサイトを担当する開発者と、公開から反映までの流れを把握しておきたいコンテンツ担当者です。

公開ページAPIの基本

公開APIは読み取り専用で、ETagによるキャッシュに対応しています。ページのフィールドは次のエンドポイントで取得します。

  • GET /api/v1/public/pages/{market}/{pageKey}?locale={locale}
  • market: vn、ph、id のいずれか。
  • pageKey: home、about、services、pricing、contact など、tixigen で管理しているページのキー。
  • locale: vi、fil、id、en のいずれか。対応していない値や省略時は市場の既定ロケールが使われます。
bash
curl "https://tixigen.com/api/v1/public/pages/ph/home?locale=fil" \
  -H "Accept: application/json"

# 200 OK
# {
#   "fields": {
#     "home.hero.headline": "Padaliang mundo, isang tap lang",
#     "home.hero.ctaLabel": "Mag-quote"
#   }
# }

# Unknown market: 400
# { "error": { "code": "invalid_market", "message": "Unknown market \"xx\"" } }

レスポンスは fields オブジェクトだけのシンプルな形で、キーは home.hero.headline のようなドット区切りのフィールドキー、値はすべて文字列です。未知の market や pageKey を指定すると、error.code に invalid_market または invalid_page を含む400エラーが返ります。

型付きの小さなクライアントを作る

取得処理は1つのファイルにまとめ、ページやコンポーネントからは関数を呼ぶだけにしておくと、キャッシュ方針の変更やエラー処理を一箇所で管理できます。市場とロケールは文字列リテラル型で表現しておくと、存在しない組み合わせをコンパイル時に防げます。

ts
// lib/cms.ts
const BASE = "https://tixigen.com";

export type Market = "vn" | "ph" | "id";
export type Locale = "vi" | "fil" | "id" | "en";
export type PageFields = Record<string, string>;

export async function getPage(
  market: Market,
  pageKey: string,
  locale: Locale,
): Promise<PageFields> {
  const url = `${BASE}/api/v1/public/pages/${market}/${pageKey}?locale=${locale}`;
  const res = await fetch(url, {
    headers: { Accept: "application/json" },
    next: {
      revalidate: 300,
      tags: [`page:${market}:${pageKey}`],
    },
  });

  if (!res.ok) {
    throw new Error(`CMS ${res.status} for ${market}/${pageKey}`);
  }

  const { fields } = (await res.json()) as { fields: PageFields };
  return fields;
}

next.revalidate は時間ベースの再検証、next.tags は後述するオンデマンド再検証のためのタグです。タグ名を page:{market}:{pageKey} にしているのは、Studio が公開時に通知するタグと同じ形式に揃えるためです。

Server Componentでフィールドを描画する

取得はServer Componentで行います。APIの呼び出しがサーバー側で完結するため、ブラウザに余計なJavaScriptを送らずに済み、検索エンジンにも完成したHTMLが渡ります。Next.js の最近のバージョンでは params がPromiseになっている点に注意してください。

tsx
// app/[market]/page.tsx
import { getPage, type Market } from "@/lib/cms";

export default async function Page({
  params,
}: {
  params: Promise<{ market: Market }>;
}) {
  const { market } = await params;
  const fields = await getPage(market, "home", "en");

  return (
    <Hero
      eyebrow={fields["home.hero.eyebrow"]}
      title={fields["home.hero.headline"] ?? ""}
      cta={fields["home.hero.ctaLabel"]}
      href={fields["home.hero.ctaHref"] ?? "/contact"}
    />
  );
}
APIはフィールド単位で「要求したロケール → 市場の既定ロケール → en」の順にフォールバックします。どのロケールにも値がないフィールドはレスポンスに含まれないため、描画側では ?? で既定値を用意しておくと安全です。

キャッシュと再検証の考え方

キャッシュは2つの層で考えると整理しやすくなります。1つ目はAPI側の層で、レスポンスには共有キャッシュ向けの Cache-Control(s-maxage と stale-while-revalidate)とETagが付きます。If-None-Match を送ったリクエストの内容が変わっていなければ、304が返り本文の転送が省かれます。2つ目はNext.js側のデータキャッシュで、fetch の next.revalidate と next.tags で制御します。

  • 時間ベースの再検証: 指定した秒数が過ぎた後の最初のリクエストで、裏側で新しいデータを取得します。設定が簡単で、通知の仕組みがなくても動きます。
  • オンデマンド再検証: 公開のタイミングでタグを無効化します。編集者が公開した直後に反映させたい場合に向いています。
オンデマンド再検証を導入していても、時間ベースの revalidate は残しておくと安心です。通知が届かなかった場合でも、一定時間後には最新の内容に追いつきます。

公開と同時に反映させる

tixigen でページのフィールドを公開すると、あらかじめ設定したサイト側の再検証URLに、tag と secret をクエリパラメータとして付けたPOSTリクエストが送られます。ページのフィールドなら tag は page:{market}:{pageKey} の形式です。サイト側では secret を検証し、そのタグを無効化するRoute Handlerを用意します。

ts
// app/api/revalidate/route.ts
import type { NextRequest } from "next/server";
import { revalidateTag } from "next/cache";

export async function POST(req: NextRequest) {
  const secret = req.nextUrl.searchParams.get("secret");
  const tag = req.nextUrl.searchParams.get("tag");

  if (!secret || secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ revalidated: false }, { status: 401 });
  }
  if (!tag) {
    return Response.json({ revalidated: false }, { status: 400 });
  }

  revalidateTag(tag, "max");
  return Response.json({ revalidated: true, tag });
}
secret は環境変数で管理し、リポジトリにコミットしないでください。また、最近のNext.jsでは引数1つの revalidateTag(tag) は非推奨です。第2引数に "max" を渡すと、再取得が終わるまでは古い内容を返しつつ、裏側で更新されます。

本番運用のチェックリスト

  • 取得はServer Componentまたはサーバー側の処理に限定し、クライアントから直接APIを呼ばない。
  • res.ok を確認し、エラー時にはページ全体を壊さないフォールバック表示を用意する。
  • 描画に必須のフィールドには ?? で既定値を持たせる。
  • 再検証URLの secret を環境変数で管理し、定期的に入れ替える。
  • UI文言は /api/v1/public/translations/{market}/{locale} から同じ方針で取得する。

取得処理を1つのクライアントにまとめ、時間ベースとオンデマンドの再検証を組み合わせれば、編集者が公開した内容はすぐに反映され、訪問者には常にキャッシュされた速いページが届きます。まずはトップページの1セクションから接続し、公開から反映までの流れを確認してみてください。

執筆

Huy Phạm

tixigen チーム

この運用を、実際の画面で。

市場とワークフローに合わせてデモをご案内します。

デモを依頼