Next.js + TypeScriptでブログを作成する【完全初心者向け】
概要
今回はNext.jsとTypeScriptの勉強がしてみたかったので、手始めにブログを作成してみました。
また、ブログを作成するにあたって、GitHubとVercelを使って公開する方法を紹介します。
Vercelは無料で公開できるので、個人開発でブログを作成するのには最適です。後ほど詳しく紹介します。¥
Reactを1週間程度触ったことがありましたが、Typescriptはほぼ触ったことがなかったので色々と規則から外れているかもしれません。
というような初心者なので一行一行詳しく解説しています。長くなりますが、よろしくお願いします。
Next.jsとは
Next.js(ネクストジェイエス)は、Node.js上に構築されたオープンソースのWebアプリケーションフレームワークであり、
サーバーサイドスクリプトや静的Webサイトの生成などの、ReactベースのWebアプリケーション機能を有効にする。
~Wikipedia~
TypeScriptとは
TypeScriptは、Microsoft社が開発したオープンソースのプログラミング言語である。
JavaScriptのスーパーセットであり、静的型付けのコンパイラである。
~Wikipedia~
スーパーセットとは
部分集合(ぶぶんしゅうごう)とは数学における概念の1つ。集合Aが集合Bの部分集合であるとは、
AがBの一部の要素だけからなることである。AがBの一部分であるという意味で部分集合という。
二つの集合の一方が他方の部分集合であるとき、この二つの集合の間に包含関係があるという。
~Wikipedia~
Javascriptの拡張版という認識でOKです。
環境
- Windows10
- Next.js 13.1.6
- TypeScript 4.9.5
- Node.js 18.7.0
- npm 8.15.0
サンプルプロジェクト
初期設定
まずはNext.jsのプロジェクトを作成します。
npx create-next-app --typescript
--typescript
をつけることでTypeScriptのプロジェクトが作成されます。
srcとappのディレクトリは作成しません。
ディレクトリ構成
ディレクトリ構成の上から順に説明していきます。
デフォルトから変更しないファイル
_app.tsx
import '../styles/globals.css'
import type { AppProps } from 'next/app'
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
export default MyApp
_document.tsx
は削除しても問題ありません。
hello.ts
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
res.status(200).json({ name: 'John Doe' })
}
gray-matterの導入
install
npmを使ってインストールします。
npm install --save gray-matter
yarnを使ってインストールする場合は以下のコマンドを実行します。
yarn add gray-matter
gray-matterとは
gray-matterはfront matterのYAML解析しjson形式で取得を行うライブラリです。
front matterとは、Markdownファイルの先頭に記述するメタデータのことです。
もっと簡単に今回利用する例で説明すると、
---
title: 'はじめに'
date: '2023-02-06T03:07:44.675Z'
description: 'MarkDownブログ作ってみました。'
thumbnail: '/img/blog/sebastiaan-stam-H33IhK53rpg-unsplash.jpg'
---
# はじめに
勉強がてら Next.js と TypeScript を利用して MarkDown を記事にするブログを開設しました。
技術に関しては後日[Zenn](https://zenn.dev/keisuke114)に上げます。お楽しみに。
## 予定
---ので囲われた部分がfront matterです。タイトルや日付、説明文などを記述できるようになります。
ジャンルやタグなども記述出来そうです!
Markedの導入
install
npmを使ってインストールします。
npm install --save marked
yarnを使ってインストールする場合は以下のコマンドを実行します。
yarn add marked
markedとは
markedはMarkdownをHTMLに変換するライブラリです。
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
// MarkDownテキストを定義。MarkDownの内容を``で囲む事を忘れずに。
const markdownText = `
# 例
ここはMarkDownテキストです。
- リスト1
- リスト2
- リスト3
`;
// MarkDownテキストをHTMLに変換。markedの引数にMarkDownテキストを渡す
const html = marked(markdownText);
// HTMLを表示するためにdiv要素を取得
// div要素のinnerHTMLに変換したHTMLを代入
document.getElementById("markdown").innerHTML = html;
</script>
</head>
<body>
<!-- MarkDownテキストを変換したHTMLを表示するためのdiv要素 -->
<div id="markdown"></div>
</body>
</html>
[slug].tsxについて
こちらは投稿した記事のタイトル、サムネイル画像、本文を表示するページを作成するものになります。
getStaticPaths関数について
getStaticPaths関数はビルド時に実行され、各投稿ページに対するパスを生成します。
export async function getStaticPaths() {
const files = fs.readdirSync(path.join('posts'))
const paths = files
.filter((filename) => filename.includes('.md'))
.map((filename) => ({
params: {
slug: filename.replace('.md', ''),
},
}))
return {
paths,
fallback: false,
}
}
fs.readdirSync(path.join('posts'))
でpostsディレクトリのファイルを取得しています。
fsはNode.jsの標準モジュールで、ファイルを操作するためのモジュールです。
ここではfiles
にpostsディレクトリのファイルを格納しています。
filter((filename) => filename.includes('.md'))
で.mdファイルのみを取得しています。
.mdファイルのみを取得するためにfilter
を使用しています。
map((filename) => ({params: {slug: filename.replace('.md', ''),},}))
でslugを取得しています。
map
を使用して、ファイル名から.md
を除いた部分をslug
として取得しています。
ここではfilename.replace('.md', '')で.mdの部分を何もない状態に置き換えて、.mdを除いたという処理をしています。
slugとは
slugとは、URLの一部分を表すものです。
例えば、https://example.com/posts/first-post
のfirst-post
がslugになります。
return {paths, fallback: false,}
でpathsを返しています。
pathsには、上記の処理で置き換えたものが格納されるということですね。
fallbackとは
ここの fallback: falseは、デフォルトのエラー処理が有効でないことを意味します。
このオプションがfalseに設定されている場合、特定のエラーが発生したときに、代わりのアクションが実行されないことを意味します。
このオプションがtrueに設定されている場合、特定のエラーが発生したときに、自分で制作した404ページを表示することができます。
getStaticProps関数について
export async function getStaticProps({ params: { slug } }: never) {
const markdownWithMeta = fs.readFileSync(path.join('posts', slug + '.md'), 'utf-8')
const { data: frontMatter, content } = matter(markdownWithMeta)
return {
props: {
frontMatter,
slug,
content,
},
}
}
({ params: { slug } }: never)とは
ここでは、getStaticProps
の引数にslug
を渡しています。
slug
は、上記のgetStaticPaths
で取得したものです。
neverとは
何も返さないことを意味します。
この例では、getStaticProps関数は、paramsオブジェクトを受け取ることが確定しています。
このparamsオブジェクトは、URLスラグを含むことが確定しています。しかし、この関数が受け取る引数については、
他のパラメータが含まれないことが確定しているということです。これは、never型が使用されている理由です。
fs.readFileSync(path.join('posts', slug + '.md'), 'utf-8')
で.mdファイルを取得しています。
ここでは、slug
を使用して、posts
ディレクトリの.mdファイルを取得しています。
const { data: frontMatter, content } = matter(markdownWithMeta)
でfrontMatterとcontentを取得しています。
ここでは、matter
を使用して、frontMatterとcontentを取得しています。
matterとは
matterは、Markdownファイルのメタデータを取得するためのモジュールです。
ここでは、frontMatter(メタデータ)とcontent(MarkDownで書いた内容)を取得しています。
return {props: {frontMatter, slug, content,},}
でpropsを返しています。
ここでは、frontMatter
、slug
、content
を返しています。
Postコンポーネントについて
const BlogPost = (props: { frontMatter: { [key: string]: string }; slug: string; content: string }) => (
<div className={styles.container}>
<div className="prose prose-sm sm:prose lg:prose-lg mx-auto prose-slate">
<Image className="thumbnail" src={props.frontMatter.thumbnail} alt={props.frontMatter.title} />
<div dangerouslySetInnerHTML={{ __html: marked(props.content) }} />
</div>
</div>
)
export default BlogPost
const BlogPost = (props: { frontMatter: { [key: string]: string }; slug: string; content: string }) => (
でBlogPostコンポーネントを作成しています。
ここでは、propsからfrontMatter
、slug
、content
を受け取っています。
{ frontMatter: { [key: string]: string }
ココがキー配列になっているのは、frontMatterの中には、titleやdescriptionなどのキーをもとに,frontMatterの中身を取得するためです。
<Image className="thumbnail" src={props.frontMatter.thumbnail} alt={props.frontMatter.title} />
で画像を表示しています。
ここでは、props.frontMatter.thumbnail
で画像のパスを取得しています。
classNameは、cssでスタイルを当てるために使用しています。
srcは、指定されたパスの画像を表示するために使用しています。
altは、画像の代替テキストを指定するために使用しています。
<div dangerouslySetInnerHTML={{ __html: marked(props.content) }} />
でMarkdownを表示しています。
ここでは、marked
を使用して、MarkdownをHTMLに変換しています。
dangerouslySetInnerHTML
は、HTMLを表示するために使用しています。
__htmlは、Reactで要素のinnerHTMLを設定するための特別な属性です。この場合、HTML文字列をその要素の内容に設定するために使用されています。
export default BlogPost
でBlogPostコンポーネントをエクスポートしています。
index.tsxについて
index.tsx ファイルは、通常、React アプリケーションのエントリポイントとなるファイルです。
このファイルには、Reactのルートコンポーネントが含まれており、アプリケーションがレンダリングされるときに最初に呼び出されます。
export async function getStaticProps() {
// postsディレクトリのファイルを取得
const files = fs.readdirSync(path.join('posts'))
const posts = files
.filter((filename) => filename.includes('.md'))
.map((filename) => {
// ファイル名から拡張子を除いたものをslugとする
const slug = filename.replace('.md', '')
const markdownWithMeta = fs.readFileSync(path.join('posts', filename), 'utf-8')
const { data: frontMatter } = matter(markdownWithMeta)
return {
slug,
frontMatter,
}
})
.sort((a, b) => new Date(b.frontMatter.date).getTime() - new Date(a.frontMatter.date).getTime())
return {
props: {
posts,
},
}
}
export async function getStaticProps() {
でgetStaticProps関数を作成しています。
ここでは、posts
ディレクトリの中の.mdファイルを取得しています。
const files = fs.readdirSync(path.join('posts'))
でファイルを取得しています。
ここでは、posts
ディレクトリの中の全てファイルを取得してfilesに格納しています。
const posts = files.filter((filename) => filename.includes('.md'))
で.mdファイルに絞っています。
ここでは、files
の中から.md
ファイルを取得しています。
map((filename) => {
でファイル名から拡張子を除いたものをslugとしています。
この.map()メソッドは、配列内の各要素に対して何らかの処理を行い、結果を含む新しい配列を生成するものです。
const slug = filename.replace('.md', '')
でファイル名から拡張子を除いたものをslugとしています。
slugは、URLの一部として使用される、記事の識別子です。
const markdownWithMeta = fs.readFileSync(path.join('posts', filename), 'utf-8')
でファイルの中身を取得しています。
path.join('posts', filename)で、フォルダー名postsとファイル名filenameを結合し、完全なファイルパスを作成しています。
fs.readFileSyncメソッドは、同期的にファイルを読み込み、そのデータを返します。
第一引数にはファイルパス、第二引数には文字コード(この例では 'utf-8')を指定します。
const { data: frontMatter } = matter(markdownWithMeta)
でfrontMatterを取得しています。
ここでは、matter
を使用して、MarkdownのfrontMatterを取得しています。(frontMatterは、記事のタイトルや日付などのメタデータのことです。)
return { slug, frontMatter }
でslugとfrontMatterを返しています。
ここでは、slugとfrontMatterを返しています。
.sort((a, b) => new Date(b.frontMatter.date).getTime() - new Date(a.frontMatter.date).getTime())
で日付順にソートしています。
b.frontMatter.date).getTime() - new Date(a.frontMatter.date).getTime()ここでは、日付の降順にソートしています。
new Date(a.frontMatter.date)はfrontMatterの日づけをDateオブジェクトに変換しています。
getTime()は、1970年1月1日からのミリ秒数を返します。
return { props: { posts } }
でpostsを返しています。
export default Home
でHomeコンポーネントをエクスポートしています。
Homeコンポーネントについて
const Home = (props: {
posts: [
{
slug: string
frontMatter: { [key: string]: string }
}
]
}) => {
return (
<div className={styles.container}>
{props.posts.map(({ slug, frontMatter: { title, description } }) => (
<Link key={slug} href={`/blog/${slug}`} passHref>
<h5>{title}</h5>
<p>{description}</p>
<hr />
</Link>
))}
</div>
)
}
const Home = (props: { posts: [ { slug: string frontMatter: { [key: string]: string } } ] }) => {
ここでは、Homeコンポーネントを作成しています。
props内にslugとfrontMatterの型を定義しています。
{props.posts.map(({ slug, frontMatter: { title, description } }) => (
で、postsをmapで回しています。
ここでは、各postsに含まれるslugとfrontMatterを取得しています。
<Link key={slug} href={
/blog/${slug}} passHref>
で、Linkコンポーネントを作成しています。
ここでは、Linkコンポーネントを作成しています。
keyにはslugを指定しています。
hrefには、/blog/${slug}
を指定しています。
passHrefを指定することで、子要素にaタグを指定できるようになります。
Linkコンポーネントについて
Linkコンポーネントは、ページ遷移を行うためのコンポーネントです。
Linkコンポーネントを使用することで、ページ遷移時にページの再読み込みを行わずに、ページ遷移を行うことができます。
Linkコンポーネントは、aタグのように使用することができます。
keyには、リストをmapで回した際に、各要素を一意に識別するための値を指定します。
<h5>{title}</h5>
で、titleを表示しています。
ここでは、各postsのfrontMatterからtitleを取得し表示しています。
<p>{description}</p>
で、descriptionを表示しています。
ここでは、各postsのfrontMatterからdescriptionを取得し表示しています。
完成!!
以上で、ブログの作成は完了です。
お疲れ様でした。
GithubにコードをアップロードしていればVercelに簡単にデプロイすることができます。
Vercelにデプロイすると、自動でビルドが行われ、デプロイが完了します。
参考URL
まとめ
今回は、Next.jsを使用してブログを作成しました。
気づいたことを書き出してみる
- 各tsxファイルはそれ1つでC#でいうところのクラスになる
- 1つのクラスにはpropsという値を保持できるものがある
-
getStatic~
系の関数はNext.jsの特殊な関数で、ビルド時に実行される.(https://nextjs.org/docs/basic-features/data-fetching/get-static-props)
今後のカスタムしていきたいこと
- ブログのデザインをtailwindcssでカスタマイズする
- frontMatterにカテゴリーを追加して、カテゴリー別に記事を表示する
midra-lab.notion.site/MidraLab-dd08b86fba4e4041a14e09a1d36f36ae 個人が興味を持ったこと × チームで面白いものや興味を持ったものを試していくコミュニティ
Discussion