📚

初心者がNext.jsとSupabaseで日記アプリを公開するまで

に公開

はじめに

この日記アプリは、Web開発を始めたばかりの自分が「本当に動くものを作りたい」と思って始めたものです。

使った技術は Next.jsSupabase。React経験者であれば馴染みやすいですが、Supabaseの認証やStorageなどは初めて触る部分も多く、試行錯誤しながら進めました。

この記事は、自分があとから復習できるように、そして これから始める人が少しでも楽になるように 書いています。


作ったもの

簡単な日記アプリを作りました。

主な機能

  • メールリンクによるログイン認証
  • 投稿(テキスト)

画面イメージ(スクリーンショット予定)

  • ログイン画面
  • 投稿フォーム
  • 投稿一覧

使用技術は Next.js(App Router)Supabaseです。


プロジェクトの作成

まずはnode.jsをダウンロードしましょう!!

https://nodejs.org

まず、TypeScript付きで Next.js プロジェクトを作成します。


npx create-next-app@latest my-diary-app --typescript

my-diary-app はプロジェクト名(作業用フォルダ名)です。

  • -typescript をつけることで、TypeScript対応になります。

実行後に以下のような表示が出るので、「y」で進めます。


Need to install the following packages:
create-next-app@15.3.1
Ok to proceed? (y)

セットアップ時の質問と選択肢(詳細解説付き)

Next.js プロジェクト作成時には、いくつかの選択肢を求められます。ここでは、実際に選択した内容とその理由、背景を詳しく解説します。


ESLint を使いますか? → Yes

ESLint は、コードに潜むミスやスタイルの乱れを検出してくれる 静的解析ツールです。

初心者のうちは「自分では気づけないミス」をしがちですが、ESLint を導入しておくことで、コードの品質を自動的にチェックしてくれます。特に、チーム開発や長期的な運用を想定した場合、コードスタイルを統一できる点も大きなメリットです。

そのため、今回のプロジェクトでは Yes を選択しました。


Tailwind CSS を使いますか? → No

Tailwind CSS は、HTML に直接クラスを記述することで見た目を整える ユーティリティファーストの CSS フレームワークです。

非常に人気がありますが、2025年時点では v4 がリリースされたばかりで、公式ドキュメントや記事が少なく、使用方法に迷う場面が多く見られました。

そのため今回は、プロジェクト初期では導入せず、あとから安定版の v3 を自分で追加する方針にしました。

Tailwind CSS v3 の導入は以下のように簡単にできます。

npm install -D tailwindcss@^3.4 postcss autoprefixer
npx tailwindcss init -p

これにより、自分のペースで Tailwind を学びながら導入できるようにしています。


src/ ディレクトリを使いますか? → Yes

src/ ディレクトリを使うことで、アプリケーションのコードを明確に整理することができます。

通常、Next.js では pages/components/ をプロジェクトのルートに置くこともできますが、src/ 配下にまとめておくことで、アプリケーションロジックと設定ファイル(next.config.js など)を明確に分離できます。

実際のディレクトリ構成例は以下のようになります。

my-app/
├─ public/
├─ src/
│  ├─ app/
│  ├─ components/
│  └─ styles/

このように、構造的にスッキリして見通しがよくなるため、今回は Yes を選択しました。


App Router を使いますか? → Yes

App Router は、Next.js に新しく追加されたルーティング方式です。

従来の pages/ ベースではなく、app/ ディレクトリを基盤として、以下のような新しい構造が採用されています。

特徴

  • レイアウトごとの分割が簡単にできる
  • サーバーコンポーネントに対応しており、パフォーマンスが向上
  • ディレクトリ構造=URL構造になるため、URLの把握が直感的

将来的にもこの App Router が主流になると考えられており、早めに慣れておく意味でも Yes を選びました。


next dev で Turbopack を使いますか? → No

Turbopack は Next.js が開発中の新しいビルドツールで、Webpack の後継と位置付けられています。

確かにビルド速度は非常に高速ですが、一部の機能がまだ安定しておらず、エラーが出る可能性があります。特に、初学者が扱うには情報が少なく、問題が発生した際の調査・対処が難しいです。

そのため今回は、安全で実績のある Webpackベースの開発環境を選択するために No にしました。


import alias(@/*)をカスタマイズしますか? → No

インポートエイリアスとは、ファイルのインポートパスを短縮できる機能です。

例えば、次のような通常の書き方:

import Header from "../../../components/Header"

これを、@/ を使えば次のように書けます:

import Header from "@/components/Header"

Next.js のプロジェクトでは、デフォルトで @/src/ を指すように設定されています。

今回のプロジェクトでは、このデフォルト設定で十分にシンプルかつ便利だったため、カスタマイズは行いませんでした。

あとから変更したくなった場合は、tsconfig.json および next.config.js で簡単に設定を変更できます。


選択内容まとめ表

以下に、すべての選択肢とその理由を簡潔にまとめます。

質問内容 選択 理由
ESLint を使いますか? Yes 書き方のミスやコードの品質を自動でチェックできる
Tailwind CSS を使いますか? No v4 は情報が少ないため、安定版 v3 を後から導入する方針
src/ ディレクトリを使いますか? Yes ファイル構成が整理され、スケール時にも見通しがよい
App Router を使いますか? Yes 今後の主流。柔軟な構成と高速化が期待できる
next dev で Turbopack を使う? No 現時点では不安定なため、Webpack を選択
import alias をカスタマイズする? No デフォルトの @/ で十分使いやすく、後から変更も可能

Supabaseの構築手順

Supabaseは**BaaS(Backend as a Service)**として、バックエンド構築を手軽にしてくれるサービスです。特に、ログイン認証やデータベース、API作成などをGUIから簡単に操作できるため、フロントエンド中心の開発者でもすぐに導入できます。

今回は、ユーザー認証つきの日記投稿アプリを構築するために、Supabaseで以下の設定を行います。


Step 1|Supabase プロジェクト作成

  1. Supabase公式サイト にアクセスします。
  2. GitHub アカウントでログインします。
  3. ダッシュボードから「New Project」をクリックします。
  4. 以下の情報を入力してプロジェクトを作成します:
項目 設定内容
Project name diary-app(任意)
Database password 任意の安全なパスワード(後で使用するのでメモしておく)
Region 日本からアクセスする場合は Tokyo を選択
  1. 「Create project」をクリックすると、数分で環境がセットアップされます。

Step 2|posts テーブルの作成

  1. プロジェクトが作成されたら、左のサイドバーから「Table Editor」に移動します。
  2. 「New Table」をクリックし、新しいテーブルを作成します。
  3. テーブル名を posts に設定します。
  4. 以下のようにカラムを追加していきます:
カラム名 型 (Type) デフォルト値 補足設定
id uuid gen_random_uuid() ✔ Primary Key
user_id uuid auth.uid() Is Nullable: オン / Is Unique: オフ
content text なし Is Nullable: 任意(必須にするならオフ) / Is Unique: オフ
created_at timestamptz now() Is Nullable: オン(ただし now() があるため問題なし)

  1. 入力が完了したら「Save」をクリックしてテーブルを保存します。

Step 3|外部キーの設定(user_idauth.users.id

次に、posts.user_idauth.users.id を関連付ける外部キー(Foreign Key)制約を設定します。

  1. 「Table Editor」から posts テーブルを選択します。
  2. user_id の行の右端にある「∨」をクリックして、「Edit Column」を選択します。
  3. 表示されたウィンドウで「Add Foreign Key」を選択します。
  4. 次のように設定します:
設定項目 内容
スキーマ(Schema) auth
テーブル名 users
対象カラム id

  1. 設定後、「Save」をクリックして外部キーを保存します。

外部キーの意味とスキーマ構成の理解

Supabaseでは初期状態で以下のような**スキーマ(データベースの区分)**が存在しています:

  • auth:Supabaseが自動で管理するスキーマ。ユーザー情報やセッションなどが格納されます。
  • public:自分で作成するデータ(例:投稿やプロフィール)を格納するスキーマ。

今回作成した posts テーブルは public スキーマに属しており、そこに user_id というカラムを追加しました。

このカラムに「投稿を行ったユーザーのID」を記録することで、誰がどの投稿をしたかを紐づけられるようになります。

ただし、値が間違っていた場合に整合性が取れなくなるため、「user_id の値は必ず auth.users.id に存在するものに限る」というルールをSupabaseに守らせるために、外部キー制約を設定しました。

これにより、無効なユーザーIDの保存が防止され、データ整合性が保たれます。


Step 4|認証(メールアドレス)を有効化する

認証機能を使うには、Supabaseが提供する認証モジュールの設定が必要です。

  1. 左サイドバーから「Authentication」を選択します。
  2. 上部タブから「Sign In / Sign Up」をクリックします。
  3. 「Auth Providers」セクションで「Email」が有効(Enabled)になっていることを確認します。

この設定が有効であれば、Supabaseが自動で以下の情報を auth.users テーブルに保存します:

  • メールアドレス
  • 一意なユーザーID(UUID)
  • 登録日時

この状態になれば、フロントエンドからは次のようなコードでログイン・サインアップ機能を実装できます。

supabase.auth.signUp({ email, password });
supabase.auth.signInWithPassword({ email, password });

このように、バックエンドを自前で構築せずとも、ログイン機能まで一気に実装できるのがSupabaseの強みです。

Step 5|supabaseClient.ts を作成する

まず、src/lib ディレクトリを作成し、その中に supabaseClient.ts というファイルを作ります。

// src/lib/supabaseClient.ts
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

このファイルは、Supabaseに接続するためのクライアントを初期化し、他のコンポーネントから再利用できるようにするためのものです。

パッケージのインストール

Supabaseの公式ライブラリ @supabase/supabase-js が未インストールの場合は、以下のコマンドでインストールします。

npm install @supabase/supabase-js

Step 6|.env.local に環境変数を設定

プロジェクトのルートに .env.local ファイルを作成し、以下のように記述します。

NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=xxxxxxxxxxxxxxxxxxxx

値の取得場所

  • NEXT_PUBLIC_SUPABASE_URL は Supabase プロジェクトの「Project Settings」>「URL」からコピー
  • NEXT_PUBLIC_SUPABASE_ANON_KEY は同じく「Project Settings」>「API」から、anon public key をコピー

このファイルは .gitignore に含まれているため、GitHubにアップロードされることはありません。


解説(環境変数・ライブラリの意味)

  • @supabase/supabase-js

    Supabaseが提供する公式クライアントライブラリ。認証・データベース操作・Storage などすべてこれで扱えます。

  • 環境変数(.env.local

    APIキーやURLなどの機密情報をソースコードと分離して管理するためのファイル。

  • TypeScriptの ! 演算子

    process.env.XYZ! のように書くと、「この値は絶対に undefined じゃない」と明示できます。

  • export構文

    export const supabase とすることで、他のファイルから import して使えるようになります。


Next.jsで画面を作る

ここからは、Supabaseと連携した**フロントエンドの画面(UI)**を Next.js で作成していきます。


Step 1|ログイン画面の作成

まずは、メールリンクによるログイン画面を作成します。

ファイルの作成場所

次の場所に新しいファイルを作成します:

src/app/login/page.tsx

コード全体

// src/app/login/page.tsx
"use client";
import { useState } from "react";
import { supabase } from "@/lib/supabaseClient";

export default function LoginPage() {
  const [email, setEmail] = useState("");
  const [loading, setLoading] = useState(false);

  const handleLogin = async () => {
    if (!email) return;
    setLoading(true);
    const { error } = await supabase.auth.signInWithOtp({
      email,
      options: { emailRedirectTo: `${location.origin}/login` },
    });
    setLoading(false);
    if (error) alert(error.message);
    else alert("メールを確認してください!");
  };

  return (
    <div>
      <h1>ログイン or 新規登録</h1>
      <inputtype="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="you@example.com"
      />
      <button onClick={handleLogin}>
        {loading ? "送信中..." : "メールでログインリンク送信"}
      </button>
    </div>
  );
}


機能の解説

状態管理(useState

const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
  • email:現在入力されているメールアドレスを保持する変数
  • setEmail:ユーザーの入力内容に応じて email を更新
  • loading:現在ログイン処理中かどうかを管理するフラグ

→ 状態を管理することで、画面上の要素(入力欄やボタンの表示)を自動的に切り替えられます。


ログイン処理関数(handleLogin

const handleLogin = async () => {
  if (!email) return;
  setLoading(true);
  const { error } = await supabase.auth.signInWithOtp({
    email,
    options: { emailRedirectTo: `${location.origin}/login` },
  });
  setLoading(false);
  if (error) alert(error.message);
  else alert("メールを確認してください!");
};

  • async:非同期処理(メール送信)を扱うために必要
  • supabase.auth.signInWithOtp:Supabaseのメールリンク認証
    • email:送信先アドレス
    • emailRedirectTo:メールからアクセス後に遷移するURL(本番でもローカルでも自動対応)
  • setLoading(true)setLoading(false):送信中状態の表示切り替え
  • 結果に応じて alert でメッセージを表示

JSX構造の解説

タイトル表示(<h1>

<h1>ログイン or 新規登録</h1>
  • 大見出しとして、画面の役割をユーザーに明確に伝えるために使用。

メールアドレス入力欄(<input>

<input
  type="email"
  value={email}
  onChange={(e) => setEmail(e.target.value)}
  placeholder="you@example.com"
/>

  • type="email":メール用キーボードがスマホで出るなどの利点あり
  • value={email}:状態に応じて入力欄を動的に反映
  • onChange:入力内容が変わったら状態を更新
  • placeholder:入力例を表示し、用途を明示

送信ボタン(<button>

<button onClick={handleLogin}>
  {loading ? "送信中..." : "メールでログインリンク送信"}
</button>

  • onClick={handleLogin}:クリックで認証処理を実行
  • loading の状態によって、文言が自動で切り替わる

Step 2|投稿画面の作成

最初に**型定義ファイル(types.ts)**を作成し、アプリ全体で共通して使う「ユーザー」と「投稿」の型を定義します。

ファイル:my-diary-app/src/types.ts

export type User = {
  id: string;
  email: string | null;
};
// DB
export type Post = {
  id: string;
  user_id: string;
  content: string;
  created_at: string;
};

型定義のポイント

  • 型の明示はTypeScriptの本質的な強みです。

    どんなデータ構造なのかを事前に宣言しておくことで、

    • エラーの早期発見

    • コード補完の効率化

    • 再利用性の向上

      など多くのメリットがあります。

  • SupabaseのユーザーID(auth.users.id)は string 型、メールアドレスは string | null です。

  • Post型にはデータベース上で必要な情報(id, user_id, content, created_at)を明示しています。


投稿ページの実装

ファイル:src/app/diary/page.tsx

"use client";
import { useEffect, useState, useCallback } from "react";
import type { User, Post } from "@/types";
import { supabase } from "@/lib/supabaseClient";

export default function DiaryPage() {
  // --- 状態管理 ---
  const [user, setUser] = useState<User | null>(null);
  const [content, setContent] = useState("");
  const [posts, setPosts] = useState<Post[]>([]);

  // --- 認証チェック ---
  useEffect(() => {
    supabase.auth.getUser().then(({ data: { user } }) => {
      if (!user) {
        window.location.href = "/login";
      } else {
        setUser({ id: user.id, email: user.email ?? null });
        fetchPosts();
      }
    });
  }, []);

  // --- 投稿取得 ---
  const fetchPosts = useCallback(async () => {
    const { data } = (await supabase
      .from("posts")
      .select("*")
      .order("created_at", { ascending: false })
      .throwOnError()) as { data: Post[] | null };

    setPosts(data || []);
  }, []);

  // --- 投稿処理 ---
  const handlePost = async () => {
    if (!content) return;
    await supabase.from("posts").insert([{ content, user_id: user!.id }]);
    setContent("");
    fetchPosts();
  };

  // --- UI ---
  return (
    <div>
      <h2>ようこそ</h2>
      {/* 投稿フォーム */}
      <div>
        <textareavalue={content}
          onChange={(e) => setContent(e.target.value)}
          placeholder="今日の出来事を書いてみよう"
        />
        <button onClick={handlePost} disabled={!content}>
          投稿
        </button>
      </div>
      {/* 投稿一覧 */}
      <ul>
        {posts.map((p) => (
          <li key={p.id}>
            <p>{p.content}</p>
            <p>{new Date(p.created_at).toLocaleString()}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

コード解説

型定義のインポート

import type { User, Post } from "@/types";
  • アプリ全体で統一された「User」「Post」型を利用できます。
  • 型を明示しておくことで、TypeScriptの恩恵を最大限受けられます(型チェック・補完・リファクタ時の安心感)。

状態管理(useState)

const [user, setUser] = useState<User | null>(null);
const [content, setContent] = useState("");
const [posts, setPosts] = useState<Post[]>([]);
  • user:現在ログインしているユーザー情報。最初は null
  • content:投稿フォームに入力したテキストを保持。
  • posts:取得した投稿一覧(Post型の配列)。

型アノテーションについて

  • useState<User | null>(null)

    User型 or null。ログイン未確認状態からスタートし、確認後にUser型の情報が入る。

  • useState<Post[]>([])

    → 投稿(Post)の配列。最初は空配列(投稿なし)からスタート。


認証チェック(useEffect)

useEffect(() => {
  supabase.auth.getUser().then(({ data: { user } }) => {
    if (!user) {
      window.location.href = "/login";
    } else {
      setUser({ id: user.id, email: user.email ?? null });
      fetchPosts();
    }
  });
}, []);
  • 目的:ページ初回表示時に「ログイン状態かどうか」を確認し、未ログインなら /login にリダイレクト。
  • ログイン済みの場合は、ユーザー情報をuser状態に保存し、投稿一覧も取得。

useEffect の意味

  • useEffect(fn, []) は「ページが表示された直後に一度だけ実行される処理」を記述します。

supabase.auth.getUser()

  • Supabaseのセッションストレージから、現在のユーザー情報を取得します。
  • 未ログイン時はusernullになる。

if (!user)

  • ユーザーがいなければ強制的にログイン画面へ遷移。

setUser, fetchPosts

  • ログイン済みの場合はユーザー情報を状態に保存し、さらに投稿データも取得。

投稿取得処理(fetchPosts)

const fetchPosts = useCallback(async () => {
  const { data } = (await supabase
    .from("posts")
    .select("*")
    .order("created_at", { ascending: false })
    .throwOnError()) as { data: Post[] | null };
  setPosts(data || []);
}, []);
  • useCallbackで関数の再生成を抑止(依存配列が空なので初回のみ関数を固定化)。
  • Supabaseクエリで「posts」テーブルからすべての投稿を新着順で取得。
  • setPosts(data || []) で、投稿一覧にデータをセット(データが無い場合は空配列でエラー防止)。

投稿処理(handlePost)

const handlePost = async () => {
  if (!content) return;
  await supabase.from("posts").insert([{ content, user_id: user!.id }]);
  setContent("");
  fetchPosts();
};
  • 投稿内容が空なら何もしない。
  • Supabaseに投稿を挿入(insert)。user_id には現在のユーザーIDを紐づけ。
  • 投稿が終わったら入力欄をクリアし、再度一覧を取得して即座に反映。

user!.id の「!」は「ここでは絶対にuserがnullではない」とTypeScriptに伝えるノン・ヌル・アサーション演算子です。


画面構成(JSX構造)

見出し

<h2>ようこそ</h2>

投稿フォーム

<textarea
  value={content}
  onChange={(e) => setContent(e.target.value)}
  placeholder="今日の出来事を書いてみよう"
/>
<button onClick={handlePost} disabled={!content}>
  投稿
</button>
  • 入力内容をcontent状態に反映。
  • 入力が空の間は投稿ボタンを無効化。

投稿一覧

<ul>
  {posts.map((p) => (
    <li key={p.id}>
      <p>{p.content}</p>
      <p>{new Date(p.created_at).toLocaleString()}</p>
    </li>
  ))}
</ul>
  • 投稿データを新しい順にリスト表示。
  • 投稿日時は日本語ローカル形式で表示。

Step 3|画面遷移設定

まずはsrc/app/page.tsxを次のように書き換えます。

"use client";

import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { supabase } from "@/lib/supabaseClient";

export default function RootPage() {
  const router = useRouter();

  useEffect(() => {
    supabase.auth.getUser().then(({ data: { user } }) => {
      if (user) {
        router.push("/diary");
      } else {
        router.push("/login");
      }
    });
  }, [router]);

  return <div>リダイレクト中...</div>;
}
  • ユーザーがログイン済みなら /diary に移動
  • 未ログインなら /login に移動
  • 画面には一時的に「リダイレクト中…」と表示し、UX低下を防ぎます

サーバー起動前の最終チェックとRLSポリシー設定

アプリが完成に近づいたら、Supabaseの設定で「ログイン後の遷移先」や「権限(RLS)」の確認が必要です。ここでは、正しいリダイレクト設定からRLSポリシーの追加、動作確認までを順に解説します。


1. Supabaseのリダイレクト設定

メールでのログイン(Magic Link)を使ったあとにどのURLに遷移するかをSupabaseで指定する必要があります。

設定手順

  1. Supabaseプロジェクトのダッシュボードを開きます。
  2. 左サイドバーの「Authentication」→「URL Configuration」など(名称は時期によって異なります)に進みます。
  3. Redirect URLs という欄に、次の2つを追加します。
http://localhost:3000/diary

これで、メール認証後のリダイレクト先として**ローカル開発環境(localhost:3000)や日記ページ(/diary)**への遷移が許可されます。


2. サーバーの起動とログイン動作確認

  1. プロジェクトルートで下記コマンドを実行:

    npm run dev
    
  2. http://localhost:3000 にアクセスし、ログイン画面が表示されることを確認します。

  3. メールアドレスを入力し、「メールでログインリンク送信」をクリック。

  4. 届いたメールのリンクをクリックすると、/diary ページに遷移します。

  5. 日記投稿フォームが表示されればOKです。


3. 投稿できない場合のエラーと原因

では日記を書いて投稿してみましょう。

おそらくできないはず。 投稿しても画面に変化がないはず。

画面上で「投稿」ボタンを押しても投稿が表示されない/保存されない場合は、

マウス右クリック →「検証」→ Consoleタブでエラーを確認しましょう。

典型的なエラー

  • POST/GETともに 403 Forbidden

このエラーは、SupabaseのテーブルにRLS(Row Level Security)が有効だが

ポリシー(許可ルール)が未設定のためすべてのリクエストが拒否されている場合に発生します。


4. RLS(Row Level Security)ポリシーの追加

Supabaseは、セキュリティ確保のためデフォルトですべての操作が拒否される仕様です。

各テーブルごとに「どんな条件のときに、誰にアクセスを許可するか」を明示的に設定する必要があります。

SELECT(取得)の権限追加

  1. Supabaseダッシュボードで posts テーブルを選択

  2. 「Authentication」→「Policies」→「Create Policy」をクリック

  3. 右側のテンプレートから「SELECT」を選択

  4. 下のSQLに以下を入力

    auth.uid() = user_id
    
  5. 「Save policy」をクリック

INSERT(追加)の権限追加

同様に「INSERT」についても新しいポリシーを作成し、

SQLに同じく

auth.uid() = user_id

を入力して保存します。

ポイント

  • auth.uid()は「現在ログインしているユーザーのID」
  • user_idは投稿の持ち主ID
  • 「自分の投稿だけ取得・追加できる」状態になります

5. RLSポリシー追加後の動作確認

  1. ページを再読み込みし、再度投稿や一覧表示を試します。
  2. 正常に投稿・取得ができるはずです。


Next.js × Tailwind CSS v3 で手軽にUIを整える

ここでは、最小限の手順で Tailwind CSS v3 を使い、日記アプリのUIを「それなり」に整える方法をまとめます。

詳細解説は省きますので、気になる方は公式ドキュメントもぜひ参照してください。


1. Tailwind CSS v3 のインストール

npm install -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init -p
  • これで tailwind.config.jspostcss.config.js が自動生成されます。
  • package.jsondevDependenciestailwindcss などが追加されているか確認してください。

2. グローバルCSSへの読み込み

ファイル:src/app/globals.css

@tailwind base;
@tailwind components;
@tailwind utilities;
  • 既存のCSSファイルの先頭にこの3行を追加します。

3. tailwind.config.js の調整

ファイル:tailwind.config.js

module.exports = {
  content: ["./src/**/*.{js,ts,jsx,tsx}"], // App Router対応
  theme: {
    extend: {
      colors: {
        primary: { DEFAULT: "#4f46e5" }, // indigo-600 相当
      },
      fontFamily: {
        sans: ["Noto Sans JP", "ui-sans-serif", "system-ui"],
      },
    },
  },
  plugins: [],
};
  • "./src/**/*.{js,ts,jsx,tsx}" でApp Router配下も検出。
  • 色やフォントは好みに応じてカスタマイズ。

4. globals.css をアプリ全体で読み込む

ファイル:src/app/layout.tsx

import './globals.css'  // 先頭付近で一回だけ読み込む

5. 各ページでTailwindクラスを適用

login/page.tsx の例

return (
  <main className="min-h-screen flex items-center justify-center bg-gray-50">
    <div className="w-full max-w-md rounded-xl bg-white p-8 shadow-lg">
      <h1 className="mb-8 text-center text-3xl font-bold">
        ログイン / 新規登録
      </h1>

      <inputtype="email"
        placeholder="sample@example.com"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        className="mb-4 w-full rounded-lg border border-gray-300 p-3
                   focus:border-indigo-500 focus:ring-indigo-500"
      />

      <buttononClick={handleLogin}
        disabled={loading || !email}
        className="w-full rounded-lg bg-indigo-600 py-3 font-medium text-white
                   hover:bg-indigo-700 disabled:opacity-40"
      >
        {loading ? "送信中..." : "メールでログインリンク送信"}
      </button>
    </div>
  </main>
);


diary/page.tsx の例

return (
  <div className="max-w-2xl mx-auto p-6 space-y-6">
    <h2 className="text-xl font-bold border-b pb-2">ようこそ</h2>

    {/* 投稿フォーム */}
    <div className="space-y-4">
      <textareavalue={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="今日の出来事を書いてみよう"
        className="w-full min-h-[120px] rounded-lg border border-gray-300
                   p-3 focus:border-indigo-500 focus:ring-indigo-500"
      />

      <buttononClick={handlePost}
        disabled={!content}
        className="inline-flex items-center gap-2 rounded-lg bg-indigo-600
                   px-4 py-2 font-medium text-white hover:bg-indigo-700
                   disabled:opacity-40"
      >
        投稿
      </button>
    </div>

    {/* 投稿一覧 */}
    <ul className="space-y-6">
      {posts.map((p) => (
        <likey={p.id}
          className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"
        >
          <p className="whitespace-pre-wrap">{p.content}</p>
          <p className="mt-2 text-xs text-gray-500">
            {new Date(p.created_at).toLocaleString()}
          </p>
        </li>
      ))}
    </ul>
  </div>
);


Tailwindクラスのポイント解説(抜粋)

要素 代表クラス 主な効果
<main> min-h-screen flex items-center justify-center bg-gray-50 画面を高さいっぱい&中央寄せ。淡いグレー背景
カード枠 max-w-md rounded-xl bg-white p-8 shadow-lg カードレイアウト。角丸・白背景・大きめ影
入力欄 w-full rounded-lg border-gray-300 p-3 focus:border-indigo-500 幅いっぱい・角丸・枠線、フォーカスで強調色
ボタン w-full rounded-lg bg-indigo-600 py-3 text-white hover:bg-indigo-700 disabled:opacity-40 幅いっぱい・色付き・ホバーや無効状態に反応
投稿一覧 space-y-6 border bg-white p-4 shadow-sm カード状に並べて適度な間隔・影をつけて区切る
日付 mt-2 text-xs text-gray-500 小さく薄いグレーで、サブ情報感を演出

Tailwindで「雰囲気の良いUI」を作るコツ

  1. 大枠はflex/gridやmax-w-で整え、中身で色や影をつける
  2. 入力やボタンは角丸・色変化・無効時の見た目を加えるだけでだいぶ印象が変わる
  3. 可読性が欲しい要素は余白(p-やmb-)を惜しまず使う
  4. クラスの順番は“外側→見た目→状態変化”の順に並べると後で調整しやすい

VercelでNext.js × Supabaseアプリを本番公開する手順

ここではNext.jsアプリをVercelにデプロイし、Supabaseと連携させるまでの一連の手順をまとめます。

設定で迷いやすい部分も解説しているので、今後の参考にしてください。


1. GitHubにコードをアップロード

まずはアプリのコードをGitHubにアップします。

# プロジェクトルートでGit初期化
git init
git add .
git commit -m "first commit"

# GitHubで新規リポジトリ作成後、リモート紐づけ&プッシュ
git remote add origin https://github.com/ユーザー名/リポジトリ名.git
git push -u origin main

注意

  • .env.local など機密情報(SupabaseのURLやAPIキー)は必ず .gitignore で除外してください。

  • npx create-next-app@latest my-diary-app --typescript で作った場合は、.gitignore が自動生成されています。

    念のため .env*.env.local の記載があるか確認してください。


2. Vercelにデプロイ

  1. Vercel公式サイト にアクセスし、GitHubアカウントでログイン

  2. 「Add New → Project」から、先ほどアップロードしたリポジトリを選択

    リポジトリが見つからない場合は「Missing Git repository? Adjust GitHub App Permissions」をクリックし、「Select repositories」から該当リポジトリ(例:my-diary-app)を追加

  1. Import画面で「Environment Variables(環境変数)」を設定
Key Value
NEXT_PUBLIC_SUPABASE_URL https://xxxxx.supabase.co (自分のSupabaseプロジェクトのURL)
NEXT_PUBLIC_SUPABASE_ANON_KEY SupabaseのAnonキー
  • 上記はSupabaseのダッシュボードで「Project Settings → API」から確認できます
  1. 「Deploy」ボタンをクリック

    デプロイが完了したら「Continue to Dashboard」へ

    画面上に発行されたURL(例:https://my-diary-app.vercel.app)を控えておきます


3. Supabase 側の本番用リダイレクトURLを設定

Supabaseの管理画面で「認証後のリダイレクト先」を本番用URLに合わせます。

  • Supabase → Authentication → Settings → URL Configuration
    • Site URL: https://my-diary-app.vercel.app
    • Redirect URLs: https://my-diary-app.vercel.app/diary

これを設定しないと、認証メールのマジックリンクを踏んだときに localhost:3000 にリダイレクトされてしまいます。


4. 本番環境での動作確認

  • デプロイURL(例:https://my-diary-app.vercel.app)にアクセス
  • ログイン〜投稿〜一覧表示までが本番用ドメイン上で正しく動くかチェック
  • もしメール認証後に「ページが移動しない」「エラーになる」場合は、Supabaseのリダイレクト設定や環境変数の値を再確認してください

5. アプリの更新・改善

  • 機能追加・修正はローカルで作業後、コミット&PushすればVercelが自動で再デプロイします
  • デプロイ状況やログはVercelのDashboardから確認できます

おわりに

ここまでお読みいただき、本当にありがとうございました。

初めての記事ということで、至らない部分や細かい説明が不足していたかもしれませんが、

実際に手を動かして「動くものを作る」楽しさを少しでも共有できていたら嬉しいです。

もし、この記事が誰かの最初の一歩や、次のチャレンジのきっかけになれば幸いです。

これからもお互いに学び続けて、一歩ずつ前進していきましょう。

今後も分かりやすい技術記事や、実践的なノウハウを発信していく予定です。

引き続きよろしくお願いします!

Discussion