こんにちは、柴田です。
「会員制メディア」のチュートリアルを全3回に分けてお届けしております。
今回は第2回のページ作成編です。
===
===
前回は認証ができるところまで実装しました。
今回は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-domain
とapi-Key
を設定してください。serviceDomain
はXXXX.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/xxxxxx
とprivate/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-resolvesgetServerSideProps
内では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)を使うか等、いくつか方法があります。
次回はこのあたりの解説から進めていきたいと思います。