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のフォルダ構成(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>
)
}