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

最終更新日:

Table of contents

  1. 記事内で使用している主なソフトウェアのバージョン
  2. 概要
  3. 1. Next.jsのセットアップ
  4. 2. Newtのセットアップ
  5. 2-1. Appを追加する
  6. 2-2. スペースUID・App UID・モデルUIDを確認する
  7. 2-3. Newt CDN API Tokenを作成する
  8. 3. リクエストの準備
  9. 3-1. 環境変数の設定
  10. 3-2. newt-client-jsのインストール
  11. 3-3. APIクライアントの作成
  12. 3-4. サーバー上でのみ実行されるよう制限する
  13. 4. 一覧ページの作成
  14. 4-1. 言語の設定をする
  15. 4-2. 投稿の型を定義する
  16. 4-3. 投稿一覧の取得メソッドを作成する
  17. 4-4. 投稿一覧を表示する
  18. 5. 詳細ページの作成
  19. 5-1. 投稿詳細の取得メソッドを作成する
  20. 5-2. 投稿詳細を表示する
  21. 次のステップ

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

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

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

概要

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@13.4.6" 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
✔ 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で作成したトークンの値を定義します。以下を、実際の値で置き換えて定義してください。

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',
})

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

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

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

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

次に 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 を指定します。

import './globals.css'
import { Inter } from 'next/font/google'

(省略)

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

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 '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() のドキュメントをご確認ください。

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 = 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

import Link from 'next/link'
import { getArticles } from '@/lib/newt'
import styles from '@/app/page.module.css'
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Newt・Next.jsブログ',
  description: 'NewtとNext.jsを利用したブログです',
}

export default async function Home() {
  const articles = await getArticles()
  return (
    <main className={styles.main}>
      <ul>
        {articles.map((article) => {
          return (
            <li key={article._id}>
              <Link href={`articles/${article.slug}`}>{article.title}</Link>
            </li>
          )
        })}
      </ul>
    </main>
  )
}

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

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

nextjs-blog2.png

5. 詳細ページの作成

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

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

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

import { getArticles, getArticleBySlug } from '@/lib/newt'
import styles from '@/app/page.module.css'
import type { Metadata } from 'next'
import type { Article } from '@/types/article'

type Props = {
  params: {
    slug: string
  }
}

export async function generateStaticParams() {
  const articles = await getArticles()
  return articles.map((article) => ({
    slug: article.slug,
  }))
}
export const dynamicParams = false

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = params
  const article = await getArticleBySlug(slug)

  return {
    title: article?.title,
    description: '投稿詳細ページです',
  }
}

export default async function Article({ params }: Props) {
  const { slug } = params
  const article = await getArticleBySlug(slug)
  if (!article) return

  return (
    <main className={styles.main}>
      <h1>{article.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.body }} />
    </main>
  )
}

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

  • 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