はじめに
この記事では、Next.jsを使用してプロダクションレディなSaaSダッシュボードを構築する方法を解説します。SaaS(Software as a Service)ビジネスにおいて、ダッシュボードはユーザーとサービスをつなぐ重要なインターフェースです。
本記事で学べること:
- Next.jsプロジェクトのセットアップと基本構成
- TailwindCSSとTremorを使った美しいUI構築
- Supabaseによるデータ管理とリアルタイム更新
- NextAuth.jsとStripeを使った認証・決済システム
想定読者:
- Next.jsの基礎を理解している開発者
- SaaSアプリケーションの開発に興味がある方
- ダッシュボードUIの効率的な構築方法を学びたい方
SaaSダッシュボードとは
SaaSダッシュボードは、SaaSアプリケーションの中心的な機能の1つです。ユーザーがアクセスするデータ管理や統計情報、設定などを提供するインターフェースで、Next.jsのような最新のフレームワークを使うことで、パフォーマンスに優れた美しいダッシュボードを構築できます。
ダッシュボードに求められる機能
一般的なSaaSダッシュボードには、以下のような機能が求められます:
| 機能 | 説明 | 実装例 |
|---|---|---|
| データ可視化 | グラフやチャートによる統計表示 | 売上推移、ユーザー数の変化 |
| リアルタイム更新 | データの自動更新 | 通知、アクティビティフィード |
| ユーザー管理 | アカウント設定、権限管理 | プロフィール編集、チームメンバー招待 |
| 課金管理 | サブスクリプション、請求履歴 | プラン変更、支払い方法の管理 |
| レスポンシブ対応 | 様々なデバイスでの表示 | モバイル、タブレット対応 |
Next.jsとSaaS UIの選定理由
Next.jsを選ぶ理由
Next.jsは、フロントエンド開発において、サーバーサイドレンダリング(SSR)とクライアントサイドレンダリング(CSR)を両立できる強力なフレームワークです。
主な利点:
- ハイブリッドレンダリング: SSR、SSG、ISRを状況に応じて使い分け可能
- 優れたパフォーマンス: 自動的なコード分割と最適化
- SEO対応: サーバーサイドレンダリングによるSEO最適化
- APIルート: バックエンドAPIをNext.js内に統合可能
- TypeScript対応: 型安全な開発が可能
// Next.js 14のApp Routerを使用したページ構成例
// app/dashboard/page.tsx
import { Suspense } from 'react';
import DashboardMetrics from '@/components/DashboardMetrics';
import LoadingSkeleton from '@/components/LoadingSkeleton';
// サーバーコンポーネントとして実行され、SEOに有利
export default async function DashboardPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">ダッシュボード</h1>
{/* Suspenseを使用してローディング状態を管理 */}
<Suspense fallback={<LoadingSkeleton />}>
<DashboardMetrics />
</Suspense>
</div>
);
}
SaaS UIライブラリの選択
SaaS UIは、ダッシュボードに特化したUIコンポーネントを提供するライブラリで、管理画面やデータの可視化に便利です。例えば、ナビゲーションバーやサイドバー、データテーブル、チャート表示など、一般的なダッシュボードに必要な機能が揃っています。
今回使用する主要なライブラリ:
| ライブラリ | 用途 | 特徴 |
|---|---|---|
| TailwindCSS | スタイリング | ユーティリティファーストのCSSフレームワーク |
| Tremor | データ可視化 | React向けのダッシュボードコンポーネント |
| Saas UI | UIコンポーネント | SaaS向けに最適化されたコンポーネント |
プロジェクトのセットアップ
初期設定
まず、Next.jsのアプリケーションを初期化します。create-next-appコマンドを使って新しいプロジェクトを作成し、必要なパッケージをインストールします。
# Next.js 14プロジェクトの作成(TypeScript、TailwindCSS、App Router有効)
npx create-next-app@latest my-saas-dashboard --typescript --tailwind --app
# プロジェクトディレクトリに移動
cd my-saas-dashboard
# 必要なパッケージをインストール
npm install @tremor/react @supabase/supabase-js
npm install @saas-ui/react @chakra-ui/react @emotion/react @emotion/styled framer-motion
npm install next-auth @stripe/stripe-js
# 開発用パッケージ
npm install -D @types/node
ディレクトリ構成
推奨するディレクトリ構成は以下の通りです:
my-saas-dashboard/
├── app/ # App Router
│ ├── (auth)/ # 認証関連ページ
│ │ ├── login/
│ │ └── register/
│ ├── (dashboard)/ # ダッシュボードページ
│ │ ├── layout.tsx # ダッシュボード共通レイアウト
│ │ ├── page.tsx # メインダッシュボード
│ │ ├── analytics/ # 分析ページ
│ │ ├── settings/ # 設定ページ
│ │ └── users/ # ユーザー管理
│ ├── api/ # APIルート
│ │ ├── auth/
│ │ └── stripe/
│ └── layout.tsx # ルートレイアウト
├── components/ # 共通コンポーネント
│ ├── charts/ # グラフ・チャート
│ ├── layout/ # レイアウトコンポーネント
│ └── ui/ # UIコンポーネント
├── lib/ # ユーティリティ
│ ├── supabase.ts # Supabaseクライアント
│ └── stripe.ts # Stripeクライアント
└── types/ # 型定義
TailwindCSSの設定
tailwind.config.tsを以下のように設定し、Tremorのコンポーネントをサポートします:
// tailwind.config.ts
import type { Config } from 'tailwindcss';
const config: Config = {
// Tremorのコンポーネントもスキャン対象に含める
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./node_modules/@tremor/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
// ダッシュボード用のカスタムカラー
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
},
// ダークモード用の背景色
dashboard: {
bg: '#0f172a',
card: '#1e293b',
border: '#334155',
},
},
},
},
// ダークモード対応
darkMode: 'class',
plugins: [],
};
export default config;
サイドバーとナビゲーションの実装
ダッシュボードでは、サイドバーが重要な役割を果たします。ユーザーのナビゲーションを円滑にするため、Saas UIのSidebarコンポーネントを利用します。
サイドバーコンポーネント
// components/layout/DashboardSidebar.tsx
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import {
HomeIcon,
UsersIcon,
ChartBarIcon,
CogIcon,
CreditCardIcon,
} from '@heroicons/react/24/outline';
// ナビゲーション項目の型定義
interface NavItem {
name: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
}
// ナビゲーション項目の定義
const navigation: NavItem[] = [
{ name: 'ダッシュボード', href: '/dashboard', icon: HomeIcon },
{ name: 'ユーザー管理', href: '/dashboard/users', icon: UsersIcon },
{ name: '分析', href: '/dashboard/analytics', icon: ChartBarIcon },
{ name: '課金管理', href: '/dashboard/billing', icon: CreditCardIcon },
{ name: '設定', href: '/dashboard/settings', icon: CogIcon },
];
export default function DashboardSidebar() {
// 現在のパスを取得してアクティブ状態を判定
const pathname = usePathname();
return (
<aside className="fixed left-0 top-0 z-40 h-screen w-64 bg-dashboard-bg border-r border-dashboard-border">
{/* ロゴエリア */}
<div className="flex h-16 items-center justify-center border-b border-dashboard-border">
<span className="text-xl font-bold text-white">SaaS Dashboard</span>
</div>
{/* ナビゲーションリスト */}
<nav className="mt-6 px-4">
<ul className="space-y-2">
{navigation.map((item) => {
// 現在のページかどうかを判定
const isActive = pathname === item.href;
return (
<li key={item.name}>
<Link
href={item.href}
className={`
flex items-center gap-3 rounded-lg px-4 py-3 text-sm font-medium transition-colors
${isActive
? 'bg-primary-600 text-white'
: 'text-gray-400 hover:bg-dashboard-card hover:text-white'
}
`}
>
<item.icon className="h-5 w-5" />
{item.name}
</Link>
</li>
);
})}
</ul>
</nav>
{/* ユーザー情報(サイドバー下部) */}
<div className="absolute bottom-0 left-0 right-0 border-t border-dashboard-border p-4">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-primary-600" />
<div>
<p className="text-sm font-medium text-white">ユーザー名</p>
<p className="text-xs text-gray-400">user@example.com</p>
</div>
</div>
</div>
</aside>
);
}
ダッシュボードレイアウト
// app/(dashboard)/layout.tsx
import DashboardSidebar from '@/components/layout/DashboardSidebar';
import DashboardHeader from '@/components/layout/DashboardHeader';
interface DashboardLayoutProps {
children: React.ReactNode;
}
export default function DashboardLayout({ children }: DashboardLayoutProps) {
return (
<div className="min-h-screen bg-dashboard-bg">
{/* サイドバー */}
<DashboardSidebar />
{/* メインコンテンツエリア */}
<div className="ml-64">
{/* ヘッダー */}
<DashboardHeader />
{/* ページコンテンツ */}
<main className="p-6">
{children}
</main>
</div>
</div>
);
}
データの表示と管理
ダッシュボードの主な機能として、データの可視化が挙げられます。例えば、売上データやユーザーのアクティビティを表示する場合、Tremorライブラリを使って簡単にチャートを作成できます。
メトリクスカードの実装
// components/charts/MetricsCard.tsx
'use client';
import { Card, Metric, Text, Flex, BadgeDelta } from '@tremor/react';
// メトリクスの型定義
interface MetricsCardProps {
title: string; // カードのタイトル
metric: string; // メインの数値
delta: string; // 前期比の変化率
deltaType: 'increase' | 'decrease' | 'unchanged'; // 変化の種類
}
export default function MetricsCard({
title,
metric,
delta,
deltaType,
}: MetricsCardProps) {
return (
<Card className="bg-dashboard-card border-dashboard-border">
<Flex justifyContent="between" alignItems="center">
<Text className="text-gray-400">{title}</Text>
{/* 変化率を色分けして表示 */}
<BadgeDelta deltaType={deltaType} size="sm">
{delta}
</BadgeDelta>
</Flex>
{/* メインの数値を大きく表示 */}
<Metric className="mt-2 text-white">{metric}</Metric>
</Card>
);
}
売上チャートの実装
// components/charts/SalesChart.tsx
'use client';
import { Card, Title, LineChart } from '@tremor/react';
// 売上データの型定義
interface SalesData {
date: string;
sales: number;
orders: number;
}
// サンプルデータ(実際はAPIから取得)
const salesData: SalesData[] = [
{ date: '2024-01-01', sales: 3000, orders: 45 },
{ date: '2024-01-02', sales: 4500, orders: 62 },
{ date: '2024-01-03', sales: 3800, orders: 51 },
{ date: '2024-01-04', sales: 5200, orders: 78 },
{ date: '2024-01-05', sales: 4100, orders: 55 },
{ date: '2024-01-06', sales: 6300, orders: 89 },
{ date: '2024-01-07', sales: 5800, orders: 82 },
];
// 数値を通貨形式にフォーマット
const formatCurrency = (value: number): string => {
return new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY',
}).format(value);
};
export default function SalesChart() {
return (
<Card className="bg-dashboard-card border-dashboard-border">
<Title className="text-white">売上推移</Title>
<LineChart
className="mt-4 h-72"
data={salesData}
index="date"
categories={['sales', 'orders']}
colors={['blue', 'emerald']}
valueFormatter={formatCurrency}
yAxisWidth={80}
// ツールチップのカスタマイズ
showLegend={true}
showGridLines={true}
curveType="monotone"
/>
</Card>
);
}
Supabaseとの連携
Supabaseなどのバックエンドサービスを使えば、リアルタイムでデータを取得して更新することも可能です。Supabaseは、PostgreSQLに基づくデータベースを持つBaaS(Backend as a Service)で、認証やデータ管理の機能を簡単に統合できます。
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
// 環境変数から接続情報を取得
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
// Supabaseクライアントの作成(シングルトンパターン)
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
// データベースの型定義
export interface Database {
public: {
Tables: {
sales: {
Row: {
id: number;
date: string;
amount: number;
created_at: string;
};
Insert: Omit<Database['public']['Tables']['sales']['Row'], 'id' | 'created_at'>;
};
users: {
Row: {
id: string;
email: string;
name: string;
plan: 'free' | 'pro' | 'enterprise';
created_at: string;
};
};
};
};
}
// components/charts/RealtimeSalesChart.tsx
'use client';
import { useEffect, useState } from 'react';
import { Card, Title, AreaChart } from '@tremor/react';
import { supabase } from '@/lib/supabase';
interface SalesRecord {
id: number;
date: string;
amount: number;
}
export default function RealtimeSalesChart() {
const [salesData, setSalesData] = useState<SalesRecord[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// 初期データの取得
const fetchInitialData = async () => {
const { data, error } = await supabase
.from('sales')
.select('*')
.order('date', { ascending: true })
.limit(30);
if (error) {
console.error('データ取得エラー:', error);
return;
}
setSalesData(data || []);
setIsLoading(false);
};
fetchInitialData();
// リアルタイム更新のサブスクリプション
const subscription = supabase
.channel('sales_changes')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'sales',
},
(payload) => {
// 新しいデータをリストに追加
setSalesData((current) => [...current, payload.new as SalesRecord]);
}
)
.subscribe();
// クリーンアップ関数
return () => {
subscription.unsubscribe();
};
}, []);
if (isLoading) {
return <div className="animate-pulse h-72 bg-dashboard-card rounded-lg" />;
}
return (
<Card className="bg-dashboard-card border-dashboard-border">
<Title className="text-white">リアルタイム売上</Title>
<AreaChart
className="mt-4 h-72"
data={salesData}
index="date"
categories={['amount']}
colors={['blue']}
showLegend={false}
/>
</Card>
);
}
認証と支払いシステムの導入
SaaSアプリケーションでは、ユーザーの認証や支払い処理が必要不可欠です。NextAuth.jsでJWTベースの認証を実装し、Stripeを使ったサブスクリプション管理を行います。
NextAuth.js の設定
// app/api/auth/[...nextauth]/route.ts
import NextAuth, { NextAuthOptions } from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import { SupabaseAdapter } from '@auth/supabase-adapter';
// NextAuth.jsの設定オプション
export const authOptions: NextAuthOptions = {
// 認証プロバイダーの設定
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
// Supabaseをデータベースアダプターとして使用
adapter: SupabaseAdapter({
url: process.env.NEXT_PUBLIC_SUPABASE_URL!,
secret: process.env.SUPABASE_SERVICE_ROLE_KEY!,
}),
// セッション設定
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30日間
},
// カスタムページの設定
pages: {
signIn: '/login',
error: '/auth/error',
},
// コールバック関数
callbacks: {
// JWTトークンにユーザー情報を追加
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
// セッションにユーザーIDを追加
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string;
}
return session;
},
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Stripe サブスクリプション管理
// components/billing/SubscriptionManager.tsx
'use client';
import { useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Card, Title, Text, Button, Badge } from '@tremor/react';
// Stripeインスタンスの初期化
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
// プラン情報の型定義
interface Plan {
id: string;
name: string;
price: number;
features: string[];
priceId: string;
}
// 利用可能なプラン
const plans: Plan[] = [
{
id: 'free',
name: 'Free',
price: 0,
features: ['基本機能', '月間1,000リクエスト', 'コミュニティサポート'],
priceId: '',
},
{
id: 'pro',
name: 'Pro',
price: 2980,
features: ['全機能', '月間100,000リクエスト', '優先サポート', 'API アクセス'],
priceId: 'price_xxxxx',
},
{
id: 'enterprise',
name: 'Enterprise',
price: 9800,
features: ['全機能', '無制限リクエスト', '専任サポート', 'カスタム連携', 'SLA保証'],
priceId: 'price_yyyyy',
},
];
interface SubscriptionManagerProps {
currentPlan: string;
}
export default function SubscriptionManager({ currentPlan }: SubscriptionManagerProps) {
const [isLoading, setIsLoading] = useState<string | null>(null);
// チェックアウトセッションの作成
const handleSubscribe = async (priceId: string, planId: string) => {
if (!priceId) return; // Freeプランの場合は何もしない
setIsLoading(planId);
try {
// APIエンドポイントを呼び出してチェックアウトセッションを作成
const response = await fetch('/api/stripe/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ priceId }),
});
const { sessionId } = await response.json();
// Stripeチェックアウトにリダイレクト
const stripe = await stripePromise;
await stripe?.redirectToCheckout({ sessionId });
} catch (error) {
console.error('チェックアウトエラー:', error);
} finally {
setIsLoading(null);
}
};
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{plans.map((plan) => (
<Card
key={plan.id}
className={`bg-dashboard-card border-2 ${
currentPlan === plan.id
? 'border-primary-500'
: 'border-dashboard-border'
}`}
>
<div className="flex items-center justify-between">
<Title className="text-white">{plan.name}</Title>
{currentPlan === plan.id && (
<Badge color="blue">現在のプラン</Badge>
)}
</div>
<div className="mt-4">
<span className="text-3xl font-bold text-white">
{plan.price === 0 ? '無料' : `¥${plan.price.toLocaleString()}`}
</span>
{plan.price > 0 && (
<span className="text-gray-400">/月</span>
)}
</div>
<ul className="mt-6 space-y-3">
{plan.features.map((feature) => (
<li key={feature} className="flex items-center text-gray-300">
<svg
className="h-5 w-5 text-primary-500 mr-2"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
{feature}
</li>
))}
</ul>
<Button
className="mt-6 w-full"
onClick={() => handleSubscribe(plan.priceId, plan.id)}
disabled={currentPlan === plan.id || isLoading === plan.id}
loading={isLoading === plan.id}
>
{currentPlan === plan.id ? '利用中' : 'このプランを選択'}
</Button>
</Card>
))}
</div>
);
}
Stripe Webhook の処理
// app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { supabase } from '@/lib/supabase';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get('stripe-signature')!;
let event: Stripe.Event;
try {
// Webhookの署名を検証
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (error) {
console.error('Webhook署名検証エラー:', error);
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
// イベントタイプに応じた処理
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
// ユーザーのプランを更新
await supabase
.from('users')
.update({ plan: 'pro' })
.eq('stripe_customer_id', session.customer);
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
// サブスクリプションがキャンセルされた場合、Freeプランに戻す
await supabase
.from('users')
.update({ plan: 'free' })
.eq('stripe_customer_id', subscription.customer);
break;
}
default:
console.log(`未処理のイベントタイプ: ${event.type}`);
}
return NextResponse.json({ received: true });
}
ダッシュボードページの完成形
これまでのコンポーネントを組み合わせて、完成形のダッシュボードページを作成します。
// app/(dashboard)/page.tsx
import { Suspense } from 'react';
import { Grid, Col } from '@tremor/react';
import MetricsCard from '@/components/charts/MetricsCard';
import SalesChart from '@/components/charts/SalesChart';
import RealtimeSalesChart from '@/components/charts/RealtimeSalesChart';
import RecentActivityList from '@/components/dashboard/RecentActivityList';
import LoadingSkeleton from '@/components/ui/LoadingSkeleton';
// メトリクスデータ(実際はAPIから取得)
const metrics = [
{ title: '総売上', metric: '¥1,234,567', delta: '+12.3%', deltaType: 'increase' as const },
{ title: 'アクティブユーザー', metric: '1,234', delta: '+5.2%', deltaType: 'increase' as const },
{ title: '新規登録', metric: '89', delta: '-2.1%', deltaType: 'decrease' as const },
{ title: 'コンバージョン率', metric: '3.2%', delta: '0%', deltaType: 'unchanged' as const },
];
export default function DashboardPage() {
return (
<div className="space-y-6">
{/* ページタイトル */}
<div>
<h1 className="text-2xl font-bold text-white">ダッシュボード</h1>
<p className="text-gray-400">サービスの概要を確認できます</p>
</div>
{/* メトリクスカード */}
<Grid numItemsMd={2} numItemsLg={4} className="gap-6">
{metrics.map((metric) => (
<Col key={metric.title}>
<MetricsCard {...metric} />
</Col>
))}
</Grid>
{/* チャートセクション */}
<Grid numItemsMd={2} className="gap-6">
<Col>
<Suspense fallback={<LoadingSkeleton height="h-96" />}>
<SalesChart />
</Suspense>
</Col>
<Col>
<Suspense fallback={<LoadingSkeleton height="h-96" />}>
<RealtimeSalesChart />
</Suspense>
</Col>
</Grid>
{/* 最近のアクティビティ */}
<Suspense fallback={<LoadingSkeleton height="h-64" />}>
<RecentActivityList />
</Suspense>
</div>
);
}
パフォーマンス最適化のポイント
SaaSダッシュボードでは、多くのデータを扱うためパフォーマンス最適化が重要です。
1. サーバーコンポーネントの活用
// データ取得はサーバーコンポーネントで行う
// app/(dashboard)/analytics/page.tsx
import { supabase } from '@/lib/supabase';
import AnalyticsChart from '@/components/charts/AnalyticsChart';
// このコンポーネントはサーバーで実行される
export default async function AnalyticsPage() {
// サーバー側でデータを取得(クライアントに露出しない)
const { data: analyticsData } = await supabase
.from('analytics')
.select('*')
.order('date', { ascending: false })
.limit(30);
return <AnalyticsChart data={analyticsData || []} />;
}
2. 動的インポートによるコード分割
// components/dashboard/HeavyChart.tsx
'use client';
import dynamic from 'next/dynamic';
// 重いコンポーネントは動的にインポート
const HeavyChartComponent = dynamic(
() => import('@/components/charts/ComplexVisualization'),
{
loading: () => <div className="animate-pulse h-96 bg-dashboard-card rounded-lg" />,
ssr: false, // クライアントサイドでのみレンダリング
}
);
export default HeavyChartComponent;
3. データのキャッシュ戦略
// lib/cache.ts
import { unstable_cache } from 'next/cache';
import { supabase } from './supabase';
// データをキャッシュして再利用
export const getCachedMetrics = unstable_cache(
async () => {
const { data } = await supabase
.from('metrics')
.select('*')
.single();
return data;
},
['dashboard-metrics'],
{
revalidate: 60, // 60秒ごとに再検証
tags: ['metrics'],
}
);
まとめ
Next.jsとSaaS UIを使ったSaaSダッシュボードの構築は、パフォーマンスと開発効率の両面で非常に優れています。リアルタイムのデータ表示やユーザー管理、直感的なUIを簡単に実装できるため、SaaSビジネスの成功に大きく寄与します。
本記事で学んだこと
- プロジェクト構成: Next.js 14のApp Routerを使った効率的なディレクトリ構成
- UI構築: TailwindCSSとTremorを使ったモダンなダッシュボードUI
- データ管理: Supabaseを使ったリアルタイムデータ連携
- 認証・課金: NextAuth.jsとStripeによるユーザー管理と課金システム
- パフォーマンス: サーバーコンポーネントやキャッシュを活用した最適化
次のステップ
- テストの追加(Jest、Playwright)
- エラーハンドリングの強化
- アクセシビリティ対応
- 多言語対応(i18n)
- モバイルアプリ対応(React Native)
参考文献
- Next.js Documentation - Next.js公式ドキュメント
- Tremor Documentation - Tremorライブラリの使い方
- Saas UI - SaaS向けUIコンポーネントライブラリ
- Supabase Documentation - Supabaseの設定とAPI
- NextAuth.js - Next.js向け認証ライブラリ
- Stripe Documentation - Stripe決済システムの実装ガイド
- TailwindCSS - ユーティリティファーストCSSフレームワーク