デザインについての学習メモブログ

Next.js 入門 #7:スタイリングとMetadata — Tailwind CSS と SEO 対策を同時に

記事内に広告が含まれています。

Next.js 入門 #7:スタイリングとMetadata — Tailwind CSS と SEO 対策を同時に

ここまでの記事でルーティング、データ取得、フォーム処理と、Next.jsの核となる機能を一通り学んできました。

ただ、正直なところ見た目は素っ気ないままです。

今回はサイトを実用レベルに仕上げる2つのテーマを扱います。

  • Tailwind CSS — クラス名を書くだけでスタイルが当たる
  • Metadata API — タイトルやOGP画像をNext.jsの仕組みで管理する

どちらも「設定が大変そう」と思われがちですが、create-next-app で作ったプロジェクトなら最初からほぼ準備が整っています。

Tailwind CSSとは

Tailwind CSSユーティリティファーストなCSSフレームワークです。

従来のCSSの書き方はこうでした。

CSS
/* style.css */
.button {
  background-color: #3b82f6;
  color: white;
  padding: 0.5rem 1rem;
  border-radius: 0.25rem;
}
HTML
<button class="button">送信</button>

Tailwindでは

クラス名を直接HTMLに書きます。

HTML
<button class="bg-blue-500 text-white px-4 py-2 rounded">送信</button>

CSSファイルを別途書く必要がなく、クラス名を見ればどんなスタイルかわかります。

最初は「クラスが多くて読みにくい」と感じるかもしれませんが、慣れると逆に「CSSファイルを行き来しなくていい」快適さに気づきます。

Tailwindのセットアップ確認

create-next-app でTailwindをYesにした場合、すでに使える状態です。

念のため確認しましょう。

app/globals.css を開くと、先頭にこの3行があるはずです。

CSS
@import "tailwindcss";

これがあればOKです。

Tailwind v4から @import "tailwindcss" の1行に統合されました(v3では @tailwind base; @tailwind components; @tailwind utilities; の3行でしたが、現在は不要です)。

また、v3では tailwind.config.ts という設定ファイルが生成されていましたが、v4からは globals.css 内の @theme ブロックで設定を管理する方式に変わっています。

カスタムカラーなどを追加したい場合はここに書きます。

CSS
/* app/globals.css */
@import "tailwindcss";

@theme inline {
  /* カスタムカラーの例 */
  --color-brand: #0070f3;
}

layout.tsx に以下のimport行があれば、どのページでもTailwindのクラスが使えます。

#3layout.tsx を書き換えた場合も、この行が残っているか確認しておきましょう。

CSS
// app/layout.tsx
import './globals.css'; // ← この行があればOK

Tailwindの基本クラスを覚える

よく使うクラスをカテゴリ別にまとめます。

すべて覚える必要はありません。

使いながら少しずつ身につけていきましょう。

レイアウト

クラス意味
flexdisplay: flex
flex-col縦方向に並べる
items-center交差軸方向に中央揃え
justify-between両端揃え
gap-4要素間の隙間(1rem)
grid grid-cols-33列のグリッド

余白

クラス意味
p-4padding: 1rem(全方向)
px-4padding: 左右1rem
py-2padding: 上下0.5rem
mt-4margin-top: 1rem
mx-auto左右のmarginをautoに(中央揃え)

テキスト

クラス意味
text-sm小さいフォントサイズ
text-xl大きいフォントサイズ
font-bold太字
text-gray-600グレーのテキスト
leading-relaxed行間を広めに

色・背景

クラス意味
bg-white白背景
bg-blue-500青背景
text-white白テキスト
border境界線
border-gray-200グレーの境界線
rounded角丸(小)
rounded-lg角丸(大)
shadow-mdドロップシャドウ

数値の単位: Tailwindの数値は 4 = 1rem = 16px が基準です。

p-20.5remp-82rem になります。

実践:記事一覧ページをTailwindで整える

これまで style={{ ... }} でインラインスタイルを書いていたコードを、Tailwindで書き直してみましょう。

TSX
// app/posts/page.tsx

import Link from 'next/link';

type Post = {
  id: number;
  title: string;
  body: string;
};

export default async function PostsPage() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
    next: { revalidate: 60 },
  });
  const posts: Post[] = await res.json();
  const recentPosts = posts.slice(0, 10);

  return (
    <main className="max-w-2xl mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">投稿一覧</h1>
      <ul className="flex flex-col gap-4">
        {recentPosts.map((post) => (
          <li key={post.id} className="border border-gray-200 rounded-lg p-5 hover:shadow-md transition-shadow">
            <Link href={`/posts/${post.id}`} className="block">
              <h2 className="text-lg font-semibold text-blue-600 hover:underline mb-1">
                {post.title}
              </h2>
              <p className="text-sm text-gray-500 leading-relaxed">
                {post.body.slice(0, 80)}...
              </p>
            </Link>
          </li>
        ))}
      </ul>
    </main>
  );
}

インラインスタイルが消えて、HTML(TSX)とスタイルが一体化しました.

hover:shadow-mdtransition-shadow のようにホバー時の動きもクラスだけで書けます。

Headerもきれいにする

TSX
// components/Header.tsx

import Link from 'next/link';
import MobileMenu from './MobileMenu';

export default function Header() {
  return (
    <header className="border-b border-gray-200 bg-white">
      <div className="max-w-2xl mx-auto px-4 py-4 flex items-center justify-between">
        <Link href="/" className="text-xl font-bold text-gray-900">
          My Blog
        </Link>
        <nav className="hidden md:flex items-center gap-6">
          <Link href="/posts" className="text-sm text-gray-600 hover:text-gray-900 transition-colors">
            ブログ
          </Link>
          <Link href="/about" className="text-sm text-gray-600 hover:text-gray-900 transition-colors">
            About
          </Link>
        </nav>
        <div className="md:hidden">
          <MobileMenu />
        </div>
      </div>
    </header>
  );
}

hidden md:flex はモバイルでは非表示、md(768px以上)では flex で表示するという意味です。

Tailwindはこのように レスポンシブ対応もクラス名で完結します。

レスポンシブのプレフィックス

プレフィックスブレークポイント
(なし)すべての画面サイズ
sm:640px以上
md:768px以上
lg:1024px以上
xl:1280px以上

「モバイルファーストで書いて、大きい画面に向けて上書き」するのがTailwindの流儀です。

Metadata API — SEO対策をNext.jsの仕組みで

検索エンジンやSNSでシェアされたときに正しい情報を表示するには、HTMLの <head> 内にメタ情報を設定する必要があります。

Next.jsではこれを Metadata API で管理します。

静的なMetadata

page.tsxlayout.tsx から metadata オブジェクトを export するだけです。

TypeScript
// app/posts/page.tsx

import { Metadata } from 'next';

export const metadata: Metadata = {
  title: '投稿一覧 | My Blog',
  description: 'Next.jsで作ったブログの投稿一覧ページです。',
};

export default async function PostsPage() {
  // ...
}

Next.jsが自動的にHTMLの <head> に以下を生成します。

HTML
<title>投稿一覧 | My Blog</title>
<meta name="description" content="Next.jsで作ったブログの投稿一覧ページです。" />

タイトルのテンプレートを設定する

layout.tsx でタイトルのテンプレートを設定すると、各ページのタイトルに自動でサイト名が付きます。

TypeScript
// app/layout.tsx

import { Metadata } from 'next';

export const metadata: Metadata = {
  title: {
    default: 'My Blog',              // タイトルが未設定のページに使われる
    template: '%s | My Blog',        // %s に各ページのタイトルが入る
  },
  description: 'Next.jsで作ったブログです。',
};

各ページでは短いタイトルだけ書けばOKです。

TypeScript
// app/posts/page.tsx

export const metadata: Metadata = {
  title: '投稿一覧',  // → 「投稿一覧 | My Blog」になる
};

動的なMetadata — 記事ページのタイトルをAPIから取得する

動的ルート([id])のページでは、取得したデータをもとにメタ情報を生成できます。

generateMetadata という特別な関数を使います。

TSX
// app/posts/[id]/page.tsx

import { Metadata } from 'next';
import { notFound } from 'next/navigation';

type Post = { id: number; title: string; body: string };
type Props = { params: Promise<{ id: string }> };

// 動的にMetadataを生成する
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id } = await params;
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);

  if (!res.ok) return { title: '記事が見つかりません' };

  const post: Post = await res.json();

  return {
    title: post.title,
    description: post.body.slice(0, 100),
  };
}

export default async function PostPage({ params }: Props) {
  const { id } = await params;
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);

  if (!res.ok) notFound();

  const post: Post = await res.json();

  return (
    <article className="max-w-2xl mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-6">{post.title}</h1>
      <p className="text-gray-700 leading-relaxed">{post.body}</p>
    </article>
  );
}

動的Metadataの確認方法

generateMetadata が正しく動いているか確認するときは、DevToolsのElementsタブは使わないでください

Next.js 15以降のStreaming Metadataの仕組みにより、DevToolsのElementsには <title> が表示されないことがあります。

正しい確認方法はページのソースを表示することです。

ブラウザで右クリック →「ページのソースを表示」

SEOクローラーもページのソースを読むため、ソースに入っていればSEO的には完全に機能しています

💡 豆知識:

Next.jsは generateMetadatapage コンポーネントのfetchを自動でデデュープ(重複排除)します。

同じURLへのfetchが2回書かれていても、実際のリクエストは1回しか飛びません。

OGP設定 — SNSシェア時のカード表示

TwitterやSlackでURLをシェアしたときに表示されるカード(OGP)も設定できます。

TSX
// app/layout.tsx

export const metadata: Metadata = {
  title: {
    default: 'My Blog',
    template: '%s | My Blog',
  },
  description: 'Next.jsで作ったブログです。',
  openGraph: {
    siteName: 'My Blog',
    type: 'website',
    locale: 'ja_JP',
  },
  twitter: {
    card: 'summary_large_image',
  },
};

各記事ページの generateMetadata にも openGraph を追加するとより丁寧です。

TSX
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id } = await params;
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  const post: Post = await res.json();

  return {
    title: post.title,
    description: post.body.slice(0, 100),
    openGraph: {
      title: post.title,
      description: post.body.slice(0, 100),
      type: 'article',
    },
  };
}

layout.tsx の完成形

ここまでの内容を組み込んだ layout.tsx の最終形です。

TSX
// app/layout.tsx

import type { Metadata } from 'next';
import './globals.css';
import Header from '@/components/Header';
import Footer from '@/components/Footer';

export const metadata: Metadata = {
  title: {
    default: 'My Blog',
    template: '%s | My Blog',
  },
  description: 'Next.jsで作ったブログです。',
  openGraph: {
    siteName: 'My Blog',
    type: 'website',
    locale: 'ja_JP',
  },
  twitter: {
    card: 'summary_large_image',
  },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body className="bg-gray-50 text-gray-900 min-h-screen flex flex-col">
        <Header />
        <main className="flex-1">
          {children}
        </main>
        <Footer />
      </body>
    </html>
  );
}

flex flex-colflex-1 の組み合わせにより、フッターが常にページ下部に固定されます。

まとめ

この記事で学んだこと:

  • Tailwind CSS はクラス名を書くだけでスタイルが当たるユーティリティファーストなフレームワーク
  • hidden md:flex のようなレスポンシブプレフィックスでモバイル対応もクラス名で完結する
  • metadata オブジェクトを export するだけで <head> のメタ情報が設定できる
  • title.template でサイト名を自動付与できる
  • generateMetadata で動的ルートのメタ情報をAPIから生成できる
  • openGraph / twitter でSNSシェア時のカード表示も設定できる

次の記事はいよいよ最終回です。作ったサイトをVercelにデプロイして、世界中からアクセスできる状態にしましょう。