microCMS

Next.jsとAuth0で会員制メディアを作る【2. ページ作成編】

柴田 和祈

この記事は公開後、1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、柴田です。

「会員制メディア」のチュートリアルを全3回に分けてお届けしております。
今回は第2回のページ作成編です。

===

  1. 認証編
  2. ページ作成編
  3. 完成編

===

前回は認証ができるところまで実装しました。
今回はmicroCMSを連携し、公開用と会員向けコンテンツをそれぞれ表示させていきます。

前回と繰り返しとなりますが、仕様は以下の通りです。

  • 記事一覧画面と全公開記事(/public配下)は事前生成をしておき、静的に配信する
  • 会員向け記事(/private配下)はログイン済みユーザーのみ閲覧可能とし、SSRで配信する


1. microCMSの用意

コンテンツはmicroCMSにて管理していきます。

手順は下記に用意しました。
詳細については、microCMSドキュメントを参照してください。

アカウント登録
ログイン
サービスの作成
APIの作成


それでは、記事用のAPIを作成していきます。
API名に記事、エンドポイントにarticlesを入力してください。

そして、リスト形式を選択します。

次にAPIスキーマの設定画面です。
下記項目を用意します。

コンテンツの作成

適当に内容を入力し、公開します。
コンテンツ一覧画面に戻り、画面右上のAPIプレビューをクリックします。
取得ボタンをクリックし、入力内容がAPI経由で取得できるか確認してください(レスポンスJSONが表示されます)。

2. APIキーをenvファイルで保護

microCMSではリクエストにAPIキーを含める事で特定のデータを取得できます。
このAPIキーをGitHubのパブリックで公開してしまうとセキュリティ面でよろしくないので、envファイルなどで保護してあげましょう。

前回、Auth0の設定時に作成した.env.localファイルにmicroCMSの情報も追記します。

API_KEY=xxxxxxxxxxxx
SERVICE_ID=xxxxxxxxxxxx

そうすると、プロジェクト内でAPIキーとサービスIDを参照することができるようになります。

process.env.API_KEY
process.env.SERVICE_ID

Next.jsのenvファイルの取り扱いはドキュメントを参照してください。
https://nextjs.org/docs/basic-features/environment-variables

3. microcms-js-sdkの準備

公式で提供しているmicrocms-js-sdkをインストールしましょう。
microcms-js-sdkはオープンソースで公開されています。
https://github.com/microcmsio/microcms-js-sdk

$ npm install microcms-js-sdk

そして、libsフォルダ -> client.tsを作成してSDKの初期化を行います。

service-domainapi-Keyを設定してください。
serviceDomainXXXX.microcms.ioの場合、XXXXの部分になります。
apiKeyは環境変数を参照してください。

// libs/client.ts
import { createClient } from 'microcms-js-sdk';

export const client = createClient({
  serviceDomain: process.env.SERVICE_ID || '',
  apiKey: process.env.API_KEY || '',
});


4. 型定義の用意

後々使うことになるので、先に型定義ファイルを用意しておきます。
ルート直下にtypesディレクトリを作成し、その中にindex.tsファイルを配置します。

// types/index.ts

import {
  MicroCMSListResponse,
  MicroCMSImage,
  MicroCMSListContent,
} from 'microcms-js-sdk';

export type Article = {
  title?: string;
  body?: string;
  thumbnail?: MicroCMSImage;
  private: boolean;
};

export type ArticleList = MicroCMSListResponse<Article>;
export type ArticleListDetail = Article & MicroCMSListContent;

Article型はmicroCMSで用意したAPIスキーマを元に定義しています。
また、microcms-js-sdkにはいくつか型定義が用意されているので、それらを利用して記事一覧、記事詳細用の型を定義しています。

5. ルーティングとレンダリング

Next.jsでは、pages/以下に作成したファイルに基づいて自動的にルーティングされる仕組みになっています。

今回は下記の構成で進めていきます。

  • pages/index.tsx : 記事一覧
  • pages/public/[id].tsx : 公開記事
  • pages/private/[id].tsx : 会員向け記事


また、記事一覧と公開記事に関してはSG(Static Generation)会員向け記事はSSR(Server Side Rendering)形式で作成していきます。
それぞれの違いを詳しく知りたい方は下記の記事をご覧ください。
https://blog.microcms.io/nextjs-sg-ssr/

6. 記事一覧画面の作成

それでは、実際にmicroCMSのデータを取得したページを作成してみましょう。
まずはpages/index.tsxを記事一覧画面として進めていきます。

記事一覧画面は公開記事と会員向け記事が入り混じって表示される形を想定しており、SG(Static Generation)形式でレンダリングを行います。
ただし、会員向けの記事も静的生成してしまうとその内容をソースコードから読み取ることが出来てしまいます

そこで、会員向けの記事においても本文以外の下記情報は公開情報として扱って良いものとして進めていきます。

  • タイトル
  • サムネイル画像
  • 公開日時
  • 会員向けか否か


すべての情報公開を許さない場合は一覧ページを公開用と会員用で分けるか、SSRで作成していく形になります。

それではpages/index.tsxを次のように書き換えましょう。

// pages/index.tsx

import type { NextPage, GetStaticProps } from 'next';
import Link from 'next/link';
import { client } from '../libs/client';
import type { Article, ArticleList } from '../types';

export const getStaticProps: GetStaticProps = async () => {
  const data = await client.getList<Article>({
    endpoint: 'articles',
    queries: {
      fields: ['id', 'private', 'title'],
    },
  });

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

type Props = {
  data: ArticleList;
};

const Index: NextPage<Props> = ({ data }) => {
  const { contents } = data;
  return (
    <div>
      <ul>
        {contents.map((content) => (
          <li key={content.id}>
            <Link href={`/${content.private ? 'private' : 'public'}/${content.id}`}>
              <a>{content.title}</a>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default Index;


microCMSから記事を公開用・会員用ともに複数入稿してみて、ちゃんと一覧画面が表示されていれば成功です。
また、それぞれのリンク先はpublic/xxxxxxprivate/xxxxxxとなっていることを確認してみてください。

7. 記事コンポーネントの作成

次に記事詳細ページを作っていきます。
公開用記事と会員用記事は別ページとして用意するのですが、表示する内容は共通化してしまっても良いと思うので、記事表示用のコンポーネントを作成していきます。

// components/Article.tsx

import { FC } from 'react';
import Image from 'next/image';
import type { ArticleListDetail } from '../types';

type Props = {
  data: ArticleListDetail;
};

const Article: FC<Props> = ({ data }) => {
  const { thumbnail, title, body, publishedAt } = data;
  return (
    <main>
      {thumbnail !== undefined && (
        <Image
          src={thumbnail.url}
          width={thumbnail.width}
          height={thumbnail.height}
          alt=""
        />
      )}
      <h1>{title}</h1>
      <p>{publishedAt}</p>
      <div
        dangerouslySetInnerHTML={{
          __html: body || '',
        }}
      />
    </main>
  );
};

export default Article;


dangerouslySetInnerHTMLはXSSを引き起こす可能性があるため非推奨とされています。
今回の構成においては、入力はmicroCMSのリッチエディタからのみであり、ユーザーからの自由入力箇所ではないため安全であるとして利用しています。

また、ImageとしてmicroCMSから入稿した画像を扱うために、next.config.jsに画像ドメインを追記します。

// next.config.js

const nextConfig = {
  reactStrictMode: true,
  images: {
    domains: ['images.microcms-assets.io']
  }
}

module.exports = nextConfig


8. 公開用記事ページの作成

いよいよ公開用記事ページです。
/pages/public/[id].tsxというファイルを用意します。

こちらのページは記事一覧ページと同様、SG形式でレンダリングを行います。
公開用なので、静的ファイルとしてデプロイし、誰からでも高速に閲覧できるようにします。

import type { NextPage, GetStaticProps } from 'next';
import { client } from '../../libs/client';
import Article from '../../components/Article';
import type { Article as ArticleType, ArticleListDetail } from '../../types';

export const getStaticPaths = async () => {
  const data = await client.getList<ArticleType>({
    endpoint: 'articles',
    queries: {
      filters: 'private[equals]false',
    },
  });

  const paths = data.contents.map((content) => `/public/${content.id}`);
  return { paths, fallback: false };
};

export const getStaticProps: GetStaticProps = async (context) => {
  const id = context?.params?.id as string;
  const data = await client.getListDetail<ArticleType>({
    endpoint: 'articles',
    contentId: id,
  });

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

type Props = {
  data: ArticleListDetail;
};

const PublicId: NextPage<Props> = ({ data }) => {
  return <Article data={data} />;
};

export default PublicId;


getStaticPathsでは公開用記事ページのパスを指定してあげる必要があるので、microCMSのfiltersクエリを用いて公開用の記事のみを取得します。
記事の表示部分は先ほど作成したArticleコンポーネントを利用します。

9. 会員用記事ページの作成

最後に会員用記事ページです。
/pages/private/[id].tsxというファイルを用意します。

こちらはSG形式としてしまうと、Auth0にてクライアントサイドで認証を行なっても、ソースコードを除けばコンテンツの中身が閲覧できてしまいます。
よって、SSR形式でレンダリングを行います。

認証に関してもサーバーサイドで行います。

ソースコードはこちらのようになります。

import { getSession, getServerSidePropsWrapper, Claims } from '@auth0/nextjs-auth0';
import type { NextPage, GetServerSideProps } from 'next';
import Article from '../../components/Article';
import { client } from '../../libs/client';
import { Article as ArticleType, ArticleListDetail } from '../../types';

export const getServerSideProps: GetServerSideProps = getServerSidePropsWrapper(
  async (context) => {
    const { req, res } = context;
    const id = context?.params?.id as string;
    const session = await getSession(req, res);
    if (!session) {
      return { props: {} };
    }

    const data = await client.getListDetail<ArticleType>({
      endpoint: 'articles',
      contentId: id,
    });
    return {
      props: {
        data,
        user: session.user,
      },
    };
  },
);

type Props = {
  data?: ArticleListDetail;
  user?: Claims;
};

const PrivateId: NextPage<Props> = ({ data, user }) => {
  if (!user || !data) {
    return <main>ログインが必要です</main>;
  }
  return <Article data={data} />;
};

export default PrivateId;

getServerSidePropsにて、ユーザーからリクエストが合った際のサーバーサイド処理を記述します。

ここで、普通にgetServerSidePropsを使ってしまうと下記のような警告が出てしまいます。

warn - You should not access 'res' after getServerSideProps resolves. 

nextjs-auth0が提供しているgetServerSidePropsWrapperを利用することで解決しています。

参考:https://github.com/auth0/nextjs-auth0/blob/main/FAQ.md#3-im-getting-the-warningerror-you-should-not-access-res-after-getserversideprops-resolves

getServerSideProps内ではAuth0のgetSessionを用いて認証を行います。

認証が通ってからコンテンツの取得を行い、propsとして渡します。
propsとしてはコンテンツのデータ(data)と認証したユーザー情報(user)を渡します。

ここで注意点として、認証が通らなかった場合でもコンテンツのデータをpropsに渡してしまうとクライアントのソースコードに乗ってきてしまうので、認証が通らなかった場合はpropsには何も渡さないようにしましょう。

実際にlocalhost:3000にて会員用記事にアクセスしてみて、ログイン時のみ下記のように記事内容を表示できていれば成功です。

10. ビルドしてみる

試しに一度ローカル環境でビルドしてみましょう。

$ npm run build


//public/[id]はSSG、/private/[id]はServerとなっていることが確認できるかと思います。

11. Vercelへのデプロイ

Next.jsの開発元でもあるVercel社が運営しているホスティングサービスを使用します。
事前にアカウント作成を済ませておいてください。
https://vercel.com

まずは、GitHubリポジトリを作成します。

$ git init
$ git add .
$ git commit -m 'first commit'
$ git remote add origin your-repository  // 自分のリポジトリを入力
$ git push -u origin main


Vercelで先ほど作成したリポジトリと連携をしていきます。
ダッシュボード上のNew Projectをクリックします。
そして、今回作成したリポジトリを選択して、importをクリックしましょう。

その後、ビルド設定画面が表示されます。

Build and Output Settings はそのままでOKです。
Environment Variables には環境変数として用意している.env.localの情報を全て入力しましょう。

ただしAUTH0_BASE_URLに関しては、正しくはVercelにデプロイされるURLを指定してあげる必要があります。
後からでも変更できるので、一旦localhostのままデプロイを進めてしまいます。

デプロイが無事完了すると次のような画面が表示されます。

一旦デプロイは完了しましたが、おそらくログイン認証周りがうまく動かないと思います。
今まで開発環境向けに設定していたhttp://localhost:3000部分を実際にデプロイされたURLに書き換える作業が必要です。

11-1. 環境変数

Vercelの設定画面から「Environment Variables」に移動し、AUTH0_BASE_URLをデプロイされたURLに書き換えてください。
再度デプロイをしないと反映されないので、Deployments画面に移動し、Redeployをかけましょう。

11-2. Auth0の設定画面

次にAuth0の管理画面に入り、アプリケーションの設定画面を開きます。
Application URIs の項目にある Allowed Callback URLs と Allowed Logout URLs にVercelのデプロイURLを追記してください。
(カンマつなぎで追記可能です)

設定例は上記のような形です。
ご自身の環境のURLを追記してください。

以上でVercel環境でも認証が動くようになったかと思います。

次回

公開用記事と会員向け記事をそれぞれmicroCMSから入稿できるようになりました。
ここまでで会員制メディアはほぼ完成していると言っても過言ではありません。

まだ出来ていないところとしては、一覧ページと公開用記事に関してはSGをしているので、microCMSから記事を公開してもサイトには反映されない状態になっています。
これを解決するためには記事公開時にmicroCMSからWebhook通知を飛ばすか、Next.jsのISR(Incremental Static Regeneration)を使うか等、いくつか方法があります。

次回はこのあたりの解説から進めていきたいと思います。

まずは、無料で試してみましょう。

APIベースの日本製ヘッドレスCMS「microCMS」を使えば、 ものの数分でAPIの作成ができます。

microCMSを無料で始める

microCMSについてお問い合わせ

初期費用無料・14日間の無料トライアル付き。ご不明な点はお気軽にお問い合わせください。

お問い合わせ

microCMS公式アカウント

microCMSは各公式アカウントで最新情報をお届けしています。
フォローよろしくお願いします。

  • X
  • Discord
  • github

ABOUT ME

柴田 和祈
microCMSのデザイン、フロントエンド担当 / ex Yahoo / 2児の父 / 著書「React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで 」 / Jamstack