こんにちは!前回のエピソードでは、Supabase Databaseにcourses
テーブルを設計し、コース一覧ページ(CLP)を構築しました。今回は、コース詳細ページ(Course Detail Page - CDP)を構築し、SEOを最適化します。Next.jsのgetStaticPaths
とgetStaticProps
を活用して静的ページを生成し、Next.js Imageで画像を最適化、Open GraphやTwitter Cardsのメタタグを追加します。これで、コースページが高速で検索エンジンに優しいものになります!
このエピソードのゴール
-
getStaticPaths
とgetStaticProps
でコース詳細ページを静的に生成。 - Next.js Imageを使ってコース画像を最適化。
- Open GraphとTwitter CardsのメタタグでSEOを強化。
- Tailwind CSSでレスポンシブなCDPをデザイン。
必要なもの
- 前回のプロジェクト(
next-lms
)がセットアップ済み。 - Supabaseプロジェクト(
courses
テーブルにデータあり)。 - 基本的なTypeScript、React、Next.jsの知識。
ステップ1: コース詳細データの取得
Supabaseからコースの詳細データを取得する関数を追加します。
-
Supabaseクエリ関数の更新:
src/lib/supabase.ts
にコース詳細取得関数を追加:
import { createClient } from '@supabase/supabase-js';
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient(supabaseUrl, supabaseKey);
export const supabaseServer = () =>
createServerComponentClient({ cookies }, { supabaseUrl, supabaseKey });
export interface Course {
id: string;
title: string;
description: string;
price: number;
instructor_id: string;
created_at: string;
updated_at: string;
image_url?: string; // コース画像(後でSupabase Storageに追加)
}
export async function getCourses(page: number = 1, limit: number = 10) {
const start = (page - 1) * limit;
const end = start + limit - 1;
const { data, error } = await supabase
.from('courses')
.select('*')
.range(start, end)
.order('created_at', { ascending: false });
if (error) throw error;
return data as Course[];
}
export async function getCoursesCount() {
const { count, error } = await supabase
.from('courses')
.select('*', { count: 'exact', head: true });
if (error) throw error;
return count || 0;
}
export async function getCourseById(id: string) {
const { data, error } = await supabase
.from('courses')
.select('*')
.eq('id', id)
.single();
if (error) throw error;
return data as Course;
}
export async function getAllCourseIds() {
const { data, error } = await supabase
.from('courses')
.select('id');
if (error) throw error;
return data.map((item) => ({ id: item.id }));
}
このコードは:
-
getCourseById
: 指定したIDのコース詳細を取得。 -
getAllCourseIds
: すべてのコースIDを取得(getStaticPaths
で使用)。
-
Supabaseテーブルの更新:
Supabaseダッシュボードでcourses
テーブルにimage_url
カラム(型:text
)を追加し、テストデータに画像URLを仮に設定(例:https://via.placeholder.com/600x400
)。
ALTER TABLE courses ADD COLUMN image_url text;
UPDATE courses SET image_url = 'https://via.placeholder.com/600x400' WHERE image_url IS NULL;
ステップ2: コース詳細ページ(CDP)の構築
Next.jsの動的ルーティングを使ってCDPを構築します。src/app/courses/[id]/page.tsx
を作成:
import Image from 'next/image';
import { notFound } from 'next/navigation';
import { getCourseById, Course } from '@/lib/supabase';
import { Metadata } from 'next';
export async function generateStaticParams() {
const courses = await getAllCourseIds();
return courses.map((course) => ({
id: course.id,
}));
}
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
const course = await getCourseById(params.id);
if (!course) return {};
return {
title: `${course.title} | Next.js LMS`,
description: course.description,
openGraph: {
title: course.title,
description: course.description,
images: [{ url: course.image_url || 'https://via.placeholder.com/600x400' }],
},
twitter: {
card: 'summary_large_image',
title: course.title,
description: course.description,
images: [course.image_url || 'https://via.placeholder.com/600x400'],
},
};
}
export default async function CourseDetailPage({ params }: { params: { id: string } }) {
const course = await getCourseById(params.id);
if (!course) {
notFound();
}
return (
<main className="container mx-auto p-4">
<div className="bg-white rounded-lg shadow-lg p-6">
<Image
src={course.image_url || 'https://via.placeholder.com/600x400'}
alt={course.title}
width={600}
height={400}
className="w-full h-64 object-cover rounded-lg mb-6"
priority
/>
<h1 className="text-3xl font-bold mb-4">{course.title}</h1>
<p className="text-gray-600 mb-4">{course.description}</p>
<p className="text-2xl font-semibold text-primary mb-4">{course.price.toLocaleString()} 円</p>
<a
href="/checkout"
className="bg-primary text-white px-6 py-3 rounded hover:bg-opacity-90"
>
このコースを購入
</a>
</div>
</main>
);
}
このコードは:
-
generateStaticParams
: すべてのコースIDを事前生成。 -
generateMetadata
: 動的なSEOメタタグ(タイトル、説明、OG/Twitter)を生成。 - Next.js Imageで画像を最適化(
priority
でLCPを改善)。 - Tailwind CSSでレスポンシブなデザインを適用。
ステップ3: ナビゲーションの強化
コース詳細ページへのリンクが正しく機能するよう、ナビゲーションを微調整します。src/app/courses/page.tsx
を確認し、コースカードのリンクが/courses/[id]
形式であることを確認:
<Link href={`/courses/${course.id}`} key={course.id}>
<div className="border rounded-lg p-4 hover:shadow-lg transition">
<Image
src={course.image_url || `/placeholder-course-${course.id}.jpg`}
alt={course.title}
width={300}
height={200}
className="w-full h-40 object-cover rounded"
loading="lazy"
/>
<h2 className="text-xl font-semibold mt-2">{course.title}</h2>
<p className="text-gray-600 line-clamp-2">{course.description}</p>
<p className="text-primary font-bold mt-2">{course.price.toLocaleString()} 円</p>
</div>
</Link>
注意: 画像URLをcourse.image_url
に更新し、プレースホルダー画像を条件付きで使用。
ステップ4: 動作確認
- Supabaseダッシュボードで
courses
テーブルのimage_url
カラムとテストデータが正しいことを確認。 - 開発サーバーを起動:
npm run dev
-
http://localhost:3000/courses
にアクセスし、コースカードをクリックして/courses/[id]
に遷移:- コースタイトル、説明、価格、画像が正しく表示される。
- 画像が高速に読み込まれ、レスポンシブに表示される。
- ブラウザの開発者ツールで
<head>
内のメタタグ(OG/Twitter)が正しいことを確認。
- ソーシャルメディア(例: Twitter)にURLを貼り付け、プレビューが正しく表示されるか確認。
- 存在しないコースID(例:
/courses/invalid-id
)にアクセスし、404ページが表示されることを確認。
エラーがあれば、Supabaseのテーブル設定やgetCourseById
のクエリを確認してください。
まとめと次のステップ
このエピソードでは、コース詳細ページ(CDP)を構築し、SEOを最適化しました。getStaticPaths
とgetStaticProps
で静的ページを生成し、Next.js Imageで画像を最適化、Open GraphやTwitter Cardsでソーシャル共有を強化しました。これで、コースページが高速で検索エンジンに優しいものになりました!
次回のエピソードでは、Supabase Storageを使ってビデオレッスンを管理・配信します。lessons
テーブルの設計と、React Playerを使ったビデオ再生機能を実装しますので、引き続きお楽しみに!
この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです!次回のエピソードもお楽しみに!