💻

Next.js(TypeScript)、TailwindCSS、microCMSでブログを作成しよう!

2022/01/16に公開約12,800字4件のコメント

Next.jsとは

React.jsを拡張してサーバー機能やファイルベースルーティングなどが追加されたものです。

詳細は下記URLで確認してください!

https://tech-parrot.com/programming/next-js-react-js-tigai/#3_URLNextjs

microCMSとは

そもそもCMSとは

microCMSの説明をする前にCMSとはContents Management Systemの頭文字を並べたもので、コンテンツを管理することができるシステムを指します。

大まかに説明すると「View」「コンテンツ管理画面」「DB」の機能を持ったシステムとなります。

  • 「View」は記事の一覧ページなどを表示する機能。
  • 「コンテンツ管理画面」は記事を追加したり、お知らせを追加する機能。
  • 「DB」は記事などの情報を保持する機能。

CMSにはいくつか種類がありますが、大まかにはViewの形によって分類することができます。

View固定型

この記事を書いているZennも「View固定型」となります。他にはQiitaやnoteなども「View固定型」に含まれます。
Zennを例に説明すると記事をmarkdownで書いて公開すると執筆者はレイアウトのコードを書いていないのにいい感じに表示してくれます。
レイアウトのことを考えずに済むことが「View固定型」の特徴となります。

View自由型

WordPressが当てはまります。「View自由型」ではViewを自由に変更できるので自分の好みのデザインにすることができます。(自分でCSSなどのデザインを書く必要がありますが...)
レイアウトのことを考える必要があることが「View自由型」の特徴となります。

microCMSは?

microCMSは上記であげた2つと違い「ヘッドレスCMS」です。ヘッドはViewのことなので「ヘッドレス」=「Viewなし」と言い換えることができます。

Viewがないとフロントエンドの選択が自由になります。(React.js、Vue.jsなどのフレームワークで書いてもいいし、ネイティブアプリにしてもいいし)
この記事ではフロントエンドにNext.jsを使ってAPI経由でmicroCMSから記事を取得することでブログサイトを作成します。

この記事のリポジトリ

https://github.com/sugayutokyo/nextjs_microcms_typescript
うまく行かない処理などありましたら、こちらをご覧ください!

環境構築

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

$ npx create-next-app . -e with-tailwindcss
  • 今回は TailwindCSS を使うためオプションwith-tailwindcssをつけます
  • ディレクトリ直下にプロジェクトを生成したいので、「.」を指定しています(ここはお好みで!)
$ npm run dev

下記画像のように Next.js のトップページが表示されれば OK です!

本記事では prettier を導入しています。導入したい方は以下の YAML ファイルを参考に設定してください!

.prettierrc.yml
printWidth: 80
tabWidth: 2
semi: true
singleQuote: true
quoteProps: as-needed
jsxSingleQuote: false
trailingComma: all
bracketSpacing: true
jsxBracketSameLine: true
arrowParens: avoid
endOfLine: lf

Next.js で実装

Header を作成する

$ mkdir components
$ touch ./components/Header.tsx
./components/Header.tsx
import Link from 'next/link'

export default function Header() {
  return (
    <header className="text-gray-600 body-font bg-blue-500">
      <div className="container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center">
        <Link href={'/'} passHref>
          <a className="flex title-font font-medium items-center text-gray-900 mb-4 md:mb-0">
            <span className="ml-3 text-xl text-white">My Blog</span>
          </a>
        </Link>
        <nav className="md:ml-auto flex flex-wrap items-center text-base justify-center">
          <a className="mr-5 text-white hover:text-gray-900">Profile</a>
        </nav>
      </div>
    </header>
  )
}
./pages/_app.tsx
import '../styles/globals.css';
import type { AppProps } from 'next/app';
+ import Header from '../components/Header';

function MyApp({ Component, pageProps }: AppProps) {
-  return <Component {...pageProps} />;
+  return (
+    <>
+      <Header />
+      <Component {...pageProps} />
+    </>
+  );
}

export default MyApp;

下記画像のようにヘッダーが表示されていれば OK です!

記事一覧ページを作成する(サンプル)

記事一覧ページにサンプルを一つ表示します
./pages/index.tsx を以下のように変更します(もともと書いてあるコードは全て消してしまって問題ありません!)

./pages/index.tsx
export default function Home() {
  return (
    <>
      <h1 className="container mx-auto px-10 pt-10 grid grid-cols-1 sm:grid-cols-1 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-3">
        記事一覧
      </h1>
      <div className="container mx-auto p-10 grid grid-cols-1 sm:grid-cols-1 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-3 gap-5">
        <div className="rounded overflow-hidden shadow-lg">
          <img
            className="w-full"
            src="https://i.gyazo.com/ad13e228541bbaf6952f447cce456dc2.jpg"
            alt="Sunset in the mountains"
          />
          <div className="px-6 py-4">テストタイトル</div>
          <div className="px-6 pt-4 pb-2">
            <span className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
              # テストタグ
            </span>
          </div>
        </div>
      </div>
    </>
  )
}

トップページが以下の画像のように変われば OK です!

microCMS から記事を作成する

microCMSのアカウントを下記URLから作成してください!

https://microcms.io/

サービスの作成

  1. サービス名、サービス ID を入力して「次へ」押下
    ※サービス ID は変更不可とのこと、後でサービス ID は必要となるのでメモをしておくと良いです

  2. サービス画像は今回用意しないので「スキップ」押下

  3. プランは「Hobby」を選択して「完了」押下

API の作成

  1. API 名、エンドポイントを入力して「次へ」押下

  2. 今回は記事を作成するので「リスト形式」を選択して「次へ」押下

  3. API スキーマ(インターフェース)は下記 json ファイルをダウンロードしてファイルインポートする
    api_schema_test.json
    「こちら」押下

    ダウンロードした json ファイルを選択して下記画像のように表示されることを確認します

コンテンツの作成

  1. 右上の「追加」押下してデータを作成する

  2. 公開したら下記コマンドで公開された API が正しく動作しているか確認する
    「API 設定」→「API リファレンス」→「試してみる」→「取得」




HTTP レスポンスが 200 で返ってきていれば OK です!

API を Next.js で実行する

  1. 環境変数を用意する
$ touch .env.local

microCMS のサイトに戻って「歯車」アイコン押下

「API キー」押下して copy ボタンを押下

コピーした API_KEY をペーストする

.env.local
API_KEY="X-MICROCMS-API-KEYをペーストする"
  1. microCMS の準備
    microcms-js-sdk をインストールする
$ npm install --save microcms-js-sdk

libs/client.js を作成して初期化する

$ mkdir libs
$ touch ./libs/client.ts
./libs/client.ts
import { createClient } from 'microcms-js-sdk'
export const client = createClient({
  serviceDomain: 'サービスIDを入力する',
  apiKey: process.env.API_KEY || '',
})

一覧ページを microCMS から取得する

  1. ./pages/index.js に getServerSideProps を実装して記事を取得する
./pages/index.tsx
+ import { client } from '../libs/client';

export default function Home() {
  return (
    ...省略
  );
}

+ export const getServerSideProps = async () => {
+   const data = await client.get({ endpoint: 'articles' });
+
+   return {
+     props: {
+       articles: data.contents,
+     },
+   };
+ };
  1. 取得した記事の情報をコンポーネントに渡す
    まずは型情報を共通化するために型情報のファイルを作成します
mkdir ./types
touch ./types/article.ts
./types/article.ts
export type Article = {
  id: string
  createdAt: string
  updatedAt: string
  publishedAt: string
  revisedAt: string
  title: string
  body: string
  eye_catch: {
    url: string
    height: number
    width: number
  }
  tag: string
}

型情報をインポートして使います

./pages/index.tsx
import { client } from '../libs/client';
+ import type { Article } from '../types/article';


+ type Props = {
+   articles: Array<Article>;
+ };

- export default function Home() {
+ export default function Home({ articles }: Props) {
  return (
    ...省略
  );
}

export const getServerSideProps = async () => {
  ...省略
};
  1. 取得した記事を表示する
./pages/index.tsx

...省略

export default function Home({ articles }: Props) {
  return (
    <>
      <h1 className="container mx-auto px-10 pt-10 grid grid-cols-1 sm:grid-cols-1 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-3">
        記事一覧
      </h1>
      <div className="container mx-auto p-10 grid grid-cols-1 sm:grid-cols-1 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-3 gap-5">
+       {articles.map(article => (
-         <div className="rounded overflow-hidden shadow-lg">
+         <div className="rounded overflow-hidden shadow-lg" key={article.id}>
            <img
              className="w-full"
-             src="https://i.gyazo.com/ad13e228541bbaf6952f447cce456dc2.jpg"
+             src={article.eye_catch.url}
              alt="Sunset in the mountains"
            />
-           <div className="px-6 py-4">テストタイトル</div>
+           <div className="px-6 py-4">{article.title}</div>
            <div className="px-6 pt-4 pb-2">
+           {article.tag && (
              <span className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
-               # テストタグ
+               #{article.tag}
              </span>
+           )}
            </div>
          </div>
+       ))}
      </div>
    </>
  );
}

export const getServerSideProps = async () => {
  ...省略
};

下記のように取得したデータに基づいて記事一覧が表示されればOKです!

詳細ページを作成する

$ mkdir ./pages/article
$ touch ./pages/article/\[id\].tsx

下記コードを記述

./pages/article/[id].tsx
import { GetServerSideProps } from 'next';
import type { Article } from '../../types/article';
import { client } from '../../libs/client';

type Props = {
  article: Article;
};

export default function Article({ article }: Props) {
  return (
    <div className="bg-gray-50">
      <div className="px-10 py-6 mx-auto">
        <div className="max-w-6xl px-10 py-6 mx-auto bg-gray-50">
          <img
            className="object-cover w-full shadow-sm h-full"
            src={article.eye_catch.url}
          />
          <div className="mt-2">
            <div className="sm:text-3xl md:text-3xl lg:text-3xl xl:text-4xl font-bold text-blue-500">
              {article.title}
            </div>
          </div>
          {article.tag && (
            <div className="flex items-center justify-start mt-4 mb-4">
              <div className="px-2 py-1 font-bold bg-red-400 text-white rounded-lg">
                #{article.tag}
              </div>
            </div>
          )}
          <div className="mt-2">
            <div className="text-2xl text-gray-700 mt-4 rounded ">
              {article.body}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = async ctx => {
  const id = ctx.params?.id;
  const idExceptArray = id instanceof Array ? id[0] : id;
  const data = await client.get({
    endpoint: 'articles',
    contentId: idExceptArray,
  });

  return {
    props: {
      article: data,
    },
  };
};

一覧ページから詳細ページに遷移できるようにする

./pages/index.tsx
import { client } from '../libs/client';
import type { Article } from '../types/article';
+ import Link from 'next/link';

type Props = {
  articles: Array<Article>;
};

export default function Home({ articles }: Props) {
  return (
    <>
      <h1 className="container mx-auto px-10 pt-10 grid grid-cols-1 sm:grid-cols-1 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-3">
        記事一覧
      </h1>
      <div className="container mx-auto p-10 grid grid-cols-1 sm:grid-cols-1 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-3 gap-5">
        {articles.map(article => (
          <div className="rounded overflow-hidden shadow-lg">
            <img
              className="w-full"
              src={article.eye_catch.url}
              alt="Sunset in the mountains"
            />
-           <div className="px-6 py-4">{article.title}</div>
+           <div className="px-6 py-4">
+             <Link href={`/article/${article.id}`} passHref>
+               <a>{article.title}</a>
+             </Link>
+           </div>
            <div className="px-6 pt-4 pb-2">
              ...省略
            </div>
          </div>
        ))}
      </div>
    </>
  );
}

export const getServerSideProps = async () => {
  ...省略
};

下記画像のようにタイトルを押下して記事詳細ページに遷移できたら実装完了です!

お疲れ様でした!!

参考

この記事は以下のサイトを参考に作成されています。

GitHubで編集を提案

Discussion

公開したら下記コマンドで公開された API が正しく動作しているか確認する
「API 設定」→「API リファレンス」→「試してみる」→「取得」

ここの箇所は画面右上の「APIプレビュー」というボタンから、直で試せたりします…!

コメントありがとうございます!
確認できました!こちらの方が簡単ですね!

はじめまして。コメント失礼致します。
記事を拝見したところ、誤記入が2箇所ほどあるっぽいので
ご確認していただきたくコメントを送らせていただきます。

1.「詳細ページを作成する」で、$ touch ./pages/article/\[id\].jsx とありますが、
実際に作成された[id].tsxでコードをペーストするとエラー発生します。
jsxだとTypeScriptがエラーで弾かれるので、拡張子は tsx になるのでは?と思います。
(語彙力なくてすみません)

2.「一覧ページから詳細ページに遷移できるようにする」の箇所で、
./pages/article/[id].tsx
とラベルに記載してありますが、内容を見ると
./pages/index.tsx
だと思われます。

ご確認いただきたく、よろしくお願い致します。

初めまして!コメントありがとうございます!
修正しましたのでご確認の程よろしくお願いいたします!

ログインするとコメントできます