こんにちは!「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)}
で一意なキーを指定し、トランジションを正しく動作させる。
- Svelteの
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にデプロイ
以下の手順でデプロイ:
- プロジェクトをGitHubリポジトリにプッシュ。
- Vercelダッシュボードで新しいプロジェクトを作成。
- GitHubリポジトリを接続し、デプロイ設定を以下のように:
- Framework Preset:
SvelteKit
- Root Directory: (デフォルト)
- Build Command:
npm run build
- Output Directory:
.svelte-kit
- Framework Preset:
- 環境変数(例:
NODE_ENV=production
)を必要に応じて設定。 - 「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:slide
やtransition: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やストックしていただけると励みになります!質問や改善アイデアがあれば、コメントで教えてください。次の記事でまたお会いしましょう!