1. 始める前に
この Codelab では、Firebase を Friendly Eats という Next.js ウェブアプリ(レストランのレビュー サイト)と統合する方法を学びます。

完成したウェブアプリには、Firebase を使用して Next.js アプリを構築する方法を示す便利な機能が用意されています。主な機能は次のとおりです。
- 自動ビルドとデプロイ: この Codelab では、Firebase App Hosting を使用して、構成済みのブランチに push するたびに Next.js コードを自動的にビルドしてデプロイします。
- ログインとログアウト: 完成したウェブアプリでは、Google でログインしてログアウトできます。ユーザーのログインと永続性は、すべて Firebase Authentication を通じて管理されます。
- 画像: 完成したウェブアプリでは、ログインしたユーザーがレストランの画像をアップロードできます。画像アセットは Cloud Storage for Firebase に保存されます。Firebase JavaScript SDK は、アップロードされた画像への公開 URL を提供します。この公開 URL は、Cloud Firestore の関連するレストラン ドキュメントに保存されます。
- レビュー: 完成したウェブアプリでは、ログインしたユーザーが星評価とテキスト メッセージで構成されるレストランのレビューを投稿できます。レビュー情報は Cloud Firestore に保存されます。
- フィルタ: 完成したウェブアプリでは、ログインしたユーザーがカテゴリ、場所、価格に基づいてレストランのリストをフィルタできます。使用する並べ替え方法をカスタマイズすることもできます。データは Cloud Firestore からアクセスされ、使用されたフィルタに基づいて Firestore クエリが適用されます。
前提条件
- GitHub アカウント
- Next.js と JavaScript の知識
学習内容
- Next.js App Router とサーバーサイド レンダリングで Firebase を使用する方法。
- Cloud Storage for Firebase で画像を永続化する方法。
- Cloud Firestore データベースでデータを読み書きする方法。
- Firebase JavaScript SDK で Google でログインを使用する方法。
必要なもの
- Git
- Node.js の最新の安定バージョン
- 任意のブラウザ(Google Chrome など)
- コードエディタとターミナルを備えた開発環境
- Firebase プロジェクトの作成と管理に使用する Google アカウント
- Firebase プロジェクトを Blaze のお支払いプランにアップグレードする機能
2. 開発環境と GitHub リポジトリを設定する
この Codelab では、アプリのスターター コードベースを提供し、Firebase CLI を使用します。
GitHub リポジトリを作成する
この Codelab のソースは https://github.com/firebase/friendlyeats-web にあります。このリポジトリには、複数のプラットフォーム用のサンプル プロジェクトが含まれています。ただし、この Codelab では nextjs-start ディレクトリのみを使用します。次のディレクトリに注意してください。
* `nextjs-start`: contains the starter code upon which you build.
* `nextjs-end`: contains the solution code for the finished web app.
nextjs-start フォルダを独自のリポジトリにコピーします。
- ターミナルを使用して、パソコンに新しいフォルダを作成し、新しいディレクトリに移動します。mkdir codelab-friendlyeats-web cd codelab-friendlyeats-web
- giget npm パッケージを使用して、nextjs-startフォルダのみを取得します。npx giget@latest gh:firebase/friendlyeats-web/nextjs-start#master . --install
- git を使用してローカルで変更を追跡します。git init git add . git commit -m "codelab starting point" git branch -M main
- 新しい GitHub リポジトリを作成します(https://github.com/new)。任意の名前を付けます。
- GitHub が作成した新しい URL をコピーします。次のいずれかのようになります。- https://github.com/<USER_NAME>/<REPOSITORY_NAME>.gitまたは
- git@github.com:<USER_NAME>/<REPOSITORY_NAME>.git
 
- 次のコマンドを実行して、ローカルの変更を新しい GitHub リポジトリに push します。<REPOSITORY_URL>プレースホルダを実際のリポジトリ URL に置き換えます。git remote add origin <REPOSITORY_URL> git push -u origin main
- GitHub リポジトリにスターター コードが表示されます。
Firebase CLI をインストールまたは更新する
次のコマンドを実行して、Firebase CLI がインストールされ、バージョンが v14.1.0 以降であることを確認します。
firebase --version
バージョンが低い場合や、Firebase CLI がインストールされていない場合は、インストール コマンドを実行します。
npm install -g firebase-tools@latest
権限エラーが原因で Firebase CLI をインストールできない場合は、npm のドキュメントを参照するか、別のインストール オプションを使用してください。
Firebase にログインする
- 次のコマンドを実行して Firebase CLI にログインします。firebase login 
- Firebase にデータを収集させるかどうかによって、YまたはNを入力します。
- ブラウザで Google アカウントを選択し、[許可] をクリックします。
3. Firebase プロジェクトを設定する
このセクションでは、Firebase プロジェクトを設定し、Firebase ウェブアプリを関連付けます。また、サンプル ウェブアプリで使用される Firebase サービスも設定します。
Firebase プロジェクトを作成する
- 前の手順で使用したのと同じ Google アカウントを使用して、Firebase コンソールにログインします。
- ボタンをクリックして新しいプロジェクトを作成し、プロジェクト名(例: FriendlyEats Codelab)を入力します。
 
- [続行] をクリックします。
- Firebase の利用規約が表示されたら、内容を読み、同意して [続行] をクリックします。
- (省略可)Firebase コンソールで AI アシスタンス(「Gemini in Firebase」)を有効にします。
- この Codelab では Google アナリティクスは必要ないため、Google アナリティクスのオプションをオフに切り替えます。
- [プロジェクトを作成] をクリックし、プロジェクトのプロビジョニングが完了するまで待ってから、[続行] をクリックします。
Firebase の料金プランをアップグレードする
Firebase App Hosting と Cloud Storage for Firebase を使用するには、Firebase プロジェクトが従量課金制(Blaze)のお支払いプランに登録されている必要があります。つまり、Cloud 請求先アカウントにリンクされている必要があります。
- Cloud 請求先アカウントには、クレジット カードなどの支払い方法が必要です。
- Firebase と Google Cloud を初めて使用する場合は、$300 のクレジットと無料トライアル用 Cloud 請求先アカウントを利用できるかどうかご確認ください。
- この Codelab をイベントの一環として行う場合は、利用可能な Cloud クレジットがあるかどうかを主催者に確認してください。
プロジェクトを Blaze プランにアップグレードする手順は次のとおりです。
- Firebase コンソールで、プランをアップグレードします。
- Blaze プランを選択します。画面の指示に沿って、Cloud 請求先アカウントをプロジェクトにリンクします。
 このアップグレードの一環として Cloud 請求先アカウントを作成する必要があった場合は、Firebase コンソールのアップグレード フローに戻ってアップグレードを完了する必要がある場合があります。
Firebase プロジェクトにウェブアプリを追加する
- Firebase プロジェクトの [プロジェクトの概要] に移動し、 [ウェブ] をクリックします。 [ウェブ] をクリックします。
 プロジェクトにアプリがすでに登録されている場合は、[アプリを追加] をクリックすると、ウェブアイコンが表示されます。
- [アプリのニックネーム] テキスト ボックスに、覚えやすいアプリのニックネーム(My Next.js appなど)を入力します。
- [このアプリの Firebase Hosting も設定します] のチェックボックスをオフのままにします。
- [アプリの登録] > [コンソールに進む] をクリックします。
Firebase コンソールで Firebase サービスを設定する
Authentication を設定する
- Firebase コンソールで [認証] に移動します。
- [Get started] をクリックします。
- [追加のプロバイダ] 列で、[Google > 有効にする] をクリックします。
- [プロジェクトの公開名] テキスト ボックスに、My Next.js appなどの覚えやすい名前を入力します。
- [プロジェクトのサポートメール] プルダウンからメールアドレスを選択します。
- [保存] をクリックします。
Cloud Firestore を設定する
- Firebase コンソールの左側のパネルで [ビルド] を展開し、[Firestore データベース] を選択します。
- [データベースを作成] をクリックします。
- [データベース ID] は (default)に設定したままにします。
- データベースの場所を選択し、[次へ] をクリックします。
 実際のアプリでは、ユーザーに近い場所を選択します。
- [テストモードで開始] をクリックします。セキュリティ ルールに関する免責条項を確認します。
 この Codelab の後半で、データを保護するためのセキュリティ ルールを追加します。データベースのセキュリティ ルールを追加せずに、アプリを配布または公開しないでください。
- [作成] をクリックします。
Cloud Storage for Firebase を設定する
- Firebase コンソールの左側のパネルで [ビルド] を展開し、[Storage] を選択します。
- [開始] をクリックします。
- デフォルトの Storage バケットのロケーションを選択します。US-WEST1、US-CENTRAL1、US-EAST1のバケットは、Google Cloud Storage の「無料枠」を利用できます。他のすべてのロケーションのバケットは、Google Cloud Storage の料金と使用量に従います。
- [テストモードで開始] をクリックします。セキュリティ ルールに関する免責条項を確認します。
 この Codelab の後半で、データを保護するためのセキュリティ ルールを追加します。Storage バケットのセキュリティ ルールを追加せずに、アプリを配布または公開しないでください。
- [作成] をクリックします。
セキュリティ ルールをデプロイする
このコードには、Firestore と Cloud Storage for Firebase のセキュリティ ルールのセットがすでに含まれています。セキュリティ ルールをデプロイすると、データベースとバケット内のデータが不正使用から保護されます。
- ターミナルで、前に作成した Firebase プロジェクトを使用するように CLI を構成します。firebase use --add friendlyeats-codelab」と入力します。
- これらのセキュリティ ルール(および後で必要になるインデックス)をデプロイするには、ターミナルで次のコマンドを実行します。firebase deploy --only firestore,storage 
- 「"Cloud Storage for Firebase needs an IAM Role to use cross-service rules. Grant the new role?"」というメッセージが表示されたら、Enterを押して [はい] を選択します。
4. スターター コードベースを確認する
このセクションでは、アプリのスターター コードベースのいくつかの領域を確認します。この Codelab では、これらの領域に機能を追加します。
フォルダとファイルの構造
次の表に、アプリのフォルダとファイル構造の概要を示します。
| フォルダとファイル | 説明 | 
| 
 | フィルタ、ヘッダー、レストランの詳細、レビューの React コンポーネント | 
| 
 | 必ずしも React や Next.js にバインドされないユーティリティ関数 | 
| 
 | Firebase 固有のコードと Firebase 構成 | 
| 
 | ウェブアプリの静的アセット(アイコンなど) | 
| 
 | Next.js App Router を使用したルーティング | 
| 
 | npm を使用したプロジェクトの依存関係 | 
| 
 | Next.js 固有の構成(サーバー アクションが有効) | 
| 
 | JavaScript 言語サービスの構成 | 
サーバー コンポーネントとクライアント コンポーネント
このアプリは、App ルーターを使用する Next.js ウェブアプリです。サーバー レンダリングはアプリ全体で使用されます。たとえば、src/app/page.js ファイルはメインページを担当するサーバー コンポーネントです。src/components/RestaurantListings.jsx ファイルは、ファイルの先頭にある "use client" ディレクティブで示されるクライアント コンポーネントです。
インポート ステートメント
次のような import ステートメントが表示されることがあります。
import RatingPicker from "@/src/components/RatingPicker.jsx";
アプリは @ 記号を使用して、相対インポート パスの煩雑さを回避しています。これは、パス エイリアスによって実現されています。
Firebase 固有の API
すべての Firebase API コードは src/lib/firebase ディレクトリにラップされています。個々の React コンポーネントは、Firebase 関数を直接インポートするのではなく、ラップされた関数を src/lib/firebase ディレクトリからインポートします。
モックデータ
レストランとレビューのモックデータは src/lib/randomData.js ファイルに含まれています。そのファイルからのデータは、src/lib/fakeRestaurants.js ファイルのコードで組み立てられます。
5. App Hosting バックエンドを作成する
このセクションでは、git リポジトリのブランチを監視するように App Hosting バックエンドを設定します。
このセクションの最後では、GitHub のリポジトリに接続された App Hosting バックエンドが作成されます。このバックエンドは、新しい commit を main ブランチに push するたびに、アプリの新しいバージョンを自動的に再ビルドしてロールアウトします。
バックエンドの作成
- Firebase コンソールの [App Hosting] ページに移動します。
![App Hosting コンソールのゼロ状態。[スタートガイド] ボタンが表示されている](https://firebase.google.com/static/codelabs/firebase-nextjs/img/app_hosting_console_zero_state.webp?authuser=5&hl=ja)
- [始める] をクリックして、バックエンド作成フローを開始します。バックエンドを次のように構成します。
- 地域を選択します。実際のアプリでは、ユーザーに最も近いリージョンを選択します。
- [GitHub リポジトリをインポートする] ステップのプロンプトに従って、先ほど作成した GitHub リポジトリを接続します。
- デプロイ設定を行います。- ルート ディレクトリを /のままにする
- ライブブランチを mainに設定する
- 自動ロールアウトを有効にする
 
- ルート ディレクトリを 
- バックエンドに friendlyeats-codelabという名前を付けます。
- [Firebase ウェブアプリを関連付ける] で、[新しい Firebase ウェブアプリを作成する] をクリックします。
- [完了してデプロイ] をクリックします。しばらくすると、新しいページに移動し、新しい App Hosting バックエンドのステータスを確認できます。
- ロールアウトが完了したら、[ドメイン] で無料ドメインをクリックします。DNS 伝播のため、動作を開始するまでに数分かかることがあります。
- タイムアウトページを読み込むと、「アプリケーション エラー: サーバーサイドの例外が発生しました(詳細についてはサーバーログをご覧ください)」というエラー メッセージが表示されます。
- Firebase コンソールで、App Hosting バックエンドの [ログ] タブを確認します。「エラー: 実装されていません」というログが表示されます。これは、次のステップで認証を追加するときに修正します。
これで最初のウェブアプリがデプロイされました。GitHub リポジトリの main ブランチに新しい commit を push するたびに、Firebase コンソールで新しいビルドとロールアウトが開始されます。ロールアウトが完了すると、サイトは自動的に更新されます。
6. ウェブアプリに認証を追加する
このセクションでは、ウェブアプリに認証を追加して、ログインできるようにします。
承認済みドメインを追加します
Firebase Authentication は、許可したドメインからのログイン リクエストのみを受け付けます。ここでは、App Hosting バックエンドのドメインをプロジェクトの承認済みドメインのリストに追加します。
- App Hosting の [概要] ページから App Hosting バックエンドのドメインをコピーします。
- [Auth Settings] タブに移動し、[Authorized Domains] を選択します。
- [ドメインを追加] ボタンをクリックします。
- App Hosting バックエンドのドメインを入力します。
- [追加] をクリックします。
ログイン関数とログアウト関数を実装する
- src/lib/firebase/auth.jsファイルで、- onAuthStateChanged、- onIdTokenChanged、- signInWithGoogle、- signOut関数を次のコードに置き換えます。
export function onAuthStateChanged(cb) {
  return _onAuthStateChanged(auth, cb);
}
export function onIdTokenChanged(cb) {
  return _onIdTokenChanged(auth, cb);
}
export async function signInWithGoogle() {
  const provider = new GoogleAuthProvider();
  try {
    await signInWithPopup(auth, provider);
  } catch (error) {
    console.error("Error signing in with Google", error);
  }
}
export async function signOut() {
  try {
    return auth.signOut();
  } catch (error) {
    console.error("Error signing out with Google", error);
  }
}
このコードでは、次の Firebase API を使用します。
| Firebase API | 説明 | 
| ユーザーのログイン状態の変更のオブザーバーを追加します。 | |
| ユーザーの ID トークンの変更のオブザーバーを追加します。 | |
| Google 認証プロバイダ インスタンスを作成します。 | |
| ダイアログ ベースの認証フローを開始します。 | |
| ユーザーをログアウトします。 | 
src/components/Header.jsx ファイルでは、コードがすでに signInWithGoogle 関数と signOut 関数を呼び出しています。
認証状態をサーバーに送信する
認証状態をサーバーに渡すために、Cookie を使用します。クライアントで認証状態が変更されるたびに、__session Cookie が更新されます。
src/components/Header.jsx で、useUserSession 関数を次のコードに置き換えます。
function useUserSession(initialUser) {
  useEffect(() => {
    return onIdTokenChanged(async (user) => {
      if (user) {
        const idToken = await user.getIdToken();
        await setCookie("__session", idToken);
      } else {
        await deleteCookie("__session");
      }
      if (initialUser?.uid === user?.uid) {
        return;
      }
      window.location.reload();
    });
  }, [initialUser]);
  return initialUser;
}
サーバーで認証状態を読み取る
FirebaseServerApp を使用して、クライアントの認証状態をサーバーにミラーリングします。
src/lib/firebase/serverApp.js を開き、getAuthenticatedAppForUser 関数を置き換えます。
export async function getAuthenticatedAppForUser() {
  const authIdToken = (await cookies()).get("__session")?.value;
  // Firebase Server App is a new feature in the JS SDK that allows you to
  // instantiate the SDK with credentials retrieved from the client & has
  // other affordances for use in server environments.
  const firebaseServerApp = initializeServerApp(
    // https://github.com/firebase/firebase-js-sdk/issues/8863#issuecomment-2751401913
    initializeApp(),
    {
      authIdToken,
    }
  );
  const auth = getAuth(firebaseServerApp);
  await auth.authStateReady();
  return { firebaseServerApp, currentUser: auth.currentUser };
}
変更を確認する
src/app/layout.js ファイルのルート レイアウトは、ヘッダーをレンダリングし、ユーザーが利用可能な場合は、ユーザーをプロパティとして渡します。
<Header initialUser={currentUser?.toJSON()} />
つまり、<Header> コンポーネントは、サーバーの実行時にユーザーデータが利用可能な場合は、そのデータをレンダリングします。ページのライフサイクル中に、最初のページ読み込み後に認証の更新があった場合は、onAuthStateChanged ハンドラが処理します。
新しいビルドをロールアウトして、作成したものを確認します。
- commit メッセージ「Add authentication」で commit を作成し、GitHub リポジトリに push します。
- Firebase コンソールの [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
- 新しい認証動作を確認します。- ブラウザでウェブアプリを更新します。ヘッダーに表示名が表示されます。
- ログアウトしてもう一度ログインしてください。 この手順は、別のユーザーでも繰り返すことができます。
- 省略可: ウェブアプリを右クリックして [ページのソースを表示] を選択し、表示名を検索します。サーバーから返された生の HTML ソースに表示されます。
 
7. レストランの情報を表示する
このウェブアプリには、レストランとレビューのモックデータが含まれています。
レストランを 1 つ以上追加する
ローカルの Cloud Firestore データベースにレストランのモックデータを挿入する手順は次のとおりです。
- まだログインしていない場合は、ウェブアプリにログインします。次に、 > [サンプル レストランを追加] を選択します。 > [サンプル レストランを追加] を選択します。
- Firebase コンソールの [Firestore データベース] ページで、[restaurants] を選択します。レストラン コレクションに、最上位のドキュメントが表示されます。各ドキュメントはレストランを表します。
- いくつかのドキュメントをクリックして、レストランのドキュメントのプロパティを確認します。
レストランのリストを表示する
これで、Cloud Firestore データベースに Next.js ウェブアプリで表示できるレストランが追加されました。
データ取得コードを定義する手順は次のとおりです。
- src/app/page.jsファイルで、- <Home />サーバー コンポーネントを見つけ、サーバーの実行時にレストランのリストを取得する- getRestaurants関数の呼び出しを確認します。次の手順で- getRestaurants関数を実装します。
- src/lib/firebase/firestore.jsファイルで、- applyQueryFilters関数と- getRestaurants関数を次のコードに置き換えます。
function applyQueryFilters(q, { category, city, price, sort }) {
  if (category) {
    q = query(q, where("category", "==", category));
  }
  if (city) {
    q = query(q, where("city", "==", city));
  }
  if (price) {
    q = query(q, where("price", "==", price.length));
  }
  if (sort === "Rating" || !sort) {
    q = query(q, orderBy("avgRating", "desc"));
  } else if (sort === "Review") {
    q = query(q, orderBy("numRatings", "desc"));
  }
  return q;
}
export async function getRestaurants(db = db, filters = {}) {
  let q = query(collection(db, "restaurants"));
  q = applyQueryFilters(q, filters);
  const results = await getDocs(q);
  return results.docs.map((doc) => {
    return {
      id: doc.id,
      ...doc.data(),
      // Only plain objects can be passed to Client Components from Server Components
      timestamp: doc.data().timestamp.toDate(),
    };
  });
}
- コミット メッセージ「Read the list of restaurants from Firestore」でコミットを作成し、GitHub リポジトリに push します。
- Firebase コンソールの [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
- ウェブアプリでページを更新します。レストランの画像がページにタイルとして表示されます。
レストランのリスティングがサーバーの実行時に読み込まれることを確認する
Next.js フレームワークを使用している場合、データがサーバーの実行時とクライアントサイドの実行時のどちらで読み込まれるかは明確でないことがあります。
レストランのリスティングがサーバーの実行時に読み込まれることを確認する手順は次のとおりです。
- ウェブアプリで DevTools を開き、JavaScript を無効にします。

- ウェブアプリを更新します。レストランのリスティングはまだ読み込まれています。レストランの情報がサーバー レスポンスで返されます。JavaScript が有効になっている場合、レストラン情報はクライアントサイドの JavaScript コードを通じてハイドレートされます。
- DevTools で、JavaScript を再度有効にします。
Cloud Firestore スナップショット リスナーでレストランの更新をリッスンする
前のセクションでは、src/app/page.js ファイルから読み込まれたレストランの初期セットを確認しました。src/app/page.js ファイルはサーバー コンポーネントであり、Firebase データ取得コードを含むサーバーでレンダリングされます。
src/components/RestaurantListings.jsx ファイルはクライアント コンポーネントであり、サーバー レンダリングされたマークアップをハイドレートするように構成できます。
サーバーサイド レンダリングされたマークアップをハイドレートするように src/components/RestaurantListings.jsx ファイルを構成する手順は次のとおりです。
- src/components/RestaurantListings.jsxファイルで、次のコードを確認します。このコードはすでに記述されています。
useEffect(() => {
    return getRestaurantsSnapshot((data) => {
      setRestaurants(data);
    }, filters);
  }, [filters]);
このコードは、前のステップで実装した getRestaurants() 関数と同様の getRestaurantsSnapshot() 関数を呼び出します。ただし、このスナップショット関数は、レストランのコレクションが変更されるたびにコールバックが呼び出されるコールバック メカニズムを提供します。
- src/lib/firebase/firestore.jsファイルで、- getRestaurantsSnapshot()関数を次のコードに置き換えます。
export function getRestaurantsSnapshot(cb, filters = {}) {
  if (typeof cb !== "function") {
    console.log("Error: The callback parameter is not a function");
    return;
  }
  let q = query(collection(db, "restaurants"));
  q = applyQueryFilters(q, filters);
  return onSnapshot(q, (querySnapshot) => {
    const results = querySnapshot.docs.map((doc) => {
      return {
        id: doc.id,
        ...doc.data(),
        // Only plain objects can be passed to Client Components from Server Components
        timestamp: doc.data().timestamp.toDate(),
      };
    });
    cb(results);
  });
}
Firestore Database ページで行った変更が、ウェブアプリにリアルタイムで反映されるようになりました。
- コミット メッセージ「Listen for realtime restaurant updates」でコミットを作成し、GitHub リポジトリに push します。
- Firebase コンソールの [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
- ウェブアプリで、 > [Add sample restaurants] を選択します。スナップショット関数が正しく実装されている場合、ページを更新しなくてもレストランがリアルタイムで表示されます。 > [Add sample restaurants] を選択します。スナップショット関数が正しく実装されている場合、ページを更新しなくてもレストランがリアルタイムで表示されます。
8. ウェブアプリからユーザーが送信したレビューを保存する
- src/lib/firebase/firestore.jsファイルで、- updateWithRating()関数を次のコードに置き換えます。
const updateWithRating = async (
  transaction,
  docRef,
  newRatingDocument,
  review
) => {
  const restaurant = await transaction.get(docRef);
  const data = restaurant.data();
  const newNumRatings = data?.numRatings ? data.numRatings + 1 : 1;
  const newSumRating = (data?.sumRating || 0) + Number(review.rating);
  const newAverage = newSumRating / newNumRatings;
  transaction.update(docRef, {
    numRatings: newNumRatings,
    sumRating: newSumRating,
    avgRating: newAverage,
  });
  transaction.set(newRatingDocument, {
    ...review,
    timestamp: Timestamp.fromDate(new Date()),
  });
};
このコードは、新しいレビューを表す新しい Firestore ドキュメントを挿入します。このコードは、レストランを表す既存の Firestore ドキュメントも更新し、評価数と計算された平均評価の更新後の数値を反映します。
- 関数全体を次の addReviewToRestaurant()に置き換えます。
export async function addReviewToRestaurant(db, restaurantId, review) {
	if (!restaurantId) {
		throw new Error("No restaurant ID has been provided.");
	}
	if (!review) {
		throw new Error("A valid review has not been provided.");
	}
	try {
		const docRef = doc(collection(db, "restaurants"), restaurantId);
		const newRatingDocument = doc(
			collection(db, `restaurants/${restaurantId}/ratings`)
		);
		// corrected line
		await runTransaction(db, transaction =>
			updateWithRating(transaction, docRef, newRatingDocument, review)
		);
	} catch (error) {
		console.error(
			"There was an error adding the rating to the restaurant",
			error
		);
		throw error;
	}
}
Next.js サーバー アクションを実装する
Next.js サーバー アクションは、フォームデータにアクセスするための便利な API を提供します。たとえば、data.get("text") を使用すると、フォーム送信ペイロードからテキスト値を取得できます。
Next.js サーバー アクションを使用してレビュー フォームの送信を処理する手順は次のとおりです。
- src/components/ReviewDialog.jsxファイルで、- <form>要素の- action属性を探します。
<form action={handleReviewFormSubmission}>
action 属性値は、次のステップで実装する関数を参照します。
- src/app/actions.jsファイルで、- handleReviewFormSubmission()関数を次のコードに置き換えます。
// This is a next.js server action, which is an alpha feature, so
// use with caution.
// https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions
export async function handleReviewFormSubmission(data) {
        const { app } = await getAuthenticatedAppForUser();
        const db = getFirestore(app);
        await addReviewToRestaurant(db, data.get("restaurantId"), {
                text: data.get("text"),
                rating: data.get("rating"),
                // This came from a hidden form field.
                userId: data.get("userId"),
        });
}
レストランのクチコミを追加する
レビューの送信のサポートを実装したので、レビューが Cloud Firestore に正しく挿入されることを確認できます。
レビューを追加して Cloud Firestore に挿入されたことを確認する手順は次のとおりです。
- コミット メッセージ「Allow users to submit restaurant reviews」でコミットを作成し、GitHub リポジトリに push します。
- Firebase コンソールの [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
- ウェブアプリを更新し、ホームページからレストランを選択します。
- レストランのページで、 をクリックします。 をクリックします。
- 評価を選択してください。
- クチコミを書く。
- [送信] をクリックします。レビューはレビュー リストの一番上に表示されます。
- Cloud Firestore で、[ドキュメントを追加] ペインでレビューしたレストランのドキュメントを検索して選択します。
- [コレクションを開始] ペインで、[ratings] を選択します。
- [ドキュメントを追加] ペインで、レビュー対象のドキュメントを見つけて、想定どおりに挿入されていることを確認します。

9. ウェブアプリからユーザーがアップロードしたファイルを保存する
このセクションでは、ログイン時にレストランに関連付けられた画像を置き換えることができるように機能を追加します。画像を Firebase Storage にアップロードし、レストランを表す Cloud Firestore ドキュメント内の画像 URL を更新します。
ウェブアプリからユーザーがアップロードしたファイルを保存する手順は次のとおりです。
- src/components/Restaurant.jsxファイルで、ユーザーがファイルをアップロードしたときに実行されるコードを確認します。
async function handleRestaurantImage(target) {
  const image = target.files ? target.files[0] : null;
  if (!image) {
    return;
  }
  const imageURL = await updateRestaurantImage(id, image);
  setRestaurantDetails({ ...restaurantDetails, photo: imageURL });
}
この関数に変更は必要ありませんが、次の手順で updateRestaurantImage() 関数の動作を実装します。
- src/lib/firebase/storage.jsファイルで、- updateRestaurantImage()関数と- uploadImage()関数を次のコードに置き換えます。
export async function updateRestaurantImage(restaurantId, image) {
  try {
    if (!restaurantId) {
      throw new Error("No restaurant ID has been provided.");
    }
    if (!image || !image.name) {
      throw new Error("A valid image has not been provided.");
    }
    const publicImageUrl = await uploadImage(restaurantId, image);
    await updateRestaurantImageReference(restaurantId, publicImageUrl);
    return publicImageUrl;
  } catch (error) {
    console.error("Error processing request:", error);
  }
}
async function uploadImage(restaurantId, image) {
  const filePath = `images/${restaurantId}/${image.name}`;
  const newImageRef = ref(storage, filePath);
  await uploadBytesResumable(newImageRef, image);
  return await getDownloadURL(newImageRef);
}
updateRestaurantImageReference() 関数はすでに実装されています。この関数は、更新された画像 URL を使用して Cloud Firestore の既存のレストラン ドキュメントを更新します。
画像アップロード機能を確認する
画像が想定どおりにアップロードされることを確認する手順は次のとおりです。
- コミット メッセージ「Allow users to change each restaurants' photo」でコミットを作成し、GitHub リポジトリに push します。
- Firebase コンソールの [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
- ウェブアプリで、ログインしていることを確認し、レストランを選択します。
 をクリックして、ファイル システムから画像をアップロードします。画像がローカル環境から Cloud Storage にアップロードされます。画像はアップロード後すぐに表示されます。 をクリックして、ファイル システムから画像をアップロードします。画像がローカル環境から Cloud Storage にアップロードされます。画像はアップロード後すぐに表示されます。
- Firebase の Cloud Storage に移動します。
- レストランを表すフォルダに移動します。アップロードした画像がフォルダに存在します。

10. 生成 AI を使用してレストランのレビューを要約する
このセクションでは、ユーザーがすべてのレビューを読まなくても、レストランに対する他のユーザーの評価を簡単に把握できるように、レビューの要約機能を追加します。
Gemini API キーを Cloud Secret Manager に保存する
- Gemini API を使用するには、API キーが必要です。Google AI Studio にアクセスし、[API キーを作成] をクリックします。
- [Google Cloud プロジェクトを検索] 入力で、Firebase プロジェクトを選択します。すべての Firebase プロジェクトは Google Cloud プロジェクトによって支えられています。
- App Hosting は Cloud Secret Manager と統合されており、API キーなどの機密性の高い値を安全に保存できます。- ターミナルでコマンドを実行して、新しいシークレットを作成します。
 firebase apphosting:secrets:set GEMINI_API_KEY- シークレット値の入力を求められたら、Google AI Studio から Gemini API キーをコピーして貼り付けます。
- 新しいシークレットが本番環境用かローカル テスト用かを尋ねられたら、[本番環境] を選択します。
- バックエンドのサービス アカウントがシークレットにアクセスできるようにアクセス権を付与するかどうかを尋ねられたら、[はい] を選択します。
- 新しいシークレットを apphosting.yamlに追加するかどうかを尋ねられたら、Yと入力して承諾します。
 
Gemini API キーが Cloud Secret Manager に安全に保存され、App Hosting バックエンドからアクセスできるようになりました。
レビューの概要コンポーネントを実装する
- src/components/Reviews/ReviewSummary.jsxで、- GeminiSummary関数を次のコードに置き換えます。- export async function GeminiSummary({ restaurantId }) { const { firebaseServerApp } = await getAuthenticatedAppForUser(); const reviews = await getReviewsByRestaurantId( getFirestore(firebaseServerApp), restaurantId ); const reviewSeparator = "@"; const prompt = ` Based on the following restaurant reviews, where each review is separated by a '${reviewSeparator}' character, create a one-sentence summary of what people think of the restaurant. Here are the reviews: ${reviews.map((review) => review.text).join(reviewSeparator)} `; try { if (!process.env.GEMINI_API_KEY) { // Make sure GEMINI_API_KEY environment variable is set: // https://firebase.google.com/docs/genkit/get-started throw new Error( 'GEMINI_API_KEY not set. Set it with "firebase apphosting:secrets:set GEMINI_API_KEY"' ); } // Configure a Genkit instance. const ai = genkit({ plugins: [googleAI()], model: gemini20Flash, // set default model }); const { text } = await ai.generate(prompt); return ( <div className="restaurant__review_summary"> <p>{text}</p> <p>✨ Summarized with Gemini</p> </div> ); } catch (e) { console.error(e); return <p>Error summarizing reviews.</p>; } }
- 「Use AI to summarize reviews」という commit メッセージで commit を作成し、GitHub リポジトリに push します。
- Firebase コンソールの [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
- レストランのページを開きます。ページの上部に、ページ上のすべてのレビューの概要が 1 文で表示されます。
- 新しいクチコミを追加して、ページを更新します。概要が変更されます。
11. まとめ
これで完了です。Firebase を使用して Next.js アプリに機能を追加する方法を学習しました。具体的には、次のものを使用しました。
- Firebase App Hosting: 構成されたブランチに push するたびに Next.js コードを自動的にビルドしてデプロイします。
- Firebase Authentication を使用して、ログインとログアウトの機能を有効にします。
- レストラン データとレストランのレビュー データ用の Cloud Firestore。
- レストランの画像には Cloud Storage for Firebase を使用します。