🎃

Next.js + TypeScriptでブログを作成する【完全初心者向け】

2023/02/08に公開

概要

今回は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

サンプルプロジェクト

GitHub

初期設定

まずはNext.jsのプロジェクトを作成します。

npx create-next-app --typescript

--typescriptをつけることでTypeScriptのプロジェクトが作成されます。

srcとappのディレクトリは作成しません。

ディレクトリ構成

directories

ディレクトリ構成の上から順に説明していきます。

デフォルトから変更しないファイル

_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-postfirst-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を返しています。

ここでは、frontMatterslugcontentを返しています。

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からfrontMatterslugcontentを受け取っています。
{ 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を指定することで、子要素に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を使用してブログを作成しました。

気づいたことを書き出してみる

今後のカスタムしていきたいこと

  • ブログのデザインをtailwindcssでカスタマイズする
  • frontMatterにカテゴリーを追加して、カテゴリー別に記事を表示する
MidraLab(ミドラボ)

Discussion