# 初心者でも簡単にNext.jsでブログサイトが作れる手順書

## はじめに

筆者の作成したWebサービスの[Hacker Sheet](https://hackersheet.com)を使ってNext.jsでブログサイトを構築する手順を紹介しています。興味のある方のみ読み進めてください。

> [!IMPORTANT]
> この記事では以下の環境での手順を紹介しています。他の環境の場合はコマンドやファイルパスなどを読み替える必要があります。
>
> - macOS
> - pnpm
> - VS Code

基本的に「記載されているコマンドをそのまま実行していくことでブログサイトの構築が完了する」という内容の手順書です。新しく作成するファイルも少なく、内容をコピーして保存するだけになっているので初心者でも簡単に進めることができます。

今回はとりあえず動くものを作ってみることが主題なので細かい解説はありません。`@hackersheet/*` のような独自のnpmパッケージの使い方などの詳細は別の記事を作成予定です。

> [!CAUTION]
> この記事で紹介しているnpmパッケージ `@hackersheet/*` はalpha版です。本番環境でのご利用にはご注意ください（2024/07/20時点）。

## Node.js v20とpnpm環境のセットアップ

まずは、pnpmが使用できるか確認します。

```sh:Terminal
pnpm -v # `9.5.0` のようにバージョンが表示されたら使用可能です
```

Node.js、pnpmがインストールされていない場合は下記の記事を参考にNode.jsとpnpmのセットアップをします。

[Voltaでローカル環境にNode.jsをインストールしよう(macOS)](2024-07-19-voltaでローカル環境にnode.jsをインストールしよう.md)

## Next.jsアプリケーションの作成

Next.jsアプリケーションのプロジェクトは、下記のコマンドを実行することで簡単に作成できます。ターミナルを開いて実行します。

```sh:Terminal
pnpm create next-app my-blog --ts --tailwind --eslint --app --src-dir --import-alias '@/*'
```

実行後にnpmパッケージのインストールが開始します。しばらく待ちます。

各オプションの簡単な説明は下記の通りです。より詳しく知りたい方は[Next.jsの公式ドキュメント](https://nextjs.org/docs/pages/api-reference/create-next-app)をご覧ください。

| オプション             | 説明                                                     |
| ---------------------- | -------------------------------------------------------- |
| `my-blog`              | プロジェクト名です。同じ名前のディレクトリが作成されます |
| `--ts`                 | TypeScripを使用します                                    |
| `--tailwind`           | Tailwind CSSを使用します                                 |
| `--eslint`             | ESLintを使用します                                       |
| `--app`                | App Routerを使用します                                   |
| `--src-dir`            | `src/` ディレクトリを作成します                          |
| `--import-alias '@/*'` | Import aliasを `@/*` に設定します                        |

`Success! Created my-blog at /path/to/my-blog` というメッセージが表示されたら作成完了です。次の手順に進みます。

## プロジェクトをVS Codeで開く

```sh:Terminal
code my-blog
```

コマンドを実行してVS Codeを開きます。

> [!TIP]
> VS Codeを開いて `control` + `~` を押すとVS Codeのターミナルを開くことができます。以降のコマンドはVS Codeのターミナルで実行することをお勧めします。

## npmパッケージのアップデート

```sh:Terminal
pnpm update
```

まず、上記のコマンドでnpmパッケージをアップデートします。

> WARN  5 deprecated subdependencies found: @humanwhocodes/config-array@0.11.14, @humanwhocodes/object-schema@2.0.3, glob@7.2.3, inflight@1.0.6, rimraf@3.0.2

のようなワーニングが出ますが気にせず進めます（**2024/07/20時点**）。

`control` + `shift` + `G` を押すとサイドバーに「ソース管理」が表示されます。次のファイルが変更されています。

- package.json
- pnpm-lock.yaml

手順毎に「wip」などの適当なメッセージでコミットしておくと変更点がわかりやすくなるのでオススメです。

## ローカルサーバーの起動

この時点で間違いがないか確認するためにローカルサーバーでプロジェクトが起動できるかどうか確認してみましょう。

```sh:Terminal
pnpm dev
```

上記のコマンドを実行後にhttp://localhost:3000 をブラウザで開いて、下記の画像のようなNext.jsのサンプルページが表示されたら成功です。

![Next.jsサンプルページ](/assets/2024-07-20-初心者でも簡単にnext.jsでブログサイトが作れる手順書/Next.jsサンプルページ.jpg)

確認できたらターミナルで `control` + `C`を押してローカルサーバーを停止します。

## npmパッケージを追加

- @hackersheet/core
- @hackersheet/next-document-content-components
- @hackersheet/next-document-content-kifu
- @hackersheet/react-document-content
- @hackersheet/react-document-content-styles
- katex

今回使用するこれらのnpmパッケージを次のコマンドでまとめて追加します。

```sh:Terminal
pnpm add @hackersheet/core@alpha @hackersheet/next-document-content-components@alpha @hackersheet/next-document-content-kifu@alpha @hackersheet/react-document-content@alpha @hackersheet/react-document-content-styles@alpha katex
```

実行後に新しく以下のWARNが表示されますが、動作に影響はないので無視して進めます（**2024/07/20時点**）。

```sh:実行後のWARN
WARN Issues with peer dependencies found
.
└─┬ @hackersheet/next-document-content-kifu 0.1.0-alpha.2
  └─┬ kifu-for-js 5.4.1
    ├── ✕ unmet peer react@^16.14.0: found 18.3.1
    ├── ✕ unmet peer react-dom@^16.14.0: found 18.3.1
    └─┬ mobx-react 6.3.1
      ├── ✕ unmet peer react@"^16.8.0 || 16.9.0-alpha.0": found 18.3.1
      └─┬ mobx-react-lite 2.2.2
        └── ✕ unmet peer react@^16.8.0: found 18.3.1
```

`control` + `shift` + `G` で「ソース管理」を開きます。次のファイルが変更されています。

- package.json

  ```json:package.json
  // ...
  "dependencies": {
    "@hackersheet/core": "0.1.0-alpha.4", // [!code ++]
    "@hackersheet/next-document-content-components": "0.1.0-alpha.5", // [!code ++]
    "@hackersheet/next-document-content-kifu": "0.1.0-alpha.1", // [!code ++]
    "@hackersheet/react-document-content": "0.1.0-alpha.5", // [!code ++]
    "@hackersheet/react-document-content-styles": "0.1.0-alpha.4", // [!code ++]
    "katex": "^0.16.11", // [!code ++]
    "next": "14.2.5",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  // ...
  ```

- pnpm-lock.yaml

変更を確認してコミットします。

## shadcn/uiのセットアップ

コンポーネントライブラリの[shadcn/ui](https://ui.shadcn.com/)のセットアップを行います。

```sh:Terminal
pnpm dlx shadcn-ui@latest init --yes
```

コマンドを実行すると対話形式のセットアップが開始します。次のように進めていきます。

- Which style would you like to use?

  **Default**

- Which color would you like to use as base color?

  **Slate**

- Would you like to use CSS variables for colors?

  **yes**

セットアップが完了すると以下のファイルが作成・変更されます(末尾の\*は新規作成されたファイルを示しています)。

- components.json \*
- package.json
- pnpm-lock.yaml
- tailwind.config.ts
- src/app/globals.css
- src/lib/utils.ts \*

変更を確認してコミットします。

## 環境変数の設定

```sh:Terminal
touch .env.development.local
```

`.env.development.local` を作成して以下の内容で保存します。

```ini:.env.development.local
HACKERSHEET_API_ENDPOINT=https://api.hackersheet.com/example/v1/graphql
HACKERSHEET_API_ACCESS_KEY=hsws_TVZ6MjdnMUNrWXdyRjZ5SEZSOFp3OWVXS0ZiR3lHSFE6akdzVzQ5WlRhc0RwRm1ZWGRpZWl5aHZpM2ZtSlhSOG42ZExEbWZMQXd1c2dwdXZ0
```

## next.config.mjsの変更

```javascript:next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: { // [!code ++]
    formats: ["image/webp"], // [!code ++]
    remotePatterns: [ // [!code ++]
      { // [!code ++]
        protocol: "https", // [!code ++]
        hostname: "public-content.hackersheet.com", // [!code ++]
        pathname: "/**", // [!code ++]
      } // [!code ++]
    ], // [!code ++]
  }, // [!code ++]
};

export default nextConfig;
```

## src/app/globals.cssの変更

`src/app/globals.css` を変更します。

以下の `/* ここから下の部分を追加 */` より下にある内容を追加してください。

```css:globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  /* ... */
  /* ... */
  /* ... */
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}

/* ここから下の部分を追加 */
@layer base {
  :root {
    --hsdc-scroll-margin-top: 0px;
    --hsdc-font-code: monospace, sans-serif;
    --hsdc-border: var(--border);
    --hsdc-muted: var(--muted);
    --hsdc-muted-foreground: var(--muted-foreground);
    --hsdc-link: var(--primary);
    --hsdc-code-block: var(--muted);
    --hsdc-shiki-diff-add: 160 84% 39%;
    --hsdc-shiki-diff-remove: 350 89% 60%;
    --hsdc-shiki-highlighted-word: 60 100% 50%;
  }

  :root {
    --github-alert-default-color: rgb(208, 215, 222);
    --github-alert-note-color: rgb(9, 105, 218);
    --github-alert-tip-color: rgb(26, 127, 55);
    --github-alert-important-color: rgb(130, 80, 223);
    --github-alert-warning-color: rgb(191, 135, 0);
    --github-alert-caution-color: rgb(207, 34, 46);

    .dark {
      --github-alert-default-color: rgb(48, 54, 61);
      --github-alert-note-color: rgb(31, 111, 235);
      --github-alert-tip-color: rgb(35, 134, 54);
      --github-alert-important-color: rgb(137, 87, 229);
      --github-alert-warning-color: rgb(158, 106, 3);
      --github-alert-caution-color: rgb(248, 81, 73);
    }
  }
}
```

## src/lib/hackersheet/client.tsを作成

```sh:Terminal
mkdir -p src/lib/hackersheet/ && touch src/lib/hackersheet/client.ts
```

上記のコマンドを実行して下記の内容で保存します。

```typescript:src/lib/hackersheet/client.ts
import { createClient } from '@hackersheet/core'
import { cache } from 'react'

const client = cache(() =>
  createClient({
    url: process.env.HACKERSHEET_API_ENDPOINT!,
    accessKey: process.env.HACKERSHEET_API_ACCESS_KEY!,
  })
)()

export { client }
```

## src/app ディレクトリのファイルを変更

- src/app/layout.tsx
- src/app/page.tsx

`src/app` ディレクトリの、これらの2つのファイルを変更します。以下の内容にそのまま置き換えます。

### src/app/layout.tsx

```tsx:src/app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Link from "next/link"; // [!code ++]

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <header className="mx-auto max-w-screen-sm"> // [!code ++]
          <div className="text-lg py-10"><Link href="/">my blog</Link></div> // [!code ++]
        </header> // [!code ++]

        {children}
      </body>
    </html>
  );
}
```

### src/app/page.tsx

Next.jpサンプルページのコードを置き換えるので差分が多いです。

```tsx:src/app/page.tsx
import { client } from "@/lib/hackersheet/client";
import Link from "next/link";

export default async function Home() {
  const { documents } = await client.getDocuments();

  return (
    <main className="mx-auto max-w-screen-sm">
      <ul className="list-disc">
        {documents &&
          documents.map((document) => (
            <li key={document.id} className="my-2">
              <Link href={`/posts/${document.slug}`}>{document.title}</Link>
            </li>
          ))}
      </ul>
    </main>
  );
}
```

## src/app/posts/[documentSlug]/page.tsx 作成

```sh:Terminal
mkdir -p 'src/app/posts/[documentSlug]/' && touch 'src/app/posts/[documentSlug]/page.tsx'
```

`src/app/posts/[documentSlug]/page.tsx` を作成し、以下の内容で保存します。

```tsx:src/app/posts/[documentSlug]/page.tsx
import { client } from "@/lib/hackersheet/client";
import {
  CodeBlock,
  Image,
  Link,
  LinkCard,
  Mermaid,
  XPost,
  Youtube,
} from "@hackersheet/next-document-content-components";
import { Kifu, KifuTo } from "@hackersheet/next-document-content-kifu";
import { notFound } from "next/navigation";
import { DocumentContent } from "@hackersheet/react-document-content";
import documentContentStyle from "@hackersheet/react-document-content-styles/basic";

import "katex/dist/katex.min.css";

export default async function PostPage({
  params: { documentSlug },
}: {
  params: { documentSlug: string };
}) {
  const { document } = await client.getDocument({ slug: documentSlug });

  if (!document) notFound();

  return (
    <main className="mx-auto max-w-screen-sm">
      <h1 className="text-xl pt-10 pb-20">{document.title}</h1>

      <DocumentContent
        document={document}
        style={documentContentStyle}
        permaLinkFormat="/posts/{{slug}}"
        components={{
          codeBlock: CodeBlock,
          image: Image,
          kifu: Kifu,
          kifuTo: KifuTo,
          link: Link,
          linkCard: LinkCard,
          mermaid: Mermaid,
          xPost: XPost,
          youtube: Youtube,
        }}
      />
    </main>
  );
}
```

## 最後にローカルサーバーの起動

```sh:Terminal
pnpm dev
```

コマンドを実行して http://localhost:3000 を確認します。

![my blog](/assets/2024-07-20-初心者でも簡単にnext.jsでブログサイトが作れる手順書/my-blog.jpg)

このような画面が表示されます。リストのタイトルをクリックすると個別ページに移動します。個別ページが表示されたら成功です。

以上ですべての手順完了です。
