楽天APIの仕様変更に振り回されながら、既存Next.jsアプリに「検索結果1件だけのEC」を追加した話
作ったもの
「検索した言葉に対して商品を1件だけ表示する」ECサイト機能を、個人運営の分析ツールサイト(Next.js App Router)に追加しました。
技術スタック
- Next.js(App Router)/ TypeScript / Tailwind CSS
- 楽天市場商品検索API(Rakuten Ichiba Item Search API, 2026-07-01版)
- Vercelにデプロイ
既存サイトに「広告もヘッダーも無い」ページを追加する
サイト全体のルートレイアウトには共通ヘッダー・フッター・Google AdSenseが仕込まれています。今回の機能は「装飾ゼロ」が要件だったため、これをそのまま継承するわけにはいきませんでした。
Next.jsのRoute Groupsを使い、既存の全ページを(main)グループに、今回の機能を独立した(shop)グループに分離。それぞれが個別の<html><body>を持つ「複数ルートレイアウト」構成にすることで、URLパスは一切変えずに済ませています。
src/app/
├─ (main)/ ← 既存ページ一式(ヘッダー・広告あり)
│ └─ layout.tsx
└─ (shop)/ ← 今回の機能(装飾なし)
└─ layout.tsx
└─ shop/page.tsx
楽天APIの仕様変更に振り回された話
実装当初はapplicationIdだけで動くと思っていましたが、実際に本番キーで叩くと400 wrong_parameter。調べると、APIが新版に切り替わっておりaccessKeyが別途必須になっていました。
さらにaccessKeyを足しても403 REQUEST_CONTEXT_BODY_HTTP_REFERRER_MISSING。こちらはサーバーサイドからのfetchにはRefererが自然に付かないことが原因で、RefererとOriginの両方を明示的に付与することで解決しました。
const res = await fetch(endpoint, {
headers: {
Referer: "https://example.com/shop",
Origin: "https://example.com",
},
});
ドキュメントの記述と実際のレスポンス構造(Itemsのキー名やネスト有無)にも差異があり、最終的には実際に叩いたレスポンスを正として型定義を組み直しました。
「1件だけ」をどう選ぶか
検索APIの既定ソート(sort=standard)は非公開の関連度アルゴリズムで、広告要素が混ざっている可能性を排除できません。そこで、hits=30で複数件取得した上で、
const qualifying = items
.filter((item) => item.reviewCount >= 100)
.sort((a, b) => b.reviewAverage - a.reviewAverage);
const chosen = qualifying[0] ?? items[0]; // 該当なしはレビュー最多にフォールバック
という形で、サーバー側でレビュー件数・評価点による絞り込みを行い、フロントには常に1件だけを渡しています。
和文フォントとnext/font/google
デザイン上、明朝体を使いたかったのですが、next/font/googleは和文グリフを含むjapaneseサブセットに対応していませんでした(自前ホストするには大きすぎるため)。外部フォント読み込みを増やすよりも、システムに入っている明朝体(游明朝・ヒラギノ明朝ProN等)をそのまま使う方針に切り替えています。
まとめ
既存の大きめなNext.jsアプリに、デザイン・要件が全く異なる機能を「間借り」させる形で追加するケースは意外と多いと思います。Route Groupsによる複数ルートレイアウトは、こういう場面で地味に効きます。