NewtとNext.jsを利用してブログを作成する

このチュートリアルでは、Newtと Next.js を利用して、ブログを作成する手順を紹介します。
具体的には、Newtで管理しているコンテンツの一覧ページと詳細ページを作る手順を紹介します。

記事内で使用している主なソフトウェアのバージョン

  • Next.js(next): 13.3.0
  • newt-client-js(newt-client-js): 3.2.4

概要

Next.jsでプロジェクトを作成し、Newtのコンテンツ情報を取得できるようにします。
コンテンツの一覧ページ(パス: /)と詳細ページ(パス: /articles/:slug 。slugがarticle-1の場合は /articles/article-1)を作成し、ローカル環境で表示を行うまでを説明します。
また、ここではビルド時にHTMLを生成する、静的生成(Static Generation)の方法を紹介します。

1. Next.jsのセットアップ

はじめに、Next.jsのセットアップを行います。Create Next App を利用することで、簡単にNext.jsのプロジェクトを作成できます。
以下のどちらかのコマンドを入力します。

npx create-next-app@latest
# or
yarn create next-app

コマンドを入力すると、以下の質問を聞かれるので、お好きな設定を選びましょう。

  • プロジェクト名(ここでは nextjs-blog としました)
  • TypeScriptを利用するか(ここでは Yes を選択)
  • ESLintを利用するか(ここでは Yes を選択)
  • Tailwind CSSを利用するか(ここでは No を選択)
  • src/ ディレクトリを使用するか(ここでは No を選択)
  • app/ ディレクトリを使用するか(ここでは No を選択)
  • インポートエイリアスの設定をどうするか(ここでは @/* を選択)

yarnを利用した場合、以下のように表示されます。

$  yarn create next-app
yarn create v1.22.19
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Installed "create-next-app@13.3.0" with binaries:
      - create-next-app
✔ What is your project named? … nextjs-blog
✔ Would you like to use TypeScript with this project? … Yes
✔ Would you like to use ESLint with this project? … Yes
✔ Would you like to use Tailwind CSS with this project? … No
✔ Would you like to use `src/` directory with this project? … No
✔ Would you like to use experimental `app/` directory with this project? … No
✔ What import alias would you like configured? … @/*
Creating a new Next.js app in /Users/hoge/fuga/nextjs-blog.

作成したプロジェクトに移動して、開発サーバーを立ち上げます。

$ cd nextjs-blog
$ yarn dev

http://localhost:3000 にアクセスして、以下のような画面が表示されることを確認します。

nextjs-blog1.png

2. Newtのセットアップ

次にNewtにコンテンツとAPIトークンを用意し、コンテンツの取得を行うための準備を行います。

2-1. Appを追加する

「Appを追加」をクリックして「テンプレートから追加」を選択します。

Appを追加する

表示されるテンプレートの中から「Blog」を選択して、「このテンプレートを追加」をクリックします。
Appテンプレート

テンプレートが追加されると、「投稿データ」「カテゴリデータ」「著者データ」が追加されます。

quick-start03.jpg

2-2. スペースUID・App UID・モデルUIDを確認する

スペースUIDは「スペース設定」から確認できます。

quick-start0402.jpgquick-start0503.jpg

上記の例だと、スペースUIDは sample-for-docs となります。
この値は3-1で環境変数として定義します。

また「Blog」テンプレートを追加した場合、App UIDは blog、「投稿データ」モデルUIDは article となります。
これらの値は、4-3や5-1で投稿情報を取得する際に利用します。

2-3. Newt CDN API Tokenを作成する

続いて、APIリクエストに必要なトークンを発行します。
スペース設定 > APIキー のページからNewt CDN API Tokenを作成します。

quick-start06.jpg

名前と取得対象を決めて「作成」を押します。

quick-start07.jpg

ここで作成したトークンの値は3-1で環境変数として定義します。

3. リクエストの準備

Newtの SDK を利用することで、NewtのAPIをより簡単に利用できます。
ここではSDKを利用して、NewtのAPIクライアントを作成します。

3-1. 環境変数の設定

Next.jsには環境変数のビルトインサポートがあり、.env.local を使用して、環境変数をロードできます。Next.jsの環境変数について、詳細は Environment Variables のドキュメントをご確認ください。

ここでは、.env.local ファイルを作成し、2-2で確認したスペースUID、2-3で作成したトークンの値を定義します。以下を、実際の値で置き換えて定義してください。

NEWT_SPACE_UID=sample-for-docs
NEWT_CDN_API_TOKEN=xxxxxxxxxxxxxxx

上記のように定義しておくと、process.env.NEWT_SPACE_UIDprocess.env.NEWT_CDN_API_TOKEN として利用できるようになります。

3-2. newt-client-jsのインストール

次に newt-client-js をインストールします。

npm install newt-client-js
# or
yarn add newt-client-js

3-3. APIクライアントの作成

CDN APIを利用するためのクライアントを作成します。
spaceUidtoken のところには3-1で設定した環境変数を入力します。
ここではCDN APIを利用するので、apiType には cdn を指定しましょう。

// lib/newt.ts

import { createClient } from 'newt-client-js'

const client = createClient({
  spaceUid: process.env.NEWT_SPACE_UID + '',
  token: process.env.NEWT_CDN_API_TOKEN + '',
  apiType: 'cdn',
})

これで、NewtにAPIリクエストを送るための準備ができました。

4. 一覧ページの作成

4-1. 言語の設定をする

pages/_document.tsx にある lang 属性を修正します。ここでは日本語 ja を指定します。

import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
  return (
-   <Html lang="en">
+   <Html lang="ja">
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

4-2. 投稿の型を定義する

投稿の型 Article を定義します。
このチュートリアルでは、_idtitleslugbody のみを使うため、以下のように定義しておきます。

// types/article.ts

export interface Article {
  _id: string
  title: string
  slug: string
  body: string
}

4-3. 投稿一覧の取得メソッドを作成する

投稿一覧を取得するために、lib/newt.ts のファイルに getArticles メソッドを追加します。
SDKが提供している getContents メソッドはNewtのコンテンツ一覧を取得するためのメソッドです。getContentsのパラメータに Article の型を渡すことで、返却される items の型として Article[] が指定されます。
また、selectパラメータを利用して、取得するフィールドを _idtitleslugbody のみに制限します。

import { createClient } from 'newt-client-js'
+ import type { Article } from '@/types/article'

const client = createClient({
  spaceUid: process.env.NEWT_SPACE_UID + '',
  token: process.env.NEWT_CDN_API_TOKEN + '',
  apiType: 'cdn',
})

+ export const getArticles = async () => {
+   const { items } = await client.getContents<Article>({
+     appUid: 'blog',
+     modelUid: 'article',
+     query: {
+       select: ['_id', 'title', 'slug', 'body'],
+     },
+   })
+   return items
+ }

1でインポートエイリアスに @/* を指定したので、Article のパスに @/types/article と指定しています。

4-4. 投稿一覧を表示する

Next.jsではファイルシステムベースのルーティングを採用しており、pages ディレクトリの配下にファイルを作成すると、自動的にルートとして利用できるようになります。
例えば、以下のようにルーティングされます。

  • pages/blog/index.ts/blog
  • pages/blog/first-post.ts/blog/first-post

※ ルーティングの詳細については、Next.jsの Routing のドキュメントをご確認ください。

ここではトップページ(パス: /)で投稿一覧を表示したいので、pages/index.tsx のファイルを修正します。
4-3で作成した getArticles を利用して、以下のように書き換えます。

// pages/index.tsx

import Head from 'next/head'
import Link from 'next/link'
import styles from '@/styles/Home.module.css'
import { getArticles } from '@/lib/newt'
import type { Article } from '@/types/article'

export default function Home({ articles }: { articles: Article[] }) {
  return (
    <>
      <Head>
        <title>Newt・Next.jsブログ</title>
        <meta name="description" content="NewtとNext.jsを利用したブログです" />
      </Head>
      <main className={styles.main}>
        <ul>
          {articles.map((article) => {
            return (
              <li key={article._id}>
                <Link href={`articles/${article.slug}`}>{article.title}</Link>
              </li>
            )
          })}
        </ul>
      </main>
    </>
  )
}

export const getStaticProps = async () => {
  const articles = await getArticles()
  return {
    props: {
      articles,
    },
  }
}

ここでは getStaticProps という関数をエクスポートしています。これにより、Next.jsはビルド時に getStaticProps から返されるpropsを使って事前レンダリングを行います。静的生成でサイトを作成する場合は、この関数を使います。
今回の場合は、propsとして articles を渡し、一覧ページで全投稿のタイトルを表示するようにしています。

http://localhost:3000/ にアクセスして、以下のように投稿一覧が表示されれば成功です。

nextjs-blog2.png

5. 詳細ページの作成

5-1. 投稿詳細の取得メソッドを作成する

投稿詳細を取得するために、lib/newt.ts のファイルに getArticleBySlug メソッドを追加します。
SDKが提供している getFirstContent メソッドはクエリに該当するコンテンツのうち、最初の1件を返却するメソッドです。指定したスラッグのコンテンツを取得したい場合は、このメソッドを利用します。

import { createClient } from 'newt-client-js'
import type { Article } from '@/types/article'

const client = createClient({
  spaceUid: process.env.NEWT_SPACE_UID + '',
  token: process.env.NEWT_CDN_API_TOKEN + '',
  apiType: 'cdn',
})

export const getArticles = async () => {
  const { items } = await client.getContents<Article>({
    appUid: 'blog',
    modelUid: 'article',
    query: {
      select: ['_id', 'title', 'slug', 'body'],
    },
  })
  return items
}

+ export const getArticleBySlug = async (slug: string) => {
+   const article = await client.getFirstContent<Article>({
+     appUid: 'blog',
+     modelUid: 'article',
+     query: {
+       slug,
+       select: ['_id', 'title', 'slug', 'body'],
+     },
+   })
+   return article
+ }

5-2. 投稿詳細を表示する

Next.jsでは [param] のようにしてページ名に角括弧を使うことで動的なルーティング(Dynamic Routes)を作成できます。ここでは、/articles/:slug/articles/article-1 など)のパスで投稿の詳細を表示したいので、pages/articles/[slug].tsx のファイルを作成します。

5-1で作成した getArticleBySlug を利用して、以下のように記載します。

// pages/articles/[slug].tsx

import Head from 'next/head'
import styles from '@/styles/Home.module.css'
import { getArticles, getArticleBySlug } from '@/lib/newt'
import type { Article } from '@/types/article'

export default function Article({ article }: { article: Article }) {
  return (
    <>
      <Head>
        <title>{article.title}</title>
        <meta name="description" content="投稿詳細ページです" />
      </Head>
      <main className={styles.main}>
        <h1>{article.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: article.body }} />
      </main>
    </>
  )
}

export const getStaticPaths = async () => {
  const articles = await getArticles()
  return {
    paths: articles.map((article) => ({
      params: {
        slug: article.slug,
      },
    })),
    fallback: false,
  }
}

export const getStaticProps = async ({
  params,
}: {
  params: { slug: string }
}) => {
  const { slug } = params
  const article = await getArticleBySlug(slug)
  return {
    props: {
      article,
    },
  }
}

ここでは投稿一覧のページでも利用した getStaticProps に加えて、getStaticPaths の関数をエクスポートしています。getStaticPaths はNext.jsで動的なルーティングを使う場合に利用する関数で、静的に生成するパスのリストを定義しておきます。
今回の場合は、全投稿のスラッグを定義しておきます。

また、fallback のパラメータで、getStaticPathsで定義していないパスでアクセスされた場合の挙動を設定します。ここでは、定義されていないパスでアクセスされた場合は、404ページを返すものとし、false を設定しています。fallbackパラメータについての詳細は、Next.jsの getStaticPaths のドキュメントをご確認ください。

※ bodyの表示で利用されている dangerouslySetInnerHTML はXSSの危険性があるため、利用には注意が必要です。ここでは、Newtで管理している投稿情報を表示するものであり、不特定多数のユーザーが入力できるものを表示するわけではないため、安全なものとして利用しています。

これで、投稿詳細についての設定も完了です。
http://localhost:3000/articles/article-3 にアクセスして、以下のように投稿詳細が表示されれば成功です。

nextjs-blog4.png

次のステップ

このチュートリアルを行うことで、Next.jsのプロジェクトを作成し、開発環境でコンテンツの取得・表示を行う方法を学習しました。
更に深く学習したい方は、以下のチュートリアルもおすすめです。

Vercelと接続してホスティングを行いたい方
GitHubのリポジトリとVercelを接続して、ホスティングする

プレビュー環境を作成したい方
Next.jsのプレビューモードを利用して、プレビュー環境を作成する

問い合わせフォームを作成したい方
NewtとNext.jsを利用して、問い合わせフォームを作成する

その他にも様々なチュートリアルを用意しているので、ぜひ チュートリアル のページもご確認ください。

Newt Made in Newt