NotionのコンテンツをNext.jsに表示する

はじめに

以前にCareer一覧ページをnext.jsのトップページに実装しました。
トップページには一覧を表示しているため、詳しい情報は載せられません。
詳しい情報はCareerの詳細ページを作成してそこで見てもらうようにします。

Careerの詳細ページには記載すべきことがまちまちだったりするので、自由に書けて、見た目も整っている状態が望ましいです。
そのような要件を達成するにあたってはいくつかやり方があります。

例えば

今回はエディタの導入コスト見た目の調整に必要な実装はなるべく少なく。それでも見た目は豪華にできると一番よいです。
そこで、Notion APIを使ってヘッドレスCMSのように使う方法を採用しました。
エディタの実装はなんだかんだいろんなアプリケーションで必要になることが多い機能です。もし皆さんが実務で実装が必要になった際にはNotionのAPIを採用することは少ないかもしれません。上記のライブラリやサードパーティツールを検討してみてください。

ゴールの確認

notion-clientとreact-notion-xをインストールする

上記のライブラリをNext.jsに組み込む

Next.jsの動的ルーティングを実装する

ここでの差分はgithubにあげています。困った際には参考にしてみてください。

Notionの準備

まずはNotionにコンテンツを用意しましょう。

Notionを初めて使う場合はアカウントの作成が必要です。以前に勤務していたところではNotionを会社のドキュメント管理ツールとして利用しており、エンジニアのドキュメント作成ツールとして採用されているケースも少なくないと思うので慣れておくとよいと思います。

適当にページを作成してその中にさらにページを作成していきましょう

作成したページ(Career 1)の中にさらに経歴についての詳細を書いていきましょう。

動作確認のためにとりあえず必要なので、内容は適当でよいです。見出し、テーブル、リストなどなどを追加してみてください。
実装が終わったら、会社名、会社の規模、その中での自分の役職・仕事内容、どんなことを頑張ったかなど自由に書いてみてください!
notionはテンプレートを公開しているので、それを参考にしてみるとよいものが作りやすいです。

作成ができたら、右上の共有>web公開>公開をしてください
これにより、このページが誰でも見られうる状態になります。

NotionをNext.jsへ組み込む

Notoinでコンテンツが準備できたら、コンテンツをnext.jsに組み込んで表示できるようにしていきます。

表示するためには、apiを実行してコンテンツを取得する(→notion-client)。取得したコンテンツを画面に描画する(→react-notion-x)。を行います。

ライブラリのインストール

notion-clientreact-notion-xをインストールします。

docker compose exec frontend bash
npm install notion-client  react-notion-x

react-notion-xのreadmeにシンプルな実装方法が書いてあるのでこちらを参考に進めていきましょう

コンテンツを表示するためのページを作成します

import ContentsTemplate from "@/features/contents/components/contents-template";
import { NotionAPI } from "notion-client";
import "react-notion-x/src/styles.css";

export default async function ContentsPage() {
  const api = new NotionAPI();
  const recordMap = await api.getPage("cabf1c78b8f6496ba06197b8db6bab76");
  return <ContentsTemplate recordMap={recordMap} />;
}
await api.getPage("cabf1c78b8f6496ba06197b8db6bab76");

getPageの引数にはnotionのページのIDを指定します。
notionのページIDはurlから判断することができます。
notionのページのurlのcabf1c78b8f6496ba06197b8db6bab76という32文字がIDになります。
ご自身が作成されたidを指定するようにしてください。

ContentsTemplateコンポーネントでrecordMapをNotionRendererに渡していきます

"use client";

import { ExtendedRecordMap } from "notion-types";
import { NotionRenderer } from "react-notion-x";

export default async function ContentsTemplate(props: {
  recordMap: ExtendedRecordMap;
}) {
  const { recordMap } = props;
  return (
    <div>
      <NotionRenderer
        recordMap={recordMap}
        fullPage={true}
        darkMode={false}
      />
    </div>
  );
}

http://localhost:8080/contents で表示されたでしょうか?

リファクタリング

とりあえず表示ができるような状態にはなったので、改善していきましょう

まずは表示するコンテンツを動的にしていきます。
そのためにはurlのパスパラメータにcontentのidを指定するようにしてその値をgetPageの引数に入れるようにしたいです。(動的ルーティング)

Next.jsで動的なurlを作成するにはcontent/[contentId]/page.tsxのように、角括弧で変数名を囲んだディレクトリの下にpage.tsxをおきます。
先に作成したpage.tsxをこのディレクトリの中に置くようにしましょう。

import ContentsTemplate from "@/features/contents/components/contents-template";
import { NotionAPI } from "notion-client";
import "react-notion-x/src/styles.css";

export default async function ContentsPage({
  params,
}: {
  params: { contentId: string };
}) {
  const { contentId } = params;
    const api = new NotionAPI();
  const recordMap = await api.getPage(contentId)
  return <ContentsTemplate recordMap={recordMap} />;
}

contentIdの値はコンポーネントの関数の引数から取り出せます。
これをgetPageに渡すようにします。

次に、api.getPage()のcontentIdが存在しないidを指定するとエラーになってしまいます。
404が適当と思われるので、存在しないidを指定した場合は404ページを表示するようにしたいです。
そのために、api.getPageをエラーハンドリングをするために、NotionAPIをラップしたクラスを作成して、そのインスタンス経由でrecordMapかundefinedを返すようにします。

import { NotionAPI } from "notion-client";

class NotionClient {
  private _api;

  constructor() {
    this._api = new NotionAPI();
  }

  async getPage(pageId: string) {
    try {
      const recordMap = await this._api.getPage(pageId);
      return recordMap;
    } catch (e) {
      console.log(e);
    }
  }
}

const notionClient = new NotionClient();
export default notionClient;
import ContentsTemplate from "@/features/contents/components/contents-template";
import notionClient from "@/lib/notion-client";
import "react-notion-x/src/styles.css";

export default async function ContentsPage({
  params,
}: {
  params: { contentId: string };
}) {
  const { contentId } = params;
  const recordMap = await notionClient.getPage(contentId);
  return <ContentsTemplate recordMap={recordMap} />;
}
import { ExtendedRecordMap } from "notion-types";
import NotionContent from "@/features/contents/components/notion-content";
import { notFound } from "next/navigation";

export default async function ContentsTemplate(props: {
  recordMap?: ExtendedRecordMap;
}) {
  const { recordMap } = props;
  if (recordMap) {
    return <NotionContent recordMap={recordMap} />;
  } else {
    return notFound();
  }
}

NotionContentのコンポーネントを作成して、その中でライブラリのNotionRendererを呼び出すことでNotionRendererの設定をアプリケーションで固定しやすくしておきます。

"use client";

import { ExtendedRecordMap } from "notion-types";
import { NotionRenderer } from "react-notion-x";

export default async function NotionContent(props: {
  recordMap: ExtendedRecordMap;
}) {
  const { recordMap } = props;
  return (
    <NotionRenderer
      recordMap={recordMap}
      fullPage={true}
      darkMode={false}
      disableHeader={true}
      mapPageUrl={(pageId) => `/contents/${pageId}`}
    />
  );
}

disableHeaderとmapPageUrlを追加で入れました。

disableHeaderはfullPageをtrueにするとbreadcrumbsが表示されてしまうのですが、これを非表示にしてくれています。

mapPageUrlはnotionのコンテンツの中にpageが入っているとlocalhost://contentIdのような形式になってしまうのですが、これをlocalhost://contents/contentId として現在のnextjsのパスのルールに合うようにしています。

おわりに

notionの2つのライブラリは私も今回が初めての導入でした。
notionのコンテンツをほとんどそのまま再現できるので非常に便利ですね。

disableHeaderやmapPageUrlの使い方についてはNotionRendererの型定義のクラスをみてそれっぽい引数をいくつか試してみてみつけました。

export declare const NotionRenderer: React.FC<{
    recordMap: ExtendedRecordMap;
    components?: Partial<NotionComponents>;
    mapPageUrl?: MapPageUrlFn;
    mapImageUrl?: MapImageUrlFn;
    searchNotion?: SearchNotionFn;
    isShowingSearch?: boolean;
    onHideSearch?: () => void;
    rootPageId?: string;
    rootDomain?: string;
    fullPage?: boolean;
    darkMode?: boolean;
    previewImages?: boolean;
    forceCustomImages?: boolean;
    showCollectionViewDropdown?: boolean;
    linkTableTitleProperties?: boolean;
    isLinkCollectionToUrlProperty?: boolean;
    isImageZoomable?: boolean;
    showTableOfContents?: boolean;
    minTableOfContentsItems?: number;
    defaultPageIcon?: string;
    defaultPageCover?: string;
    defaultPageCoverPosition?: number;
    className?: string;
    bodyClassName?: string;
    header?: React.ReactNode;
    footer?: React.ReactNode;
    pageHeader?: React.ReactNode;
    pageFooter?: React.ReactNode;
    pageTitle?: React.ReactNode;
    pageAside?: React.ReactNode;
    pageCover?: React.ReactNode;
    blockId?: string;
    hideBlockId?: boolean;

ドキュメントでみつからなくても型定義のある言語であればライブラリのコードを読んで使い方をある程度推測できるので楽ですね。

今のままだと、contentIdを直接urlに指定しないとコンテンツを見ることができないので、railsからcareerとコンテンツのIDを紐づけて、nextjsのcareerをリンクにしてuiからコンテンツを見れるまでの導線を実装していきます。

コメント

タイトルとURLをコピーしました