ここまでの記事でルーティング、データ取得、フォーム処理と、Next.jsの核となる機能を一通り学んできました。
ただ、正直なところ見た目は素っ気ないままです。
今回はサイトを実用レベルに仕上げる2つのテーマを扱います。
- Tailwind CSS — クラス名を書くだけでスタイルが当たる
- Metadata API — タイトルやOGP画像をNext.jsの仕組みで管理する
どちらも「設定が大変そう」と思われがちですが、create-next-app で作ったプロジェクトなら最初からほぼ準備が整っています。
Contents
Tailwind CSSとは
Tailwind CSSは ユーティリティファーストなCSSフレームワークです。
従来のCSSの書き方はこうでした。
/* style.css */
.button {
background-color: #3b82f6;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
}<button class="button">送信</button>Tailwindでは
クラス名を直接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行があるはずです。
@import "tailwindcss";これがあればOKです。
Tailwind v4から @import "tailwindcss" の1行に統合されました(v3では @tailwind base; @tailwind components; @tailwind utilities; の3行でしたが、現在は不要です)。
また、v3では tailwind.config.ts という設定ファイルが生成されていましたが、v4からは globals.css 内の @theme ブロックで設定を管理する方式に変わっています。
カスタムカラーなどを追加したい場合はここに書きます。
/* app/globals.css */
@import "tailwindcss";
@theme inline {
/* カスタムカラーの例 */
--color-brand: #0070f3;
}layout.tsx に以下のimport行があれば、どのページでもTailwindのクラスが使えます。
#3で layout.tsx を書き換えた場合も、この行が残っているか確認しておきましょう。
// app/layout.tsx
import './globals.css'; // ← この行があればOKTailwindの基本クラスを覚える
よく使うクラスをカテゴリ別にまとめます。
すべて覚える必要はありません。
使いながら少しずつ身につけていきましょう。
レイアウト
| クラス | 意味 |
|---|---|
flex | display: flex |
flex-col | 縦方向に並べる |
items-center | 交差軸方向に中央揃え |
justify-between | 両端揃え |
gap-4 | 要素間の隙間(1rem) |
grid grid-cols-3 | 3列のグリッド |
余白
| クラス | 意味 |
|---|---|
p-4 | padding: 1rem(全方向) |
px-4 | padding: 左右1rem |
py-2 | padding: 上下0.5rem |
mt-4 | margin-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-2は0.5rem、p-8は2remになります。
実践:記事一覧ページをTailwindで整える
これまで style={{ ... }} でインラインスタイルを書いていたコードを、Tailwindで書き直してみましょう。
// 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-md や transition-shadow のようにホバー時の動きもクラスだけで書けます。
Headerもきれいにする
// 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.tsx や layout.tsx から metadata オブジェクトを export するだけです。
// 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> に以下を生成します。
<title>投稿一覧 | My Blog</title>
<meta name="description" content="Next.jsで作ったブログの投稿一覧ページです。" />タイトルのテンプレートを設定する
layout.tsx でタイトルのテンプレートを設定すると、各ページのタイトルに自動でサイト名が付きます。
// app/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: {
default: 'My Blog', // タイトルが未設定のページに使われる
template: '%s | My Blog', // %s に各ページのタイトルが入る
},
description: 'Next.jsで作ったブログです。',
};各ページでは短いタイトルだけ書けばOKです。
// app/posts/page.tsx
export const metadata: Metadata = {
title: '投稿一覧', // → 「投稿一覧 | My Blog」になる
};動的なMetadata — 記事ページのタイトルをAPIから取得する
動的ルート([id])のページでは、取得したデータをもとにメタ情報を生成できます。
generateMetadata という特別な関数を使います。
// 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は
generateMetadataとpageコンポーネントのfetchを自動でデデュープ(重複排除)します。同じURLへのfetchが2回書かれていても、実際のリクエストは1回しか飛びません。
OGP設定 — SNSシェア時のカード表示
TwitterやSlackでURLをシェアしたときに表示されるカード(OGP)も設定できます。
// 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 を追加するとより丁寧です。
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 の最終形です。
// 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-col と flex-1 の組み合わせにより、フッターが常にページ下部に固定されます。
まとめ
この記事で学んだこと:
- Tailwind CSS はクラス名を書くだけでスタイルが当たるユーティリティファーストなフレームワーク
hidden md:flexのようなレスポンシブプレフィックスでモバイル対応もクラス名で完結するmetadataオブジェクトをexportするだけで<head>のメタ情報が設定できるtitle.templateでサイト名を自動付与できるgenerateMetadataで動的ルートのメタ情報をAPIから生成できるopenGraph/twitterでSNSシェア時のカード表示も設定できる
次の記事はいよいよ最終回です。作ったサイトをVercelにデプロイして、世界中からアクセスできる状態にしましょう。


























