2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsとSupabaseでオンライ学習プラットフォーム(LMS)を構築する | エピソード4: コース詳細ページとSEO最適化

Posted at

こんにちは!前回のエピソードでは、Supabase Databaseにcoursesテーブルを設計し、コース一覧ページ(CLP)を構築しました。今回は、コース詳細ページ(Course Detail Page - CDP)を構築し、SEOを最適化します。Next.jsのgetStaticPathsgetStaticPropsを活用して静的ページを生成し、Next.js Imageで画像を最適化、Open GraphやTwitter Cardsのメタタグを追加します。これで、コースページが高速で検索エンジンに優しいものになります!

このエピソードのゴール

  • getStaticPathsgetStaticPropsでコース詳細ページを静的に生成。
  • Next.js Imageを使ってコース画像を最適化。
  • Open GraphとTwitter CardsのメタタグでSEOを強化。
  • Tailwind CSSでレスポンシブなCDPをデザイン。

必要なもの

  • 前回のプロジェクト(next-lms)がセットアップ済み。
  • Supabaseプロジェクト(coursesテーブルにデータあり)。
  • 基本的なTypeScript、React、Next.jsの知識。

ステップ1: コース詳細データの取得

Supabaseからコースの詳細データを取得する関数を追加します。

  1. 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で使用)。
  1. 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: 動作確認

  1. Supabaseダッシュボードでcoursesテーブルのimage_urlカラムとテストデータが正しいことを確認。
  2. 開発サーバーを起動:
npm run dev
  1. http://localhost:3000/coursesにアクセスし、コースカードをクリックして/courses/[id]に遷移:
    • コースタイトル、説明、価格、画像が正しく表示される。
    • 画像が高速に読み込まれ、レスポンシブに表示される。
    • ブラウザの開発者ツールで<head>内のメタタグ(OG/Twitter)が正しいことを確認。
  2. ソーシャルメディア(例: Twitter)にURLを貼り付け、プレビューが正しく表示されるか確認。
  3. 存在しないコースID(例: /courses/invalid-id)にアクセスし、404ページが表示されることを確認。

エラーがあれば、Supabaseのテーブル設定やgetCourseByIdのクエリを確認してください。


まとめと次のステップ

このエピソードでは、コース詳細ページ(CDP)を構築し、SEOを最適化しました。getStaticPathsgetStaticPropsで静的ページを生成し、Next.js Imageで画像を最適化、Open GraphやTwitter Cardsでソーシャル共有を強化しました。これで、コースページが高速で検索エンジンに優しいものになりました!

次回のエピソードでは、Supabase Storageを使ってビデオレッスンを管理・配信します。lessonsテーブルの設計と、React Playerを使ったビデオ再生機能を実装しますので、引き続きお楽しみに!


この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです!次回のエピソードもお楽しみに!

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?