React + TypeScript: Next入門02 イメージとメタデータおよびCSSを扱う

Next.jsチュートリアルの公式作例を、TypeScriptも加えて、一からつくってみようというシリーズの第2回です。前回は、トップページを書き替えて、新たに追加した別ページとリンクしてみたというくらいでした。今回から、公式作例に少しずつ近づけてゆきましょう。

# `Image`コンポーネントでイメージを差し込む
Next.jsはイメージなどの静的素材も扱えます。素材ファイルを置くトップレベルは、[`public`ディレクトリ](https://nextjs.org/docs/basic-features/static-file-serving)です。Reactコンポーネントを収める`pages`と同じく、アプリケーションのルートから参照できます。

ページに表示するプロフィール画像を用意しましょう。ディレクトリ`public`に`images`をつくってください。

* 画像を`public/images/profile.png`として収めます。
    - [`next-learn/basics/basics-final/public/images/profile.jpg`](https://github.com/vercel/next-learn/blob/master/basics/basics-final/public/images/profile.jpg)を使っても構いません(拡張子が`.jpg`であることにご注意ください)。
* イメージの大きさは幅400px×高さ400pxくらいが適切です。
    - 作例のコードは正方形を前提としています。
* ひな形プロジェクトにつくられた`public`ディレクトリの使わないSVGファイルは削除して構いません。
    - `public/favicon.ico`は残しておいてください。

[\<Image\>](https://nextjs.org/docs/pages/api-reference/components/image)の`Image`コンポーネントは、イメージの扱いを最適化するために備わりました。`Image`コンポーネントの使い方はつぎに示したコード例のとおりです(図001)。`width`と`height`には希望のサイズが与えられます。アスペクト比は元画像の比率を保ってください。

```react:pages/posts/first-post.tsx
import Image from 'next/image';

const FirstPost: FC = () => {
	return (
		<>
			<Image
				src="/images/profile.png" // イメージファイルのルート
				height={144} // イメージサイズの設定
				width={144} // アスペクト比を正しく
				alt="Your Name"
			/>

		</>
	);
};
```

#### 図001■ページに`Link`コンポーネントでプロフィール画像が加わった
![qiita_2301001_007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/694e576f-db6e-fc2e-6749-eedc65e38961.png)


# `Head`コンポーネントでメタデータを定める
`<title>`など`<head>`要素に加えるページのメタデータをサポートするのが`Head`コンポーネント([\<Head\>](https://nextjs.org/docs/pages/api-reference/components/head))です。モジュール`pages/index.tsx`に`Head`コンポーネントをつぎのように加えてみましょう。トップページに`<title>`とアイコンが表示されたはずです。

```react:pages/index.tsx
import Head from 'next/head';

export default function Home() {
	return (
		<>
			<Head>
				<title>Create Next App</title>
				<link rel="icon" href="/favicon.ico" />
			</Head>

		</>
	);
}
```

`pages/posts/first-post.tsx`モジュールにも、`Head`コンポーネントで`<title>`をつぎのように定めます。ページを遷移すると、ブラウザのタブに示されるタイトルが切り替わるようになりました。

```react:pages/posts/first-post.tsx
import Head from 'next/head';

const FirstPost: FC = () => {
	return (
		<>
			<Head>
				<title>First Post</title>
			</Head>

		</>
	);
};
```


# LayoutコンポーネントにCSSモジュールのスタイルを加える
プロジェクトにはすでに`styles/globals.css`があり、`pages/_app.tsx`に読み込まれて、アプリケーション全体のスタイルを定めていました。これに対してコンポーネントごとのスタイルをローカルに割り当てるのがCSSモジュールです。

では、CSSモジュールを新たなコンポーネントモジュール`components/layout.tsx`で使ってみましょう。このコンポーネントは、あとで他のすべてのページで共有されます。まだ、CSSモジュールは定めていません。

```react:components/layout.tsx
import type { FC, ReactNode } from 'react';

type Props = {
	children: ReactNode;
};
const Layout: FC<Props> = ({ children }) => {
	return <div>{children}</div>;
};
export default Layout;
```

まず、`pages/posts/first-post.tsx`で`Layout`コンポーネントを読み込み、JSXの[フラグメント](https://ja.reactjs.org/docs/fragments.html#short-syntax)`<>`と置き替えます。つまり、コンポーネント`FirstPost`を`Layout`で包み込むのです。

```react:pages/posts/first-post.tsx
import Layout from '../../components/layout';

const FirstPost: FC = () => {
	return (
		// <>
		<Layout>

		{/* </> */}
		</Layout>
	);
};
```

CSSモジュール`components/layout.module.css`は、以下のように定めます。CSSの書き方にとくに変わったところはありません。ただし、ファイル名は`.module.css`で結んでください。

> **Important**: To use [CSS Modules](https://nextjs.org/docs/pages/building-your-application/styling/css-modules), the CSS file name must end with `.module.css`.
(「[Adding CSS](https://nextjs.org/learn/basics/assets-metadata-css/layout-component#:~:text=%3B%0A%7D-,Adding%20CSS,-Now%2C%20let%E2%80%99s%20add)」)

```css:components/layout.module.css
.container {
	max-width: 36rem;
	padding: 0 1rem;
	margin: 3rem auto 6rem;
}
```

CSSモジュールは`components/layout.tsx`から以下のように用います。

* `import`したCSSモジュールファイルのスタイルに名前(ここでは`styles`)を与えてください。
* スタイルから参照したクラス(ここでは`styles.container`)を`className`に加えます。

```react:components/layout.tsx
import styles from './layout.module.css';

const Layout: FC<Props> = ({ children }) => {
	// return <div>{children}</div>;
	return <div className={styles.container}>{children}</div>;
};
```

`pages/posts/first-post.tsx`のページには、CSSモジュールのスタイルが割り当てられたでしょう。そのうえで、デベロッパーツールで`class`を確かめてください。すると、モジュールとクラスの名前を示す識別子のあとに、つぎのような一意の記号(`__fbLkO`)が加わったはずです。一意の記号が自動的に生成されるために、CSSモジュールではクラス名の重複は起こらないのです。

> `<div class="layout_container__fbLkO">...</div>`


# グローバルスタイルを定める
すでにひな形プロジェクトにつくられていた`styles/globals.css`は、以下のように`pages/_app.tsx`に読み込まれて、アプリケーションのすべてのページに用いられます。ただし、グローバルCSS(`globals.css`)を`import`できるのは`pages/_app.tsx`だけです。

> In Next.js, you can add [global CSS](https://nextjs.org/docs/pages/building-your-application/styling#adding-a-global-stylesheet) files by importing them from [`pages/_app.js`](https://nextjs.org/docs/pages/building-your-application/routing/custom-app). You **cannot** import global CSS anywhere else.
(「[Adding Global CSS](https://nextjs.org/learn/basics/assets-metadata-css/global-styles#:~:text=npm%20run%20dev-,Adding%20Global%20CSS,-In%20Next.js)」)

```react:pages/_app.tsx
import '@/styles/globals.css'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {
	return <Component {...pageProps} />
}
```

`pages/_app.tsx`の`export default`されたReactコンポーネント(`App`)は、トップレベルでアプリケーションのすべてのページを包みます。状態はページを遷移しても保たれ、グローバルスタイルもこのコンポーネントに加えるのです。`pages/_app.tsx`を書き替えたときは、開発サーバーを[control] + [C]で停止したうえで、再起動(`npm run dev`)してください。

なお、プロジェクトの[`styles/globals.css`](https://github.com/vercel/next-learn/blob/master/basics/typescript-final/styles/global.css)の定めは、GitHubに公開された公式作例のコードで上書きました。


# レイアウトを改善する
CSSモジュールのスタイルにもう少し手を加えましょう。`components/layout.module.css`につぎのクラスを加えてください。

```css:components/layout.module.css

.header {
	display: flex;
	flex-direction: column;
	align-items: center;
}
.backToHome {
	margin: 3rem 0 0;
}
```

さらに、CSSモジュール[`styles/utils.module.css`](https://github.com/vercel/next-learn/blob/master/basics/typescript-final/styles/utils.module.css)を、GitHubのコードから新たにコピーして加えます。定められているのは複数のコンポーネントから使いまわされるユーティリティ的なクラスです。

そのうえで、モジュール`components/layout.tsx`につぎのようにCSSモジュール`styles/utils.module.css`を`import`し、コンテンツも書き加えます。コンポーネントが引数に受け取るプロパティ`home`は、トップページを他と区別してレイアウトするためのフラグです(JSXの中で条件分岐させていることをお確かめください)。なお、変数`name`の値の文字列(`'Your Name'`)は書き替えて構いません。

```react:components/layout.tsx
import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';

import utilStyles from '../styles/utils.module.css';

const name = 'Your Name';
export const siteTitle = 'Next.js Sample Website';
type Props = {

	home?: boolean;
};
// const Layout: FC<Props> = ({ children }) => {
const Layout: FC<Props> = ({ children, home }) => {
	// return <div className={styles.container}>{children}</div>;
	return (
		<div className={styles.container}>
			<Head>
				<link rel="icon" href="/favicon.ico" />
				<meta
					name="description"
					content="Learn how to build a personal website using Next.js"
				/>
				<meta
					property="og:image"
					content={`https://og-image.vercel.app/${encodeURI(
						siteTitle,
					)}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`}
				/>
				<meta name="og:title" content={siteTitle} />
				<meta name="twitter:card" content="summary_large_image" />
			</Head>
			<header className={styles.header}>
				{home ? (
					<>
						<Image
							priority
							src="/images/profile.png"
							className={utilStyles.borderCircle}
							height={144}
							width={144}
							alt=""
						/>
						<h1 className={utilStyles.heading2Xl}>{name}</h1>
					</>
				) : (
					<>
						<Link href="/">
							<Image
								priority
								src="/images/profile.png"
								className={utilStyles.borderCircle}
								height={108}
								width={108}
								alt=""
							/>
						</Link>
						<h2 className={utilStyles.headingLg}>
							<Link href="/" className={utilStyles.colorInherit}>
								{name}
							</Link>
						</h2>
					</>
				)}
			</header>
			<main>{children}</main>
			{!home && (
				<div className={styles.backToHome}>
					<Link href="/">← Back to home</Link>
				</div>
			)}
		</div>
	);
};
```

そして、トップページの`pages/index.tsx`モジュールに手を加えたのがつぎのコードです。`Layout`コンポーネントで包みましたので、`<Image>`は要らなくなりましたし、アイコンの定めも除きました。`[Your Self Introduction]`は自己紹介文に書き替えてください。トップページのレイアウトが変わりました。

```react:pages/index.tsx
import Head from 'next/head';
// import Link from 'next/link';
import Layout, { siteTitle } from '../components/layout';
import utilStyles from '../styles/utils.module.css';

export default function Home() {
	return (
		/* <>

		</> */
		<Layout home>
			<Head>
				<title>{siteTitle}</title>
			</Head>
			<section className={utilStyles.headingMd}>
				<p>[Your Self Introduction]</p>
				<p>
					(This is a sample website - you’ll be building a site like this on{' '}
					<a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
				</p>
			</section>
		</Layout>
	);
}
```

#### 図002■改められたトップページのレイアウト
![qiita_2301001_008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/ade5231f-cf34-cd8a-dfe6-80c6cf90e877.png)

モジュール`pages/posts/first-post.tsx`は、このあとはもう使いません。けれど、つぎのように少し手を入れてトップページと比べてみましょう。やはり、`Layout`コンポーネントで包みました。コンポーネント`FirstPost`は`home`プロパティを受け取らないので、トップページではないという扱いです。すると、プロフィール画像がトップページより小さくなるとともに、「← Back to home」のリンクが表示されます。この条件判定は、`Layout`コンポーネントが行っているのです。ここまでのコードは、以下のソース01に公開しました。

```react:pages/posts/first-post.tsx
// import Image from 'next/image';
// import Link from 'next/link';

import Layout from '../../components/layout';

const FirstPost: FC = () => {
	return (
		// <>
		<Layout>
			<Head>
				<title>First Post</title>
			</Head>
			{/* <Image

			/> */}

			{/* <h2>
				<Link href="/">← Back to home</Link>
			</h2> */}
		{/* </> */}
		</Layout>
	);
};
```

#### ソース01■Next入門02 イメージとメタデータおよびCSSを扱う
https://github.com/FumioNonaka/react-typescript-next-example/tree/react-typescript-next-example-02

トップページのレイアウトが少し公式作例に近づきました。次回は、データを動的に取得して、トップページに表示します。


## React + TypeScript: Next入門シリーズ

* [Next入門01 チュートリアルの作例を一からつくってみる](https://qiita.com/FumioNonaka/items/37ad26dab7e5051f672e)
* Next入門02 イメージとメタデータおよびCSSを扱う
* [Next入門03 プリレンダリングとデータ取得](https://qiita.com/FumioNonaka/items/82fd364d746de5206749)
* [Next入門04 動的ルートを定める](https://qiita.com/FumioNonaka/items/dd9b21834d3634e36540)