0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-10-17

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

技術選定

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

React

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

Next.js

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

Typescript

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

Tailwind.css

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

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

環境を構築する

Next.jsのプロジェクトを作成する

以下コマンドを実行する

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

最終的なフォルダ構成

最近の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

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

# きょうのできごと

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

app/layout.tsx

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

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

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

@tailwind base;
@tailwind components;
@tailwind utilities;

tsconfig.json

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

components/PostPreview.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

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

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?