🌋

Next.js と Spearly でブログを作る【鹿児島.mk】

2023/09/12に公開

はじめに

この記事は、Next.js ミートアップ【鹿児島.mk】 で行う内容をまとめたものです。

イベントに参加していない人も、この記事を読んで Next.js と Spearly でブログを作ってみてください。

注: 本記事のマークアップは ChatGPT によって作成されました。デザインやスタイルにこだわりのある方は、お好みに合わせて調整してください。

https://kagoshima-mk.connpass.com/event/292081/

事前準備

  • Node 18.17 以上 の環境を準備します
  • Spearly のアカウントを作成します
    • この記事の内容を試す場合は、無料プランで十分です

https://spearly.com/ja/cms/

Next.js のセットアップ

公式ドキュメント に従い、Next.js をインストールします。

npx create-next-app@latest

今回のプロジェクトは以下のような設定で実施しました。

✔ What is your project named? … mk-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? … Yes
✔ 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? … @

☕️ ここで一息

Spearly の設定

Spearly はサイトのコンテンツ管理ができる俗に言う「ヘッドレスCMS」です。簡単に「Google オプティマイズ」みたいなこともできるのですが、今回の趣旨とは外れるので説明は割愛します。

まずは、Spearly で管理しているコンテンツを Next.js で表示するために、Spearly の SDK をインストールします。
Spearly SDK を利用することで効率的にコンテンツの取得が可能になります。

npm install -S @spearly/sdk-js

https://www.npmjs.com/package/@spearly/sdk-js

Spearly のアカウントを作成したら以下のようなサンプル記事が表示されているはずです。今回はこのデータを使ってブログを作成していきます。

次に「チーム設定」→「APIキー」からAPIキーを確認してコピーします。

コピーしたAPIは .env.local に保存しておきます。

.env.local
API_KEY=xxxxxxxxxx

☕️ ここで一息

各画面の作成

ホーム画面

ホーム画面では、Spearly で管理しているコンテンツの一覧を表示します。

src/app/page.tsx
import styles from './page.module.css'
import {Content, SpearlyApiClient} from "@spearly/sdk-js";
import Link from "next/link";

const apiClient = new SpearlyApiClient(process.env.API_KEY as string)

export default async function Home() {
  const contents = await apiClient.getList("sample-post")

  return (
    <main className={styles.main}>
      <article>
        <ul>
          {
            (contents.data).map((content: Content) => {
              const values = content.values as any;
              const path = `/blog/${content.attributes.publicUid}`

              return (
                <li className={styles.articleList} key={content.id}>
                  <Link href={path}>
                      {values.title}
                  </Link>
                </li>
              )
            })
          }
        </ul>
      </article>
    </main>
  )
}
src/app/layout.tsx
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
+  title: 'Next.js ミートアップ【鹿児島.mk】 サンプルブログ',
+  description: '2023-09-15 のNext.js ミートアップ【鹿児島.mk】で作成したサンプルブログです。',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
      {children}
      </body>
    </html>
  )
}
src/app/page.module.css
.articleList {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: 2rem;
    background-color: #f5f5f5;
    border-radius: 8px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    margin-bottom: 20px;
}

.articleList a {
    display: block;
    padding: 1rem;
    background-color: #ffffff;
    border-radius: 6px;
    transition: background-color 0.3s;
    text-decoration: none;
    color: #333;
    font-weight: 500;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.articleList a:hover {
    background-color: #eaeaea;
}

☕️ ここで一息

記事詳細画面

記事の詳細画面では、選択された記事の内容を表示します。dangerouslySetInnerHTML を使用していますが、XSS攻撃のリスクがあるため、信頼できるコンテンツのみを表示するようにしてください。

src/app/blog/[id]/page.tsx
import styles from '../../page.module.css'
import {Content, SpearlyApiClient} from "@spearly/sdk-js"

const apiClient = new SpearlyApiClient(process.env.API_KEY as string)

export default async function Page({ params }: { params: { id: string } }) {
  const content = await apiClient.getContent("sample-post", params.id)
  const values= content.values as any;

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


export const generateStaticParams = async () => {
  const contents = await apiClient.getList("sample-post")

  return contents.data.map((column) => {
    return {
      id: column.attributes.publicUid
    }
  })
}

☕️ ここで一息

https://gizumo-inc.jp/media/next-app-router/

コンポーネントの作成

ヘッダーとフッターをコンポーネント化して、各画面で利用できるようにします。
React Server Components と Client Components の違いを知るためにわざと分けています。

React Server Components なヘッダー

src/app/page.module.css

.header {
    background-color: #333;
    color: #fff;
    padding: 15px 0;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.header h1 {
    text-align: center;
    font-size: 18px;
    font-weight: bold;
}


.footer {
    background-color: #2a2a2a;
    color: #b3b3b3;
    padding: 10px 0;
    text-align: center;
    font-size: 14px;
    border-top: 1px solid #444;
}

src/app/components/MkHeader.tsx
import styles from '@/app/page.module.css'
import Link from 'next/link'

export const MkHeader = () => {
  console.log('MkHeader')

  return (
    <header className={styles.header}>
      <Link href="/">
        <p>Next.js ミートアップ【鹿児島.mk】 サンプルブログ</p>
      </Link>
    </header>
  )
}

Client Components なフッター

src/app/components/MkFooter.tsx
'use client';

import styles from '@/app/page.module.css'

export const MkFooter = () => {
  console.log('MkFooter')

  return (
    <footer className={styles.footer}>
      <p>©︎ 2023 鹿児島.mk</p>
    </footer>
  )
}

作成したコンポーネントを index.ts でまとめておきます。

src/app/components/index.ts
export * from "./MkHeader"
export * from "./MkFooter"

各画面でコンポーネントを利用するように修正します。

src/app/layout.tsx
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
+ import {MkFooter, MkHeader} from "@/app/components";

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Next.js ミートアップ【鹿児島.mk】 サンプルブログ',
  description: '2023-09-15 のNext.js ミートアップ【鹿児島.mk】で作成したサンプルブログです。',
}

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

☕️ ここで一息

https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#when-to-use-server-and-client-components

デプロイ

Next.js で開発しているので、 Firebase Hosting でも Vercel でもなんでもいいですが、今回は Spearly CLOUD でデプロイします。

こちらも無料プランで十分です。

https://cloud.spearly.com/

以下のドキュメントに沿って、 Next.js 用のリポジトリと Spearly CLOUD のプロジェクトを作成してください。

https://docs.spearly.com/en/cloud/withCms/c-2FlLcUYzu5Vi1o4Cd7SE#content-1

記事の通りに Next.js を動かしている場合には、 Spearly CLOUD のプロジェクトに以下のような設定が必要です。

SSG でデプロイするので、以下の設定を加えておきましょう。

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "export",
}

module.exports = nextConfig

基本設定

ビルドコマンド

npm run build

ルートパス

out

環境変数

  • API_KEY に Spearly の APIキーを設定します
  • NODE_VERSION に 現在利用中の Node のバージョンを設定します

そうしてデプロイすることで、このハンズオンで作成したブログが全世界に公開されます。

https://snowy-sun-9627.spearly.app

実際のコードも公開しているので、参考にしてみてください。

https://github.com/qst-exe/mk-blog-sample

☕️ ここで一息

おまけ:A/Bテスト機能を追加してみる

A/Bテスト機能を利用することで、どっちのタイトルの方がウケたかを調べることができるので、組み込んでみましょう。

https://www.npmjs.com/package/@spearly/sdk-js#:~:text=A/B Testing analytics

Discussion