0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SvelteKitで作るフルスタック個人ブログ | 第6回: SEO最適化とVercelでのデプロイ

Posted at

こんにちは!「SvelteKitで作るフルスタック個人ブログ」シリーズの第6回へようこそ。第5回では、Luciaを使って認証機能を追加し、ログインしたユーザーがダッシュボードから記事を作成できるようにしました。今回は、ブログを本番環境に公開するため、SEO最適化を行い、Vercelを使ってデプロイします。さらに、Skeleton UIのアニメーションやパフォーマンス最適化を追加し、ユーザー体験を向上させます。Next.jsのデプロイとの比較も交えながら、SvelteKitのシンプルなデプロイ体験を体感しましょう!


この記事の目標

  • ブログのSEO最適化(メタデータ、サイトマップ、静的生成)を行う。
  • Skeleton UIのトランジションを使ってUIを滑らかにする。
  • パフォーマンスを最適化(画像の遅延読み込みなど)。
  • Vercel Adapterを使ってSvelteKitプロジェクトをVercelにデプロイする。
  • Next.jsのデプロイやSEOとの違いを比較し、SvelteKitの開発体験(DX)の良さを確認する。

最終的には、SEOに優れ、パフォーマンスの高いブログを本番環境で公開し、誰でもアクセスできる状態にします。では、始めましょう!


SEO最適化

SEO(検索エンジン最適化)は、ブログが検索エンジンで上位に表示されるために重要です。SvelteKitは、メタデータや**静的生成(SSG)**を簡単に設定できる機能を提供します。

1. メタデータの設定

各ページにSEO用のメタデータを追加します。src/routes/+layout.svelteを更新して、ベースメタデータを設定:

<script>
  import { AppBar, AppShell } from '@skeletonlabs/skeleton';
  export let data;
</script>

<svelte:head>
  <title>My Blog</title>
  <meta name="description" content="SvelteKitで構築された個人ブログ。技術記事やチュートリアルを公開しています。" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta charset="UTF-8" />
</svelte:head>

<AppShell>
  <svelte:fragment slot="header">
    <AppBar background="bg-primary-500">
      <svelte:fragment slot="lead">
        <h1 class="text-2xl font-bold text-white">My Blog</h1>
      </svelte:fragment>
      <svelte:fragment slot="trail">
        <a href="/" class="btn variant-ghost-surface">Home</a>
        <a href="/blog" class="btn variant-ghost-surface">Blog</a>
        <a href="/about" class="btn variant-ghost-surface">About</a>
        {#if data.session}
          <a href="/dashboard" class="btn variant-ghost-surface">ダッシュボード</a>
          <form method="POST" action="/logout" class="inline">
            <button type="submit" class="btn variant-ghost-error">ログアウト</button>
          </form>
        {:else}
          <a href="/login" class="btn variant-ghost-surface">ログイン</a>
        {/if}
      </svelte:fragment>
    </AppBar>
  </svelte:fragment>
  <slot />
  <svelte:fragment slot="footer">
    <footer class="bg-surface-900 text-white p-4 text-center">
      <p>© 2025 My Blog. All rights reserved.</p>
    </footer>
  </svelte:fragment>
</AppShell>
  • 解説
    • <svelte:head>でベースの<title><meta>タグを設定。
    • ナビゲーションバーにログイン状態に応じたリンク(ダッシュボード、ログアウト)を追加。

ブログ一覧ページ(src/routes/blog/+page.svelte)に動的なメタデータを設定:

<script>
  import { useQuery } from '@sveltestack/svelte-query';
  import { Card, CardHeader, CardContent, CardFooter, Spinner } from '@skeletonlabs/skeleton';

  const query = useQuery('posts', async () => {
    const response = await fetch('/api/posts');
    if (!response.ok) throw new Error('データの取得に失敗しました');
    return response.json();
  });
</script>

<svelte:head>
  <title>ブログ記事一覧 | My Blog</title>
  <meta name="description" content="SvelteKitで構築されたブログの記事一覧。最新の技術記事をチェック!" />
</svelte:head>

<div class="container mx-auto p-8">
  <h1 class="h1 mb-8">ブログ記事一覧</h1>
  {#if $query.isLoading}
    <div class="flex justify-center">
      <Spinner />
    </div>
  {:else if $query.isError}
    <p class="text-error-500">エラー: {$query.error.message}</p>
  {:else}
    <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
      {#each $query.data as post}
        <Card>
          <CardHeader>
            <h2 class="h3">{post.title}</h2>
            <p class="text-sm text-surface-500">{post.createdAt}</p>
          </CardHeader>
          <CardContent>
            <p>{post.excerpt}</p>
          </CardContent>
          <CardFooter>
            <a href="/blog/{post.slug}" class="btn variant-filled-primary">続きを読む</a>
          </CardFooter>
        </Card>
      {/each}
    </div>
  {/if}
</div>

記事詳細ページ(src/routes/blog/[slug]/+page.svelte)も同様に:

<script>
  import { useQuery } from '@sveltestack/svelte-query';
  import { Spinner } from '@skeletonlabs/skeleton';
  import { error } from '@sveltejs/kit';

  export let data;

  const query = useQuery(['post', data.slug], async () => {
    const response = await fetch(`/api/posts/${data.slug}`);
    if (!response.ok) throw error(404, '記事が見つかりません');
    return response.json();
  });
</script>

<svelte:head>
  {#if !$query.isLoading && !$query.isError}
    <title>{$query.data.title} | My Blog</title>
    <meta name="description" content={$query.data.excerpt} />
  {/if}
</svelte:head>

<div class="container mx-auto p-8">
  {#if $query.isLoading}
    <div class="flex justify-center">
      <Spinner />
    </div>
  {:else if $query.isError}
    <p class="text-error-500">エラー: {$query.error.message}</p>
  {:else}
    <article class="card p-6 bg-surface-50">
      <header class="mb-4">
        <h1 class="h2">{$query.data.title}</h1>
        <p class="text-sm text-surface-500">{$query.data.createdAt}</p>
      </header>
      <section class="prose">
        <p>{$query.data.content}</p>
      </section>
      <footer class="mt-6">
        <a href="/blog" class="btn variant-ghost-primary">一覧に戻る</a>
      </footer>
    </article>
  {/if}
</div>
  • 解説
    • 詳細ページでは、データがロードされた後に動的に<title><meta>を設定。
    • $query.data.excerptをメタ記述に使用し、SEO効果を高める。

2. サイトマップの生成

サイトマップは、検索エンジンがブログの構造を理解するのに役立ちます。src/routes/sitemap.xml/+server.tsを作成:

import { db } from '$lib/db';
import { posts } from '$lib/db/schema';

export async function GET() {
  const allPosts = await db.select().from(posts).all();
  const baseUrl = 'https://your-blog.vercel.app';

  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>${baseUrl}</loc>
    <lastmod>${new Date().toISOString()}</lastmod>
    <changefreq>daily</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>${baseUrl}/blog</loc>
    <lastmod>${new Date().toISOString()}</lastmod>
    <changefreq>daily</changefreq>
    <priority>0.8</priority>
  </url>
  ${allPosts
    .map(
      (post) => `
  <url>
    <loc>${baseUrl}/blog/${post.slug}</loc>
    <lastmod>${post.createdAt}</lastmod>
    <changefreq>weekly</changefreq>
    <priority>0.6</priority>
  </url>`
    )
    .join('')}
</urlset>`;

  return new Response(sitemap, {
    headers: {
      'Content-Type': 'application/xml'
    }
  });
}
  • 解説
    • データベースから全記事を取得し、動的にサイトマップを生成。
    • ホームページ、ブログ一覧、個別記事をリストアップ。

パフォーマンス最適化

1. Skeleton UIのトランジション

記事一覧ページにスムーズなアニメーションを追加します。src/routes/blog/+page.svelteを更新:

<script>
  import { useQuery } from '@sveltestack/svelte-query';
  import { Card, CardHeader, CardContent, CardFooter, Spinner } from '@skeletonlabs/skeleton';
  import { fade } from 'svelte/transition';

  const query = useQuery('posts', async () => {
    const response = await fetch('/api/posts');
    if (!response.ok) throw new Error('データの取得に失敗しました');
    return response.json();
  });
</script>

<svelte:head>
  <title>ブログ記事一覧 | My Blog</title>
  <meta name="description" content="SvelteKitで構築されたブログの記事一覧。最新の技術記事をチェック!" />
</svelte:head>

<div class="container mx-auto p-8">
  <h1 class="h1 mb-8">ブログ記事一覧</h1>
  {#if $query.isLoading}
    <div class="flex justify-center">
      <Spinner />
    </div>
  {:else if $query.isError}
    <p class="text-error-500">エラー: {$query.error.message}</p>
  {:else}
    <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
      {#each $query.data as post (post.slug)}
        <div transition:fade={{ duration: 300 }}>
          <Card>
            <CardHeader>
              <h2 class="h3">{post.title}</h2>
              <p class="text-sm text-surface-500">{post.createdAt}</p>
            </CardHeader>
            <CardContent>
              <p>{post.excerpt}</p>
            </CardContent>
            <CardFooter>
              <a href="/blog/{post.slug}" class="btn variant-filled-primary">続きを読む</a>
            </CardFooter>
          </Card>
        </div>
      {/each}
    </div>
  {/if}
</div>
  • 解説
    • Svelteのtransition:fadeをカードに適用し、記事が表示される際に滑らかなフェードイン効果を追加。
    • {#each ... (post.slug)}で一意なキーを指定し、トランジションを正しく動作させる。

2. 画像の遅延読み込み

ブログに画像を追加する場合、遅延読み込み(lazy loading)を設定します。記事詳細ページで仮に画像を表示する例:

<img src="https://via.placeholder.com/800x400" alt={$query.data.title} loading="lazy" class="w-full mb-4" />
  • loading="lazy"はブラウザのネイティブ機能で、画像がビューポートに入るまで読み込みを遅らせます。

3. Next.jsとの比較

Next.jsでは、SEOとパフォーマンス最適化のために以下のような機能を使います:

  • <Head>コンポーネントでメタデータ設定。
  • next/imageで画像最適化。
  • getStaticPropsで静的生成。

Next.jsのnext/imageは強力ですが、設定が複雑で、外部画像には制限があります。一方、SvelteKitは<svelte:head>でメタデータがシンプルに設定でき、静的生成もexport const prerender = trueで簡単に有効化できます。Svelteのトランジションは、Reactのアニメーションライブラリ(例:Framer Motion)に比べ、軽量で簡単に適用可能です。


Vercelでのデプロイ

1. Vercel Adapterのインストール

SvelteKitをVercelに最適化するため、Vercel Adapterをインストール:

npm install @sveltejs/adapter-vercel

svelte.config.jsを更新:

import adapter from '@sveltejs/adapter-vercel';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter()
  }
};

export default config;

2. 静的生成の有効化

ブログページを静的生成(SSG)にするため、src/routes/blog/+page.tsを作成:

export const prerender = true;

記事詳細ページも同様に、src/routes/blog/[slug]/+page.ts

import { db } from '$lib/db';
import { posts } from '$lib/db/schema';

export const prerender = true;

export async function entries() {
  const allPosts = await db.select().from(posts).all();
  return allPosts.map((post) => ({ slug: post.slug }));
}

export function load({ params }) {
  return {
    slug: params.slug
  };
}
  • 解説
    • prerender = trueで静的生成を有効化。
    • entries関数で全記事のslugを返し、ビルド時に各ページを生成。

3. Vercelにデプロイ

以下の手順でデプロイ:

  1. プロジェクトをGitHubリポジトリにプッシュ。
  2. Vercelダッシュボードで新しいプロジェクトを作成。
  3. GitHubリポジトリを接続し、デプロイ設定を以下のように:
    • Framework Preset: SvelteKit
    • Root Directory: (デフォルト)
    • Build Command: npm run build
    • Output Directory: .svelte-kit
  4. 環境変数(例:NODE_ENV=production)を必要に応じて設定。
  5. 「Deploy」ボタンをクリック。

デプロイ後、Vercelが提供するURL(例:https://your-blog.vercel.app)でブログが公開されます。

4. Vercel Analyticsの有効化

Vercelダッシュボードで「Analytics」を有効化し、ブログのトラフィックやパフォーマンスを監視します。コードの変更は不要で、すぐにデータが確認できます。


動作確認

デプロイ後、以下のURLを確認:

  • https://your-blog.vercel.app/:ホームページ。
  • https://your-blog.vercel.app/blog:静的生成された記事一覧。
  • https://your-blog.vercel.app/blog/your-post-slug:個別記事。
  • https://{your-blog.vercel.app}/sitemap.xml:サイトマップ。

Skeleton UIのトランジションにより、記事一覧が滑らかに表示され、SEOメタデータが検索エンジンに正しく認識されます。Vercelの自動スケーリングにより、高トラフィックでも安定して動作します。


やってみよう!(チャレンジ)

ブログにカスタムドメインを追加してみましょう!Vercelダッシュボードの「Domains」セクションで、購入済みのドメイン(例:myblog.com)を追加し、DNS設定を行います。また、Svelteのtransition:slidetransition:scaleを他のページ(例:ダッシュボードやログインフォーム)に適用して、UIをさらに滑らかにしてみてください。以下の例を参考に:

<div transition:slide={{ duration: 300 }}>
  <Card>...</Card>
</div>

Svelteのトランジションは軽量で、簡単にカスタマイズできるので、さまざまな効果を試してみましょう!


まとめ

この記事では、SEO最適化(メタデータ、サイトマップ、静的生成)を行い、Skeleton UIのトランジションでUIを強化しました。さらに、Vercel Adapterを使ってブログをVercelにデプロイし、本番環境で公開しました。Next.jsと比べ、SvelteKitのSEO設定はシンプルで、Vercelとの統合もスムーズです。Svelteの軽量なトランジションは、Reactのアニメーションライブラリより簡単に適用でき、DXの良さを実感できたと思います。

次回は、リアルタイム機能(例:コメント)やテストの追加など、ブログをさらに進化させる方法を解説します。お楽しみに!

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

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?