SvelteKitで作るフルスタック個人ブログ | 第2回: ブログのUI構築と静的データ処理

こんにちは！「SvelteKitで作るフルスタック個人ブログ」シリーズの第2回へようこそ。第1回では、SvelteKitの概要を紹介し、**Skeleton UI**と**Tailwind CSS**を使ってブログの基本レイアウトを構築しました。今回は、ブログのUIをさらに充実させ、**記事一覧**や**記事詳細ページ**を作ります。最初は静的データ（モックデータ）を使い、Svelteの直感的な記述を活かしてコンポーネントを構築します。ReactやNext.jsとの比較も交えながら、SvelteKitの魅力を深掘りしましょう！

---

## この記事の目標
- SvelteKitの**ファイルベースのルーティング**を理解する。
- Svelteコンポーネントで**記事一覧**と**記事詳細ページ**を構築する。
- **Skeleton UI**を活用して美しいUIを作る。
- Svelteの**リアクティブ変数**を使ってデータを簡単に管理する。
- Reactとの違いを比較して、Svelteの開発体験（DX）の良さを体感する。

最終的には、ブログのフロントエンドが完成し、静的な記事データを表示できる状態を目指します。では、始めましょう！

---

## SvelteKitのルーティングをおさらい
SvelteKitは**ファイルベースのルーティング**を採用しており、プロジェクトの`src/routes/`フォルダ内のファイル構造がそのままURLに対応します。例えば：
- `src/routes/+page.svelte` → ホームページ (`/`)
- `src/routes/blog/+page.svelte` → ブログ一覧ページ (`/blog`)
- `src/routes/about/+page.svelte` → Aboutページ (`/about`)

今回は、以下のページを追加します：
- **ブログ一覧ページ** (`/blog`)：記事のリストを表示。
- **記事詳細ページ** (`/blog/[slug]`)：個別の記事内容を表示。

これらは、Next.jsのファイルベースのルーティング（例：`pages/blog/index.js`や`pages/blog/[slug].js`）と似ていますが、SvelteKitでは`.svelte`ファイルでUIを直接記述できるため、より直感的です。

---

## ブログ一覧ページの構築

### 1. モックデータの準備
まず、ブログ記事のモックデータを作成します。`src/lib/data/posts.js`に以下を追加：

```js
export const posts = [
  {
    slug: 'first-post',
    title: '初めてのブログ記事',
    excerpt: 'これはSvelteKitで作った最初の記事です。',
    date: '2025-04-10',
    content: 'ここに記事の全文が入ります。SvelteKitはシンプルで高速なフレームワークです。'
  },
  {
    slug: 'second-post',
    title: 'SvelteKitの魅力',
    excerpt: 'SvelteKitの開発体験について語ります。',
    date: '2025-04-11',
    content: 'SvelteKitは、ReactやNext.jsと比べて記述量が少なく、初心者にも優しいです。'
  }
];
```

このデータを使って、記事一覧を表示します。`src/lib/`は、プロジェクト全体で再利用可能なコードを置く場所として便利です。

### 2. ブログ一覧ページの作成
`src/routes/blog/+page.svelte`を作成し、以下を記述：

```svelte
<script>
  import { posts } from '$lib/data/posts';
  import { Card, CardHeader, CardContent, CardFooter } from '@skeletonlabs/skeleton';
</script>

<div class="container mx-auto p-8">
  <h1 class="h1 mb-8">ブログ記事一覧</h1>
  <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
    {#each posts as post}
      <Card>
        <CardHeader>
          <h2 class="h3">{post.title}</h2>
          <p class="text-sm text-surface-500">{post.date}</p>
        </CardHeader>
        <CardContent>
          <p>{post.excerpt}</p>
        </CardContent>
        <CardFooter>
          <a href="/blog/{post.slug}" class="btn variant-filled-primary">続きを読む</a>
        </CardFooter>
      </Card>
    {/each}
  </div>
</div>
```

- **解説**：
  - `posts`をインポートし、`#each`ディレクティブで記事をループ表示。
  - **Skeleton UI**の`Card`コンポーネントを使って、記事をカード形式で表示。
  - 各カードには、タイトル、投稿日、抜粋、詳細ページへのリンクを配置。
  - `grid-cols-1 md:grid-cols-2`で、レスポンシブなグリッドレイアウトを適用。

### 3. Reactとの比較
Reactで同じ一覧を作る場合、以下のように`useState`や`map`を使います：

```jsx
// React
import { useState } from 'react';

const posts = [...]; // モックデータ
function BlogList() {
  return (
    <div className="container mx-auto p-8">
      <h1>ブログ記事一覧</h1>
      <div className="grid">
        {posts.map(post => (
          <div key={post.slug} className="card">
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
            <a href={`/blog/${post.slug}`}>続きを読む</a>
          </div>
        ))}
      </div>
    </div>
  );
}
```

Reactでは、状態管理やJSXの記述量が多くなりがちです。一方、Svelteでは`#each`やテンプレート構文がシンプルで、**HTMLに近い感覚**で書けるのが魅力です。

---

## 記事詳細ページの構築

### 1. ダイナミックルートの作成
記事詳細ページは、URLの`slug`に基づいて表示します。`src/routes/blog/[slug]/+page.svelte`を作成：

```svelte
<script>
  import { posts } from '$lib/data/posts';
  import { error } from '@sveltejs/kit';
  
  export let data;

  const post = posts.find(p => p.slug === data.slug);
  if (!post) {
    throw error(404, '記事が見つかりません');
  }
</script>

<div class="container mx-auto p-8">
  <article class="card p-6 bg-surface-50">
    <header class="mb-4">
      <h1 class="h2">{post.title}</h1>
      <p class="text-sm text-surface-500">{post.date}</p>
    </header>
    <section class="prose">
      <p>{post.content}</p>
    </section>
    <footer class="mt-6">
      <a href="/blog" class="btn variant-ghost-primary">一覧に戻る</a>
    </footer>
  </article>
</div>
```

- **解説**：
  - `export let data`で、SvelteKitが提供する`slug`を受け取る（後述）。
  - `posts`から`slug`に一致する記事を検索。見つからない場合は`404`エラーを投げる。
  - **Skeleton UI**の`card`クラスと`prose`（Tailwindのタイポグラフィ）で、読みやすい記事ページを構築。

### 2. ルートデータの取得
SvelteKitでは、ダイナミックルートのために`+page.js`（または`+page.server.js`）でデータを準備できます。`src/routes/blog/[slug]/+page.js`を作成：

```js
export function load({ params }) {
  return {
    slug: params.slug
  };
}
```

この`load`関数は、URLの`[slug]`部分を`data.slug`としてページに渡します。これにより、ページ側で`slug`を使って記事を検索できます。

### 3. React/Next.jsとの比較
Next.jsで同様のダイナミックルートを作る場合、`getStaticProps`や`getStaticPaths`が必要です：

```jsx
// Next.js
export async function getStaticProps({ params }) {
  const post = posts.find(p => p.slug === params.slug);
  return { props: { post } };
}

export async function getStaticPaths() {
  return {
    paths: posts.map(post => ({ params: { slug: post.slug } })),
    fallback: false
  };
}
```

Next.jsでは、静的生成（SSG）のために追加の設定が必要ですが、SvelteKitの`load`関数はシンプルで、**動的データも静的データも統一的に扱える**点が優れています。

---

## 動作確認
プロジェクトを起動します：

```bash
npm run dev
```

ブラウザで以下を確認：
- `http://localhost:5173/blog`：記事一覧ページ。カード形式で2つの記事が表示され、「続きを読む」ボタンが詳細ページにリンク。
- `http://localhost:5173/blog/first-post`：記事詳細ページ。タイトル、日付、本文が表示。

Skeleton UIのおかげで、UIはモダンでレスポンシブです。Svelteのリアクティブ変数（例：`let post`）を使うことで、状態管理がシンプルになり、Reactの`useState`や`useEffect`を意識せずに済みます。

---

## やってみよう！（チャレンジ）
記事一覧ページに簡単なフィルター機能を追加してみましょう！例えば、以下のようにタグをモックデータに追加：

```js
// src/lib/data/posts.js
{
  slug: 'first-post',
  title: '初めてのブログ記事',
  excerpt: 'これはSvelteKitで作った最初の記事です。',
  date: '2025-04-10',
  content: '...',
  tags: ['Svelte', 'Tutorial']
}
```

そして、`<select>`要素でタグを選択して記事をフィルタリングする機能を試してみてください。Svelteのリアクティブ変数を使うと、以下のように簡単に実装できます：

```svelte
<script>
  let selectedTag = '';
  $: filteredPosts = selectedTag ? posts.filter(p => p.tags.includes(selectedTag)) : posts;
</script>

<select bind:value={selectedTag}>
  <option value="">すべてのタグ</option>
  <option value="Svelte">Svelte</option>
  <option value="Tutorial">Tutorial</option>
</select>
```

このチャレンジで、Svelteの`$:`（リアクティブステートメント）の力を体感してください！

---

## まとめ
この記事では、SvelteKitのファイルベースのルーティングを活用し、ブログの**記事一覧ページ**と**記事詳細ページ**を構築しました。**Skeleton UI**で美しいUIを作り、Svelteのシンプルな構文でデータを表示しました。ReactやNext.jsと比べ、記述量が少なく、直感的な開発体験がSvelteKitの強みだと感じていただけたと思います。

次回は、**Svelte-Query**を使って外部APIから動的な記事データを取得し、ブログをさらにリアルなアプリケーションに進化させます。お楽しみに！

この記事が役に立ったと思ったら、**LGTM**や**ストック**していただけると励みになります！質問やアイデアがあれば、ぜひコメントで教えてください。次の記事でまたお会いしましょう！