作って学ぶ Next.js 13!マークダウンブログを作ってみよう | App Router対応

当サイトでは一部リンクに広告が含まれています

Next.jsの最新バージョン、すなわちバージョン13に、新たな機能が導入されました。その名も「appディレクトリ」です。これにより、ウェブ開発者の私たちは以前とは全く異なる方法でさまざまなタスクを実行できるようになります。

「appディレクトリ」とは?:

App Router は新たに作られたフォルダで、従来の「pagesディレクトリ」と同様、URLのルーティングを管理するためのものです。

例えば、’app/blog/post’というフォルダがあった場合、これは’www.yourwebsite.com/blog/post’というURLパスにマッピングされます。

この新しいシステムは「App Router」あるいは「app directory」として知られています。

ただし、まだ App Router が導入されてからあまり時間が経っていないため、 App Router を利用した開発例はあまり多くありません。

そこで今回、この新機能を使って一緒にマークダウンブログを作成してみましょう!

 なお、Next.js を使うには、まず JavaScript や React を習得する必要があります。

まだ自信がなければ、ロードマップに沿って改めて学習しておきましょう!

目次

Next.jsプロジェクトのセットアップ(Tailwind CSS付き)

それでは、まずNext.jsプロジェクトをセットアップしましょう。
この作業はターミナルまたはコマンドプロンプトを開き、次のコマンドを実行します。

npx create-next-app@latest myblog

実行すると、いくつかの設定項目が表示されます。ここでは以下のように選択してください。

✔ Would you like to use TypeScript with this project? … No
✔ Would you like to use ESLint with this project? … Yes
✔ Would you like to use Tailwind CSS with this project? … Yes
✔ Would you like to use `src/` directory with this project? … No
✔ Use App Router (recommended)? … Yes
✔ Would you like to customize the default import alias? … No

今回は、Next.js 13.4から正式に導入されたApp Router(app directory)を使用します。
また、後々見た目を整えるためにTailwind CSSも有効にしておきます。

上記の設定を終えると、「my-blog」という名前の新しいディレクトリが作成され、そこにNext.jsプロジェクトがセットアップされます。

次に、「my-blog」ディレクトリが作成されたら、.eslintrc.jsonというファイルを編集します。
これは、初期状態で発生してしまうESLintのエラーを解消するためです。

{
  "extends":["next","next/core-web-vitals","next/babel"]
}

また、全ページに不要なスタイルが適用されてしまうので、それを削除します。具体的には、app/global.css ファイルの一番下にある以下の記述を削除します。

/* 以下を丸ごと削除 */
body {
  color: rgb(var(--foreground-rgb));
  background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb)))
    rgb(var(--background-start-rgb));
}

これらのステップを踏むことで、プロジェクトの準備は一旦完了となります。

マークダウンファイル配置場所の作成

はじめに、私たちのブログ記事のコンテンツを保存するための場所を作成しましょう。
これを実現するために、my-blogディレクトリ内に新たなcontentディレクトリを作成します。以下のコマンドをターミナルに入力しましょう。

cd my-blog
mkdir content

これで、contentディレクトリが作成され、ブログ記事用のマークダウンファイルをここに配置します。

ブログ記事の作成

次に、具体的なブログ記事を作成してみましょう。

今回は例として、hello-world.mdというマークダウンファイルをmy-blog/contentディレクトリ内に作成します。

---
title: Hello World
date: "2023-06-24"
description: "This is my first blog post!"
---

## 見出し

This is my first blog post!

上記のように、記事のタイトル、公開日、説明、そして本文を記述してみましょう。

Next.jsでは、マークダウンファイルを解析し、必要なデータ(例えばタイトルや本文)を取得するためにgray-matterパッケージを使用します。

以下のコマンドを実行して、gray-matterパッケージをインストールします。

npm install gray-matter

また、マークダウンをHTMLに変換するためには、remarkremark-htmlパッケージを使用します。

これらも同様に、以下のコマンドでインストールできます。

npm install remark remark-html

インストールが完了したら、my-blog/app/page.jsファイルを編集します。このファイルはブログの一覧ページとして機能し、ローカル環境で実行する場合にはhttp://localhost:3000にアクセスした際に表示されます。

以下のように編集しましょう:

import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import Link from 'next/link';

export default async function Blogs() {
  // contentディレクトリ内のマークダウンファイル一覧を取得
  const postsDirectory = path.join(process.cwd(), 'content');
  const fileNames = fs.readdirSync(postsDirectory);

  // 各ファイルの中身を取得
  const posts = await Promise.all(
    // 各ファイル情報を取得
    fileNames.map(async (fileName) => {
      const filePath = path.join(postsDirectory, fileName);
      const fileContents = fs.readFileSync(filePath, 'utf8');
      const { data } = matter(fileContents);

      // slugとfrontmatter(title, date, description)を取得
      return {
        slug: fileName.replace('.md', ''),
        frontmatter: data,
      };
    })
  ).then((posts) =>
    // 最新日付順に並び替え
    posts.sort((a, b) => new Date(b.frontmatter.date) - new Date(a.frontmatter.date))
  );

  return (
    <div>
      <h1>My Blog</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.slug}>
            <Link href={`/blog/${post.slug}`}>{post.frontmatter.title}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

このコードでは、fspath、そしてgray-matterパッケージを用いて、contentディレクトリ内のマークダウンファイルを取得します。

以上のステップにより、[slug].jsページでマークダウンが適切にHTMLとして表示されるようになります。

ブログ記事の表示

ブログ記事の詳細ページを作成しましょう。

そのために、my-blog/app/blog/[slug]/page.jsという新しいフォルダ・ファイルを作成します。

Next.js 13では、appディレクトリ内にページを作成することで、そのページが自動的にルーティングされます。

また、[slug]という名前のフォルダの中にpage.jsファイルを作成することで、動的ルーティングを実現できます。

なおApp Routerでは、[slug].jsという名前のファイルはページコンポーネントとして機能しなくなります。

そのため、ページとして表示するためには必ずpage.jsという名前のファイルを作成する必要があります。

では app/blog/[slug]/page.js ファイルを以下のように編集しましょう。
以下のコードでは、指定されたスラッグのブログ記事のメタデータとコンテンツを取得しています。

import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkHtml from 'remark-html';

// ブログ記事ページ
export default async function BlogPost({ params }) {
  // URLのパラメータから該当するファイル名を取得 (今回は hello-world)
  const { slug } = params;
  const filePath = path.join(process.cwd(), 'content', `${slug}.md`);

  // ファイルの中身を取得
  const fileContents = fs.readFileSync(filePath, 'utf8');
  const { data, content } = matter(fileContents);
  const title = data.title; // 記事のタイトル
  const processedContent = await unified().use(remarkParse).use(remarkHtml).process(content);
  const contentHtml = processedContent.toString(); // 記事の本文をHTMLに変換

  return (
    <div>
      <h1>{title}</h1>
      <div dangerouslySetInnerHTML={{ __html: contentHtml }}></div>
    </div>
  );
}

動作確認

最後に、Next.jsアプリケーションを起動して、作成したマークダウンファイルが静的に出力されることを確認しましょう。

以下のコマンドを実行します。

npm run dev

これで、Next.jsアプリケーションが実行されます。
ブログの一覧ページはhttp://localhost:3000/で、各ブログ記事の詳細ページはhttp://localhost:3000/blog/[slug]でアクセスできます。
[slug]には実際のブログ記事のスラッグが入ります。

まず http://localhost:3000 は以下のようになっています。

また、タイトルをクリックすると各ブログ記事の詳細ページへアクセスでき、以下のようになっていればOKです。

ただ、これではブログとしては読みにくすぎるので次はデザインを整えていきます。

ブログ一覧ページのデザイン

次に、Tailwind CSS を使用してブログ一覧ページの見た目を改良します。
そのために、app/page.js の return 部分を以下のように更新します。

...
  return (
    <div className="bg-white py-24 sm:py-32">
      <div className="mx-auto max-w-7xl px-6 lg:px-8">
        <div className="mx-auto max-w-2xl">
          <h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">Blog</h2>
          <div className="mt-10 space-y-16 border-t border-gray-200 pt-10 sm:mt-16 sm:pt-16">
            {posts.map((post) => (
              <article
                key={post.slug}
                className="flex max-w-xl flex-col items-start justify-between"
              >
                <div className="group relative">
                  <h3 className="mt-3 text-lg font-semibold leading-6 text-gray-900 group-hover:text-gray-600">
                    <Link
                      href={`/blog/${post.slug}`}
                      className="mt-3 text-lg font-semibold leading-6 text-gray-900 group-hover:text-gray-600"
                    >
                      {post.frontmatter.title}
                    </Link>
                  </h3>
                </div>
              </article>
            ))}
          </div>
        </div>
      </div>
    </div>
  );

この変更により、ブログ一覧ページの視覚的なレイアウトが改良されます。

しかし、記事のタイトルだけが表示されても少々物足りないですよね。
そこで、ブログ記事の日付と説明文も表示させるようにしましょう。

return (
  <div className="bg-white py-24 sm:py-32">
    <div className="mx-auto max-w-7xl px-6 lg:px-8">
      <div className="mx-auto max-w-2xl">
        <h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">Blog</h2>
        <div className="mt-10 space-y-16 border-t border-gray-200 pt-10 sm:mt-16 sm:pt-16">
          {posts.map((post) => (
            <article
              key={post.slug}
              className="flex max-w-xl flex-col items-start justify-between"
            >
              <div className="group relative">
                {/* 日付を表示 */}
                <div class="flex items-center gap-x-4 text-xs">
                  <div class="text-gray-500">{post.frontmatter.date}</div>
                </div>
                {/* 記事タイトル・リンク */}
                <h3 className="mt-3 text-lg font-semibold leading-6 text-blue-700 group-hover:text-blue-400">
                  <Link
                    href={`/blog/${post.slug}`}
                    className="mt-3 text-lg font-semibold leading-6 text-blue-700 group-hover:text-blue-400"
                  >
                    {post.frontmatter.title}
                  </Link>
                </h3>
                {/* 記事説明文を表示 */}
                <p
                  className="mt-5 line-clamp-3 text-sm leading-6 text-gray-600"
                  dangerouslySetInnerHTML={{ __html: `${post.frontmatter.description}` }}
                ></p>
              </div>
            </article>
          ))}
        </div>
      </div>
    </div>
  </div>
);

この更新により、ブログ記事の日付と説明文がそれぞれ表示されるようになります。この段階で、ブラウザで表示を確認してみてください。

さらに、複数のブログ記事が存在する状態を確認するため、content ディレクトリに新たなマークダウンファイル good-bye-world.md を作成します。
ファイルの内容は以下のようにします。

---
title: Good Bye World
date: "2023-06-25"
description: "This is my second blog post!"
---

## 見出し

This is my second blog post!

この更新を行うと、複数の記事が存在するときに最新の記事(dateが新しいもの)が一番上に表示されることが確認できます。

ブログ記事の詳細ページのデザイン

次に、ブログ記事の詳細ページを改良します。

マークダウンがHTMLに変換されて表示されるため、直接HTML要素にスタイルを適用することはできません。

この問題を解決するため、app/blog/[slug]/content.cssという新しいCSSファイルを作成します。

このファイルでは、ブログ記事のHTMLに適用するスタイルを定義します。

p {
  @apply mt-6 text-xl leading-8;
}

h1 {
  @apply mt-6 text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl;
}

h2 {
  @apply mt-8 text-2xl font-bold tracking-tight text-gray-900;
}

h3 {
  @apply mt-6 text-xl font-bold tracking-tight text-gray-900;
}

h4 {
  @apply mt-6 text-lg font-bold tracking-tight text-gray-900;
}

ul,
ol {
  @apply mt-6 list-disc ml-8;
}

li {
  @apply mt-2;
}

a {
  @apply text-indigo-600;
}

このCSSを適用するために、app/blog/[slug]/page.js の最初の部分に次の行を追加します。

import './content.css';
// 以下略

最後に、app/blog/[slug]/page.js の return の中身を以下のように変更します。

  // 略
  return (
    <div className="bg-white px-6 py-32 lg:px-8">
      <div className="mx-auto max-w-3xl text-base leading-7 text-gray-700">
        <h1 className="mt-2 text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
          {title}
        </h1>
        <div
          className="mt-6"
          dangerouslySetInnerHTML={{ __html: contentHtml }}
        ></div>
      </div>
    </div>
  );

以上の変更により、ブログ記事の見た目が改良されます。

http://localhost:3000/blog/hello-worldにアクセスして、結果を確認してみてください。

まとめ

以上の手順により App Router の仕組みを使ったブログ一覧ページとブログ記事の詳細ページを作成できました。

ブログ一覧ページでは、各ブログ記事のタイトルだけでなく、日付と説明文も表示させるようにしました。

また、ブログ記事の詳細ページでは、個別のHTML要素に対してスタイルを適用するためのCSSファイルを作成しました。

追加でヘッダーやフッターなどを作成することで、より実用的なブログサイトを作成することができますので、余力があればチャレンジしてみてください。

フロントエンドを学んでいる方におすすめの記事

目次