Next.jsでブログを作る #5 SEOとデプロイ (まとめ)

前回はコメント機能まで完成させました。私たちのブログはこれで、記事を書く/見る/分類する/検索する/コメントまで、すべての中核機能を備えました。最後の記事では検索エンジン最適化sitemap/RSSVercelデプロイまで仕上げて、このシリーズとReactコンテンツ全体を振り返って整理します。

Metadata API — 検索エンジンのための情報 #

各ページに適切な<title><meta description>、OpenGraphなどがあってこそ、検索エンジンとSNSが私たちの記事をうまく表示してくれます。Next.jsのMetadata APIがこの作業を優雅に処理します。

静的なmetadata #

src/app/layout.jsのmetadataはサイト全体のデフォルト値です。

src/app/layout.js
export const metadata = {
  metadataBase: new URL('https://your-blog.example.com'),
  title: {
    default: '私のブログ',
    template: '%s | 私のブログ',
  },
  description: 'Reactとウェブ開発に関する記事',
  openGraph: {
    type: 'website',
    locale: 'ko_KR',
    url: 'https://your-blog.example.com',
    siteName: '私のブログ',
  },
};

title.templateは子ページでtitleを定めるときに自動で適用される形式です。子が'こんにちは'をtitleにすると、最終的なtitleはこんにちは | 私のブログになります。

metadataBaseはOG画像のような絶対URLを作るときの基準となるドメイン。デプロイ先のドメインに変えてください。

動的metadata — 記事詳細ページ #

各記事ページのmetadataは、記事のfrontmatterをもとに動的に生成します。generateMetadata関数をexportすればOKです。

src/app/posts/[slug]/page.js (追加)
export async function generateMetadata({ params }) {
  const { slug } = await params;
  const post = getPostBySlug(slug);

  if (!post) return {};

  return {
    title: post.frontmatter.title,
    description: post.frontmatter.description,
    openGraph: {
      title: post.frontmatter.title,
      description: post.frontmatter.description,
      type: 'article',
      publishedTime: post.frontmatter.date,
      tags: post.frontmatter.tags,
    },
  };
}

これで各記事ページが自分だけのtitle/descriptionを持つようになり、それがそのまま検索結果やSNSプレビューに反映されます。

タグページも #

src/app/tags/[tag]/page.js (追加)
export async function generateMetadata({ params }) {
  const { tag } = await params;
  const decodedTag = decodeURIComponent(tag);
  return {
    title: `#${decodedTag}`,
    description: `${decodedTag} タグが付いた記事`,
  };
}

一覧ページはlayoutのデフォルトmetadataがそのまま使われるので、別途設定なしでもOKです。

Sitemap — 検索エンジンにページを知らせる #

検索エンジンのクローラーがサイトのすべてのページを漏れなくたどれるように、sitemap.xmlを提供します。App Routerではsitemap.jsファイルで自動生成できます。

src/app/sitemap.js

src/app/sitemap.js
import { getAllPosts, getAllTags } from './lib/posts';

const SITE_URL = 'https://your-blog.example.com';

export default function sitemap() {
  const posts = getAllPosts();
  const tags = getAllTags();

  const staticPages = ['', '/tags', '/search'].map(path => ({
    url: `${SITE_URL}${path}`,
    lastModified: new Date(),
  }));

  const postPages = posts.map(post => ({
    url: `${SITE_URL}/posts/${post.slug}`,
    lastModified: new Date(post.frontmatter.date),
  }));

  const tagPages = tags.map(({ tag }) => ({
    url: `${SITE_URL}/tags/${encodeURIComponent(tag)}`,
    lastModified: new Date(),
  }));

  return [...staticPages, ...postPages, ...tagPages];
}

このファイルがあれば/sitemap.xmlが自動で動作します。Next.jsが関数の戻り値をXMLに変換してくれるからです。ビルドして結果を確認すると、標準のsitemapフォーマットで出力されます。

Robots.txt #

検索エンジンのクローラーに、どのページをクロールしてよいかを伝えるファイルです。

src/app/robots.js

src/app/robots.js
const SITE_URL = 'https://your-blog.example.com';

export default function robots() {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
    },
    sitemap: `${SITE_URL}/sitemap.xml`,
  };
}

/robots.txtとして自動的に公開されます。すべてのクローラーがすべてのページを自由に収集できるよう許可しつつ、sitemapの位置を知らせる標準的な設定です。

RSS Feed #

ブログを購読するユーザーのためのRSS feedも作ってみましょう。RSSリーダーで私たちの記事を受け取れるようにする機能です。

App RouterにはRSS用の自動ファイル規約がないので、route handlerで直接作ります。

src/app/feed.xml/route.js

src/app/feed.xml/route.js
import { getAllPosts } from '../lib/posts';

const SITE_URL = 'https://your-blog.example.com';
const SITE_TITLE = '私のブログ';
const SITE_DESCRIPTION = 'Reactとウェブ開発に関する記事';

function escapeXml(text) {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;');
}

export async function GET() {
  const posts = getAllPosts().slice(0, 50);
  const buildDate = new Date().toUTCString();

  const items = posts.map(post => `
    <item>
      <title>${escapeXml(post.frontmatter.title)}</title>
      <link>${SITE_URL}/posts/${post.slug}</link>
      <guid isPermaLink="true">${SITE_URL}/posts/${post.slug}</guid>
      <pubDate>${new Date(post.frontmatter.date).toUTCString()}</pubDate>
      <description>${escapeXml(post.frontmatter.description ?? '')}</description>
    </item>
  `).join('\n');

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>${escapeXml(SITE_TITLE)}</title>
    <link>${SITE_URL}</link>
    <description>${escapeXml(SITE_DESCRIPTION)}</description>
    <language>ko</language>
    <lastBuildDate>${buildDate}</lastBuildDate>
    <atom:link href="${SITE_URL}/feed.xml" rel="self" type="application/rss+xml" />
    ${items}
  </channel>
</rss>
`;

  return new Response(xml, {
    headers: {
      'Content-Type': 'application/rss+xml; charset=utf-8',
    },
  });
}

/feed.xmlにアクセスすると、RSS形式のXMLが返されます。

route.jsファイルはページではなくAPIエンドポイントを定義する特別なファイルです。私たちのサイトのapp/feed.xml/route.tsもほぼ同じパターンでRSSを提供しています。

レイアウトの<head>にRSS自動発見リンクも追加すると、RSSリーダーが勝手に見つけてくれます。

src/app/layout.js (追加)
export const metadata = {
  // ...既存の設定...
  alternates: {
    types: {
      'application/rss+xml': '/feed.xml',
    },
  },
};

ビルドして確認する #

それでは一度productionビルドを回してみましょう。

ビルド
npm run build

ビルドログに、各ページがどう生成されたか(静的/動的)、ページサイズ、どんなルートが作られたかが出力されます。/sitemap.xml/robots.txt/feed.xmlがルートに含まれているのが見えるはずです。

ビルド結果をローカルで動かしてみるには次のとおりです。

production モード実行
npm start

http://localhost:3000がproductionモードで動作します。devモードよりずっと速く軽く感じられるはずです — すべての静的ページが事前生成され、JavaScriptが最小化されているからです。

Vercelデプロイ #

これで本当にインターネットに公開する番です。VercelがNext.jsの公式ホスティングプラットフォームなので、デプロイが最もスムーズです。

1. GitHubにコードをpush #

git セットアップ
cd my-blog
git init
git add .
git commit -m "Initial blog"

GitHubに空のリポジトリを作ってpushします。

リモート接続 + push
git remote add origin https://github.com/<あなた>/my-blog.git
git branch -M main
git push -u origin main

2. Vercel登録 + リポジトリ連携 #

https://vercel.comにGitHubアカウントで登録し、「New Project」 → 先ほどpushしたリポジトリを選べば終わりです。

VercelはNext.jsプロジェクトを自動で検出し、適切なビルド設定を適用します。追加設定なしで初回のデプロイが進みます。

デプロイが終わるとyour-blog.vercel.appの形のドメインができ、そこで私たちのブログが動作します。インスタンスが生きている間だけコメントが維持されるという点はメモリストアの限界そのままですが、記事の表示は完璧に動作します。

3. ドメイン接続 (オプション) #

自分のドメインがあるなら、Vercelプロジェクト設定の「Domains」タブで接続できます。DNSの案内が段階的に出るので従えばOKです。

4. 記事追加のワークフロー #

これで新しい記事を書く流れは次のようになります。

  1. posts/new-article.mdxファイルを作成
  2. git commit -m "feat(content): publish ..."
  3. git push
  4. Vercelが自動検出 → 自動ビルド → 自動デプロイ

git push一発で新しい記事がインターネットに上がるわけです。これがファイルベースブログの大きな魅力の1つです。

環境別設定 — 開発 vs プロダクション #

ここまでサイトURLをyour-blog.example.comにハードコーディングしていましたが、環境に応じて動的に設定するのが望ましいです。

.env.local

.env.local
NEXT_PUBLIC_SITE_URL=http://localhost:3000

Vercelプロジェクト設定のEnvironment Variablesには次のように設定します。

Vercel 環境変数
NEXT_PUBLIC_SITE_URL=https://your-actual-domain.com

コードでの使用。

const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000';

このように分離しておけば、dev/preview/production環境ごとに適切なURLが適用されます。

シリーズの振り返り — Next.jsでブログを作る #

このシリーズで私たちは、空のプロジェクトから始めて実運用可能なレベルのブログまで作りました。

#追加された機能登場した中核パターン/ツール
1データモデル、セットアップMDX セットアップ、frontmatter、フォルダ構造
2記事一覧 + 詳細Server Component が fs で直接読む、compileMDXgenerateStaticParams
3タグ + 検索動的フォルダ vs searchParamsdecodeURIComponent
4コメントServer Actions、useActionStateuseFormStatusbindrevalidatePath
5SEO + デプロイMetadata API、sitemap、robots、RSS、Vercel

同じ5編なのに、Todoシリーズとは違うトーンでしたね。Todoはクライアントサイドのパターン中心でしたが、ブログはサーバーサイド(RSC + Actions)が中心でした。両方のビルドを追いかけてこられた方は、クライアントとサーバー両方の実戦パターンが手に取れるようになっているはずです。

Reactコンテンツ31編全体の振り返り #

このシリーズを終えるにあたり、Reactカテゴリ全体の像を整理してみます。

A. Reactの基礎講座 (#1〜#15) — クライアントサイドのファンダメンタルズ #

JSX、コンポーネント、props、state、useEffect、カスタムフック、パフォーマンス最適化、ルーティングまで。これがこの上に積み重なるすべてのもののベースです。

B. ReactでTodoアプリを作る (#1〜#5) — 最初の実戦ビルド #

基礎を段階的に組み合わせて、小さなインタラクティブなアプリを作る経験。追加 / トグル / フィルター / 編集 / 永続化までの自然な進化の流れ。

C. モダンReact + Next.js (#1〜#6) — サーバー/クライアントのパラダイム #

Server Components、Server Actions、Suspense、streamingまで。「このコードはどこで実行されるのか?」というメンタルモデルの転換。

D. Next.jsでブログを作る (#1〜#5) — 2回目の実戦ビルド (今回のシリーズ) #

A〜Cで学んだすべてを合わせたフルスタックブログ。実運用可能なレベルの成果物 + 実際のデプロイまで。

合計31編。これをすべて追ってこられた方は、Reactエコシステムのほぼすべての中核の流れを一度ずつ経験したことになります。

次に行けるところ #

これで、自分のプロジェクトを始めるベースが十分に整いました。次のステップとして進める方向は次のとおりです。

すぐに挑戦できるもの #

  • このブログを自分のものに — 記事を書いて運営してみる。最も速い学習は本当に使ってみること
  • TypeScriptへのマイグレーション.js.tsx。大きなコードベースの安全性 ↑
  • テスティングの導入 — Vitest + React Testing Libraryでコア動作のユニットテスト

もう少し大きな領域 #

  • DB連携 — PrismaまたはDrizzle、SupabaseまたはVercel Postgres
  • 認証 — NextAuth.js / Clerk / Lucia
  • 状態管理ライブラリ — 大きなアプリではZustand、Jotai、Redux Toolkit
  • データフェッチライブラリ — クライアントサイドのフェッチが必要なときはTanStack Query

また別のビルド #

  • ECサイト — カート、決済、在庫、注文 — 最も総合的な挑戦
  • ソーシャルアプリ — 掲示板、DM、通知 — リアルタイム性とデータ整合性
  • ダッシュボード/管理画面 — チャート、テーブル、フィルター — データ可視化

おわりに #

ここまで付いてきてくださって本当にありがとうございます。このサイトの既存コンテンツの上に、31編のReactの記事が加わりました。小さなコンポーネント1つから始めて、クラスコンポーネントは使わずに (現代のReactは関数コンポーネント + フックが標準)、クライアントサイドのすべての基礎 → モダンRSCパラダイム → 2種類の実戦アプリまで扱いました。

覚えておいていただきたいことが1つあるなら — 「自分が作りたい小さなものを、自分で作ってみてください」ということです。このブログを持っていって自分のものとして運営しても良いし、日常のツールをReactで作ってみても良いです。何を作るにせよ、行き詰まるところで本物の学習が起こります。

Reactは速く進化するライブラリですが、このシリーズで扱ったファンダメンタルズ(コンポーネント単位の思考、単方向のデータフロー、宣言的なUI、「コードがどこで実行されるか」)は変わりません。その上に新しいツールが積み重なり続けるだけです。本質を知っていれば、どんな新しいツールが出てきても速く身につけられます。

以上でシリーズを終了します。

X