React,Next.js,Tailwind.css,TypeScriptでフロント初心者がブログサイトを構築してみる

Reactの環境構築までは下記へ記載しています。

https://qiita.com/otaruit/items/63dc7ab5f113fcbdc7fd

# 技術選定
Reactの勉強をするためにブログサイトを作りたいのですが、どんな技術を使うべきか調べてみました。

### React
JavaScriptのUI側のライラブリ。フレームワークと違ってあくまでライラブリ。

### Next.js
サーバー側で画面を構築してクライアントに返す（SSR）ことで高速表示が可能なフレームワーク。

### Typescript
静的型付けなのでエラーを防ぎやすい。大規模プロジェクトで採用されることが多い。
仕事で使う技術の勉強の為こちらを採用。

### Tailwind.css
bootstrapみたいなcssのフレームワーク。
最近よく使われているみたいです。bootstrapより自由度が高いが慣れが必要。


上記を使って作っていきます。
羅列してもよくわからなかったので調べると、
Next.jsがReactを拡張したもので、Typescriptはこれらのフレームワークやライラブリの中で使用されている、という関係だそうです。

# 環境を構築する

### Next.jsのプロジェクトを作成する
以下コマンドを実行する

```powershell
npx create-next-app@latest my-blog --tpescript

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

npm run dev

```

Next.jsのプロジェクトが作成できました。
![Screenshot 2024-10-21 135637.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722467/aa3924f5-aaf8-83c4-5982-6057c369e819.png)

# 最終的なフォルダ構成
最近のNext.jsのフォルダ構成（App  Router)で作成しています。

```
/
├── app/
│   ├── page.tsx
│   ├── layout.tsx
│   ├── [slug]/
│   │   └── page.tsx
├── components/
│   ├── PostList.tsx
│   ├── PostPreview.tsx
├── lib/
│   ├── api.ts
├── posts/
│   ├── example-post-1.md
│   ├── example-post-2.md
├── public/
├── styles/
│   └── globals.css
├── next.config.js
├── tailwind.config.js
├── tsconfig.json
```

example-post-1.md
```md
---
title: 'きょうのできごと'
date: '2023-05-21'
excerpt: '今日は楽しかった。Reactでブログを作成することができた。'
---

# きょうのできごと

今日は楽しかった。
なぜなら、Reactでブログを作成することができたからだ。
僕はうれしかった。
```

### app/layout.tsx
まず、全体に適用するスタイルをglobals.cssで設定しているのでimportします。
RootLayoutの関数によってレイアウトコンポーネントを定義します。
これによってapp以下のフォルダでレイアウトが共通で使われることになります。

この関数の引数のchildrenは子要素の内容を受け取るための変数で、これがmainタグの中で表示されることになります。


```tsx
import '../styles/globals.css'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ja">
      <body>
        <div className="max-w-4xl mx-auto px-4 py-8">
          <header className="mb-8">
            <h1 className="text-4xl font-bold">My Blog</h1>
          </header>
          <main>{children}</main>
          <footer className="mt-8 text-center text-gray-500">
            © 2023 My Blog
          </footer>
        </div>
      </body>
    </html>
  )
}
```

styles/globals.css
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

tsconfig.json
```json
{
  "compilerOptions": {
    // ... 他の設定 ...
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  },
  // ... 他の設定 ...
}
```

components/PostPreview.tsx
```tsx
import Link from 'next/link'
import { Post } from '../lib/api'

export default function PostPreview({ post }: { post: Post }) {
  return (
    <div className="mb-8">
      <h2 className="text-3xl font-bold mb-2">
        <Link href={`/${post.slug}`}>
          <span className="hover:underline">{post.title}</span>
        </Link>
      </h2>
      <p className="text-gray-500 mb-4">{post.date}</p>
      <p className="text-lg">{post.excerpt}</p>
      <Link href={`/${post.slug}`}>
        <span className="text-blue-500 hover:underline">続きを読む</span>
      </Link>
    </div>
  )
}
```

components/PostList.tsx
```tsx
import Link from 'next/link'
import { Post } from '../lib/api'

export default function PostList({ posts }: { posts: Post[] }) {
  return (
    <ul className="space-y-4">
      {posts.map((post) => (
        <li key={post.slug}>
          <Link href={`/${post.slug}`}>
            <div className="block hover:bg-gray-100 p-4 rounded">
              <h3 className="text-lg font-semibold">{post.title}</h3>
              <p className="text-sm text-gray-500">{post.date}</p>
            </div>
          </Link>
        </li>
      ))}
    </ul>
  )
}
```

lib/api.ts
```ts
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

const postsDirectory = path.join(process.cwd(), 'posts')

export interface Post {
  slug: string;
  title: string;
  date: string;
  excerpt: string;
  content?: string;
}

export function getAllPosts(fields: (keyof Post)[] = []): Post[] {
  console.log('Posts directory:', postsDirectory); // デバッグ用
  const slugs = fs.readdirSync(postsDirectory)
  console.log('Found slugs:', slugs); // デバッグ用
  const posts = slugs
    .map((slug) => getPostBySlug(slug, fields))
    .sort((post1, post2) => (post1.date > post2.date ? -1 : 1))
  console.log('Processed posts:', posts); // デバッグ用
  return posts
}

export function getPostBySlug(slug: string, fields: (keyof Post)[] = []): Post {
  const realSlug = slug.replace(/\.md$/, '')
  const fullPath = path.join(postsDirectory, `${realSlug}.md`)
  console.log('Reading file:', fullPath); // デバッグ用
  const fileContents = fs.readFileSync(fullPath, 'utf8')
  const { data, content } = matter(fileContents)
  console.log('Parsed frontmatter:', data); // デバッグ用

  const items: Post = {
    slug: realSlug,
    title: '',
    date: '',
    excerpt: '',
  }

  fields.forEach((field) => {
    if (field === 'slug') {
      items[field] = realSlug
    }
    if (field === 'content') {
      items[field] = content
    }
    if (typeof data[field] !== 'undefined') {
      items[field] = data[field]
    }
  })

  console.log('Processed post:', items); // デバッグ用
  return items
}
```

app/page.tsx
```tsx
import { getAllPosts, Post } from '../lib/api'
import PostList from '../components/PostList'
import PostPreview from '../components/PostPreview'

export default function Home() {
  const posts = getAllPosts(['title', 'date', 'slug', 'excerpt'])
  console.log('Posts in Home:', posts); // デバッグ用
  const latestPost = posts[0]
  const otherPosts = posts.slice(1)

  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
      <div className="md:col-span-2">
        {latestPost && <PostPreview post={latestPost} />}
      </div>
      <div>
        <h2 className="text-2xl font-bold mb-4">過去の記事</h2>
        <PostList posts={otherPosts} />
      </div>
    </div>
  )
}
```


# できあがった画面

![Screenshot 2024-10-21 143008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722467/f454ddc0-47ee-25df-b30f-e870f1a036c2.png)