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

最終更新日:

Table of contents

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

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

  • Next.js(next): 14.2.3
  • newt-client-js(newt-client-js): 3.3.2

概要

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

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 Routerを使用するか(ここでは Yes を選択)
  • デフォルトのインポートエイリアスの設定をカスタマイズするか(ここでは Yes を選択)
  • インポートエイリアスの設定をどうするか(ここでは @/* を選択)

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@14.2.3" with binaries:
      - create-next-app
✔ What is your project named? … nextjs-blog
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … No
✔ Would you like to use `src/` directory? … No
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias (@/*)? … Yes
✔ What import alias would you like configured? … @/*
Creating a new Next.js app in /Users/foo/bar/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.jpg
quick-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で作成したトークンの値を定義します。以下を、実際の値で置き換えて定義してください。

.env.local
1NEWT_SPACE_UID=sample-for-docs
2NEWT_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
1import { createClient } from 'newt-client-js'
2
3const client = createClient({
4  spaceUid: process.env.NEWT_SPACE_UID + '',
5  token: process.env.NEWT_CDN_API_TOKEN + '',
6  apiType: 'cdn',
7})

3-4. サーバー上でのみ実行されるよう制限する

このAPIクライアントはサーバー上でのみ実行することを想定しています。
そこで、server-only を利用して、誤ってクライアントコンポーネントにインポートした場合、エラーを発生させるようにします。
詳細はNext.jsの server-only のドキュメントをご確認ください。

まずインストールします。

npm install server-only
# or
yarn add server-only

次に lib/newt.ts のコードを修正します。

lib/newt.ts
import 'server-only'
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. 言語の設定をする

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

app/layout.tsx
(省略)

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
    <html lang="ja">
      <body className={inter.className}>{children}</body>
    </html>
  )
}

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

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

types/article.ts
1export interface Article {
2  _id: string
3  title: string
4  slug: string
5  body: string
6}

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

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

lib/newt.ts
import 'server-only'
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 と指定しています。

また、余計なリクエストを送らないように、Reactの cache() でラップしておきます。
詳細はNext.jsの React cache() のドキュメントをご確認ください。

lib/newt.ts
import 'server-only'
import { createClient } from 'newt-client-js'
import { cache } from 'react'
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 () => {
export const getArticles = cache(async () => {
  const { items } = await client.getContents<Article>({
    appUid: 'blog',
    modelUid: 'article',
    query: {
      select: ['_id', 'title', 'slug', 'body'],
    },
  })
  return items
}
})

※ NewtのCDN APIでは認証に Authorization ヘッダを利用していますが、Next.jsの fetch()Authorization ヘッダを利用する場合はキャッシュされないため、ここではReactの cache() を利用します。

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

Next.jsではファイルシステムベースのルーティングを採用しており、app ディレクトリの配下にフォルダを作成して、ルートを定義します。そしてpageファイルを作成することで、ルートを公開します。
例えば、以下のようにpageファイルとURLが対応します。

  • app/dashboard/page.tsx/dashboard
  • app/dashboard/settings/page.tsx/dashboard/settings

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

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

app/page.tsx
1import Link from 'next/link'
2import { getArticles } from '@/lib/newt'
3import styles from '@/app/page.module.css'
4import type { Metadata } from 'next'
5
6export const metadata: Metadata = {
7  title: 'Newt・Next.jsブログ',
8  description: 'NewtとNext.jsを利用したブログです',
9}
10
11export default async function Home() {
12  const articles = await getArticles()
13  return (
14    <main className={styles.main}>
15      <ul>
16        {articles.map((article) => {
17          return (
18            <li key={article._id}>
19              <Link href={`articles/${article.slug}`}>{article.title}</Link>
20            </li>
21          )
22        })}
23      </ul>
24    </main>
25  )
26}

ここでは、getArticles で取得した投稿一覧を表示しています。
また、メタデータを設定するために metadata をエクスポートしています。詳細についてはNext.jsの Metadata のドキュメントをご確認ください。

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

nextjs-blog2.png

5. 詳細ページの作成

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

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

lib/newt.ts
import 'server-only'
import { cache } from 'react'
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 = cache(async () => {
  const { items } = await client.getContents<Article>({
    appUid: 'blog',
    modelUid: 'article',
    query: {
      select: ['_id', 'title', 'slug', 'body'],
    },
  })
  return items
})

export const getArticleBySlug = cache(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 など)のパスで投稿の詳細を表示したいので、app/articles/[slug]/page.tsx のファイルを作成します。

以下のように記載します。

app/articles/[slug]/page.tsx
1import { getArticles, getArticleBySlug } from '@/lib/newt'
2import styles from '@/app/page.module.css'
3import type { Metadata } from 'next'
4import type { Article } from '@/types/article'
5
6type Props = {
7  params: {
8    slug: string
9  }
10}
11
12export async function generateStaticParams() {
13  const articles = await getArticles()
14  return articles.map((article) => ({
15    slug: article.slug,
16  }))
17}
18export const dynamicParams = false
19
20export async function generateMetadata({ params }: Props): Promise<Metadata> {
21  const { slug } = params
22  const article = await getArticleBySlug(slug)
23
24  return {
25    title: article?.title,
26    description: '投稿詳細ページです',
27  }
28}
29
30export default async function Article({ params }: Props) {
31  const { slug } = params
32  const article = await getArticleBySlug(slug)
33  if (!article) return
34
35  return (
36    <main className={styles.main}>
37      <h1>{article.title}</h1>
38      <div dangerouslySetInnerHTML={{ __html: article.body }} />
39    </main>
40  )
41}

以下のことを行っています。

  • generateStaticParams を利用して、リクエスト時に動的にルートを生成するのではなく、ビルド時に静的にルートを生成しています。今回の場合は、全投稿のスラッグを定義します
  • dynamicParamsfalse に設定して、もし generateStaticParams で定義されていないパスにアクセスされた場合、404 を返すようにします
  • 投稿一覧と異なり、ここではメタデータを動的に生成しています。そのため generateMetadata を利用して、titledescription を設定しています
  • getArticleBySlug で取得した投稿から、詳細ページを表示しています

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

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

nextjs-blog4.png

次のステップ

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

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

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

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

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

NewtMade in Newt