ヘッダーロゴ

【ダイナミックルーティング編】 Notion の記事でブログサイト作成

2022-02-11

React

TypeScript

Tailwind CSS

Notion

API

Next.js

hero画像

はじめに

今回はブログサイト作成課題でポイントになるのが、複数ある記事をどうやって表示させるか?ここが実装できるかできないかで大きく変わります。

1つの記事に対して1つの index.tsx を作成していくのか?記事数がすくなければ最悪良いかもしれませんが、100記事合った場合100の index.tsx を作成し、紐付けていくのか…生産性のかけらもない実装になります。ではどうするのか?1つのファイルを使い回していければ楽です。

今回は2つある記事を、1つのファイルでそれぞれ表示させていくことをゴールとします。

※TypeScript で実装していますが、型定義は any 型で一旦回避しています

こんな方におすすめ

  • 各記事を表示させたいが、どのように実装していけばいいかわからない方
  • getStaticPaths | getStaticProps の使い方、どう理解すればいいか迷っている方
  • ダイナミックルーティングを実装と通じて体験して理解したい方
  • map メソッドを使えるようになりたい方

Notion の API に対して右も左もわからない状態の方は こちら の記事をまずは確認してください。

全体の流れ

  • map メソッドを使用してメインページに全ての記事を表示させる
  • ブラウザにどの部分を表示させたいかを決める
  • Notion 全体のデータ構成からどの要素を利用するのか確認する
  • 簡単に記事サイトへ移動する方法でイメージを掴む
  • ページ ID を使用してページの情報を取得する
  • ダイナミックルーティングの凄さを体験してみる

map メソッドを使い倒そう

複数のデータが存在する場合、この map 関数が非常に重要になってきます。というより、これを使わないと今回の課題は乗り越えられません。map をうまく使用することで実装がとても楽になりました。ここは最初に押さえておきたいポイントです。

map メソッドができることを確認

これを必ず押さえておきましょう。こちら のサイトを参照してください。

メソッドは、与えられた関数を配列のすべての要素に対して呼び出し、その結果からなる

map メソッドを体験してみる

簡単な例で体験してみましょう。

// console.log で出力してみる const numbers = [1 ,2, 3, 4, 5] console.log(numbers) [ 1, 2, 3, 4, 5 ] // console.log.map で出力してみる console.log(numbers.map((number, i) => { return number })) [ 1, 2, 3, 4, 5 ]

新しい配列 を生成していることが確認できます。でも出力結果だけみると、通常に console.log したのと map メソッドをした結果が全く一緒です。違いがこれだけだと理解できません。

違いを可視化するために、ブラウザに表示してみます。

export default function Home() { const numbers = [1, 2, 3, 4, 5]; return ( <> // 通常のconsole.log <p className='p-1'>{`通常のconsole.logの結果: ${numbers}`}</p> // map メソッドを使用 {numbers.map((number, i) => { return <p className='p-1' key={i}>{`map メソッドの結果: ${number}`}</p>; })} </> ); }
画像

ブラウザで可視化すると違いが一目瞭然ですね。通常の console.log は単に numbers の中身をそのまま出力しただけなのに対して、map メソッドは配列の中身を1つ1つ出力していることがわかります(p タグは改行を意味)

map メソッドの動きを確認

numbers の要素順(1から5の順番)で1つ毎、中身をmap メソッドを使用して取り出しています。取り出した1つの要素を、numbers.map((number, i) の number が受け取っています。この引数名は正直なんでもOKですが、複数の要素を保有している numbers から1つのデータを取得しているので、単数形(ここでは number )と書くとわかりやすいです。

最後に受け取った要素を return () でデータを返します。これを要素数分繰り返します。

今回は numbers に5つの要素分、5回処理が繰り返されブラウザに表示されています。

流れを確認すると…

① numbers から1つ要素を取り出す

② 受け取った要素をnumber が保有

③ return で受け取った number をもとに情報を返す

④ 要素数分 ①〜③の処理を繰り返す(要素分実行したら処理が終了)

map メソッドの第二引数の意味と使い方

第二引数とは numbers.map((number, i) の i を指しています。ここは別に i でなくても問題ありません。index としてもOKです。

これは numbers が保有している要素のインデックス番号を受け取っています。

画像

第二引数を活用したコードに変更していきます。

export default function Home() { const numbers = [1, 2, 3, 4, 5]; return ( <> {numbers.map((number, i) => { return <p className='p-1' key={i}>{`${i}番目: ${number}`}</p>; })} </> ); }
画像

インデックス番号が5つ正しく表示されています。ただし、これだと少し見にくいですね。0番目を1番目に変えてみます。

return <p className='p-1' key={i}>{`${i + 1}番目: ${number}`}</p>;
画像

インデックス番号に +1 してあげればイメージ通りの結果が返ってきています。このインデックス番号の活用方法に関しては、今回の課題で使用しますので頭に入れておくことで、アイディアの幅が広がります。

key ってなに?必要なのか?

map メソッドを使う = key を設置する必要がある と理解してしまうのが一番楽だと思います。また、key の設置がマストで必要な場合、Vs Code 上でエラーとして教えてくれます。

画像

以上が基本的な map メソッドの使い方になります。細かい部分はこの後、実装の中で使い方を紹介していきます。

ブラウザに表示させたい要素がどれかを確認

今回はトップページに各記事のタイトル、日付、名前が表示されたカードを作成していきます。

画像

どこから情報を持ってくればいいのかを最初に確認します。データーベースから持ってきます。

画像画像

Notion のデータ構成の全体像は何度も使用しますので、押さえておきましょう。

トップページから記事サイトへ移動する

簡単な方法ですが、ゴールイメージを明確にするために実際にトップページの記事カードをクリックしたら、Notion の記事に移動するようにしてみます。ヒントが隠れていそうです。

上記動画を実装したコード

export default function Home({ posts }: { posts: any }) { return ( <> {posts.results.map((result: any, i: any) => { return ( <div key={i}> <a href={result.url} target={'_blank'} rel='noreferrer'> <button className='m-1 rounded-md border-2 border-gray-200 p-1 text-left'> <p className='p-2'>{`タイトル:${result.properties.Title.title[0].plain_text}`}</p> <p className='p-2'>{`日付:${result.properties.Date.date.start}`}</p> <p className='p-2'>{`著者:${result.properties.User.people[0].name}`}</p> </button> </a> </div> ); })} </> ); }

考え方は、それぞれの記事サイトに url が存在しているので、a タグでページが移動するようにしています。console.log で確認してみます。

{posts.results.map((result: any, i: any) => { // 追記 {console.log(result)} ... // ターミナルでの出力結果 { object: 'page', id: '2727c73a-0849-479a-aeed-d500b23078ed', created_time: '2022-02-11T08:17:00.000Z', last_edited_time: '2022-02-11T08:18:00.000Z', cover: null, icon: null, parent: { type: 'database_id', database_id: '75643bde-8274-4edc-a977-0c6d7c01cd9e' }, archived: false, properties: { Date: { id: 'iUzE', type: 'date', date: [Object] }, User: { id: 'wYUn', type: 'people', people: [Array] }, Title: { id: 'title', type: 'title', title: [Array] } }, url:
{posts.results.map((result: any, i: any) => { {console.log(result.url)} ... // ターミナルでの出力結果 https://www.notion.so/2727c73a0849479aaeedd500b23078ed https://www.notion.so/ed36a8df806e4507868bc548cc1418ee

ターミナルで表示された https:〜をクリックして実際にページが表示されるか確認してみてください。

どうでしょう、これから完成させたいブログサイトのイメージが掴めたのではないでしょうか。

移動した url を確認してみる

上記までは難易度を下げて、完成版のイメージを掴んできました。イメージを掴めましたが、ここにヒントがないか考えてみましょう。

url をそれぞれ確認してみます。

記事①

画像

記事②

画像

notion.so/以降が異なっています。ここに各ページでしか保有していない値を指定することで、指定したページに移動しているのではないか?

上記の仮設を検証してみます。その前にここからはNotion のデータ構成でどこから情報を取得したいかをもう一度確認します。今回はページです。ページにアクセスしてページの情報を取得するからです。

では検証していきます。データベースからの情報取得の流れと一緒です。ページごとにリンク先が設定されているので、各ページのリンク先を確認してみます。

画像

取得したリンク先を見てみます。

// 記事①のリンク先 https://www.notion.so/

リンク先を取得して見えてきたことは、notion.so/以降は pageID になっているということです。Notion の ID は32桁になっています。

console.log を使ってターミナルでも確認してみます。

export default function Home({ posts }: { posts: any }) { console.log(posts.results[0]) ... // ターミナルの出力結果 { object: 'page', id: '

id と url の id が一致していることがわかります。- を含んだままの ID を url に貼り付けてもページエラーとして返ってきますが、- を削除したら url の ID と一緒になるので正しくページが表示されます。

ローカルサーバーで表示できるか試してみる

では同じように各ページの ID をローカルサーバーの url に貼り付けたら、そのページに移動するか試してみます。

画像画像

404と存在しないページです、と怒られてしまいました。当然といえば当然ですね。難易度を下げて実装したのは、a タグに https:〜と成立した url をリンク先に指定していました。

当然ですが今回はローカルサーバーで、この世に存在しない url を作成しアクセルしようとしたのでエラーになりました。

難易度を下げて発見できたこと

  • トップページのデータベースから url を取得してページ移動することができた
  • 各ページに ID というものが存在していること
  • url の後ろに各ページの ID が表示されていること
  • ID 部分を変更するだけでページ移動ができること

ここまでで、大枠でやりたいことのイメージとそこから発見できたことを押さえておきましょう。

今は点としての理解で問題ありません。後々、線としてつながっていきます。

ページの情報を取得してみる

次のステップとして、ページの情報を取得してこないことには始まらないので、Notion からページデータを取得してみましょう。

まずはページを表示するだけのファイルを作成していきます。

pagesフォルダ ➣ posts(新規作成) ➣ id.tsx を作成します。

※フォルダ・ファイル名はなんでもOKですが、シンプルにしておくことをおすすめします。

画像

Notion の 公式ドキュメント を確認します。PAGES からどのようにデータを取得するか書いていますので、こちらを参考にしてコードを書いていきます。

画像

今回はテスト用の記事①の ID を指定して情報を取得してきます。一気に console.log で期待した値が出力できているか確認します。

まずローカルサーバーで作業するファイルにアクセスします。/posts/id を追加します。

画像
import { notion } from '../index'; export default function Page({posts} : {posts: any}): JSX.Element { console.log(posts.properties.Title.title[0].plain_text) console.log(posts.properties.Date.date.start) console.log(posts.properties.User.people[0].name) return ( <> <p>取得したページの情報を反映させる</p> </> ); } const pageId = 'ed36a8df806e4507868bc548cc1418ee'; export async function getStaticProps() { const response = await notion.pages.retrieve({ page_id: pageId }); return { props: { posts: response }, }; } // ターミナルでの出力結果 テスト用の記事① 2022-02-11 片山真介

期待した結果になっています。

データベースからの取得方法と大きな違いはありませんね。

ブラウザに表示されるように修正します。

import { notion } from '../index'; export default function Page({ posts }: { posts: any }): JSX.Element { return ( <div className=' mx-auto w-[70%] font-bold'> <h1 className='border-b-4 py-5 text-5xl'>{posts.properties.Title.title[0].plain_text}</h1> <h2 className='py-5 text-xl'>{posts.properties.Date.date.start}</h2> <h3 className='py-1 text-xl'>{posts.properties.User.people[0].name}</h3> </div> ); } const pageId = 'ed36a8df806e4507868bc548cc1418ee'; export async function getStaticProps() { const response = await notion.pages.retrieve({ page_id: pageId }); return { props: { posts: response }, }; }
画像

見やすくなりました。

この実装で押さえておくべき問題点は…

  • url に直接アクセスしたいフォルダ、ファイル名を入力していること( /posts/id )
  • ページID 1に対して1つのファイルを使用していること( const pageId )

この方法では最初に記載したように、生産性がまったくない実装になってしまいます。100記事が合った場合、100のページID ごとにファイルを作成して…と、気が遠くなります。

ではこの問題を解決するためにはどうしたらいいのか?

ここで思い出してほしいのが、難易度を下げて実装したとき url の後ろに ページID が存在し、ID を変更することで移動することができたことを。 そして url に入っている ID をファイル内で取得できたら、ページID 1つに対して1つのファイルを作成する必要はなくなりますね。

解決したい問題点を整理しよう

これまでの流れで解決したい、実装したいことを整理します。

  • 1つのファイルを使いまわして実装を楽にしたい
  • ページID を取り出してファイル内で使えるようにしたい

この2点が解決すればいけそうな気がしてきました。

ダイナミックルーティングで1つのファイルを使い回す

1つ目の問題ですが、一瞬で解決します。一瞬で終わります…ファイル名に [ ] をつけるだけです。これだけです…

これだけですが、利便性などを理解しないと使いこなせないので確認していきます。

posts フォルダに [test].tsx というファイルを新規に作成します。ファイルの中身は、ブラウザに何かが表示されればOKです。

画像

ではローカルザーバーを立ち上げて確認します。実際の凄さは動画のほうが伝わると思いますので、確認してください。

やってることは、/postsフォルダ へアクセス(エラーになる)➣ /posts/test でファイル名も指定する ➣ ファイル名を適当にしてもアクセスできるか確認

[ ] でファイル名を囲うことで url が自由自在に変化させることができ、表示させたい内容は変わらないことが理解できました。

ページID を url に送り表示させる

一歩一歩ゴールに近づいています。次はトップページから url にそれぞれのページID を送るように実装していきます。

画像
// ターミナルの出力結果 2727c73a-0849-479a-aeed-d500b23078ed

まずは console.log で期待した出力結果が得られているか丁寧に確認します。その上で以下のコードに書き換えます。

import { Client } from '@notionhq/client'; import Head from 'next/head'; import Link from 'next/link'; export const notion = new Client({ auth: process.env.NOTION_KEY }); export const databaseId = process.env.NOTION_DATABASE_ID; export default function Home({ posts }: { posts: any }) { {console.log(posts.results[0].id);} return ( <> {posts.results.map((result: any, i: any) => { return ( <Link key={i} href={`

ページ移動後はまだそのままですが、 url に各ページID が反映されるようになりました。

テスト用の記事①url

画像

テスト用の記事②url

画像

getStaticPaths | getStaticProps を使ってページID を取得しよう

2つ目の問題、ページID を取り出してファイル内で使えるようにする。ここを解決していきましょう。あと少しです…

まず、テスト用で使っていた [test].tsx は削除し、id.tsx を [id].tsx へ変更してください。

画像

ローカルサーバーを再度立ち上げます。立ち上げた後、メインページのブログカードをクリックして挙動を確認してみます。いきなりめちゃくちゃエラーで怒られます…怒られますが url は期待した通りの動きは変わりありません。

画像

ではエラーメッセージを確認します。確認しますが私は英語が全くわからないので、翻訳ツールの力を借ります。こちら のツール

画像

エラーメッセージで丁寧に getStaticPaths が必要であることnext.js のリンク先も丁寧に案内してくれいるのでその通りにやっていきます。

画像

やることは…

  • 生成されるパスを全て取得する

paths

ビルド時に静的に生成されるパスの配列を paths に渡す必要があるようです。

getStaticPaths 内で全てのパスを取得する

Notion のデータベースから、各ページのID が取得できましたので getStaticPaths 内で取得していきます。今回も console.log で一つ一つ丁寧に見てきます。

export async function getStaticPaths(){ const response = await notion.databases.query({ database_id: databaseId!}) console.log(response.results) }
画像

エラーで即怒られました。何やら { paths: [], fallback: boolean } を返す必要があるようですね。

export async function getStaticPaths(){ const response = await notion.databases.query({ database_id: databaseId!}) console.log(response.results) return { paths:[], fallback: false, } } // ターミナルの出力結果 [ { object: 'page', id: '2727c73a-0849-479a-aeed-d500b23078ed', created_time: '2022-02-11T08:17:00.000Z', last_edited_time: '2022-02-11T08:18:00.000Z', cover: null, icon: null, parent: { type: 'database_id', database_id: '75643bde-8274-4edc-a977-0c6d7c01cd9e' }, archived: false, properties: { Date: [Object], User: [Object], Title: [Object] }, url: 'https://www.notion.so/2727c73a0849479aaeedd500b23078ed' }, { object: 'page', id: 'ed36a8df-806e-4507-868b-c548cc1418ee', created_time: '2022-02-10T21:51:00.000Z', last_edited_time: '2022-02-11T08:18:00.000Z', cover: null, icon: null, parent: { type: 'database_id', database_id: '75643bde-8274-4edc-a977-0c6d7c01cd9e' }, archived: false, properties: { Date: [Object], User: [Object], Title: [Object] }, url: 'https://www.notion.so/ed36a8df806e4507868bc548cc1418ee' } ]

欲しいデータが取れています。この中でも id: 〜 の部分が必要になるので、ここだけ取得します。

export async function getStaticPaths(){ const response = await notion.databases.query({ database_id: databaseId!}) console.log(response.results.map((result, i) => { return result.id })) return { paths:[], fallback: false, } } // ターミナルの出力結果 [ '2727c73a-0849-479a-aeed-d500b23078ed', 'ed36a8df-806e-4507-868b-c548cc1418ee' ]

ID だけ取得できました。これを変数に保存します。取得したパスはページID になっているので、getStaticsProps に渡してあげればページの情報を取得できそうです。

受け取り方はどうすればいいのか、こちら の公式ドキュメントを確認します。

画像

{ params } で受け取っていますね。

export async function getStaticProps({params}: {params:any}) { const response = await notion.pages.retrieve({ page_id: params.id}) return { props: { posts: response} } }

うまくいくのかと思いきや…またエラーですね。

画像画像

恐らく、getStaticPaths で取得した ID と url にある ID が一致していないと予測しました。

画像

url で表示されている ID と getStaticPaths で取得して渡している ID が違います。

getStaticPaths の パスを保存している Paths の中身を見ていきます。

export async function getStaticPaths(){ const response = await notion.databases.query({ database_id: databaseId!}) const Paths = (response.results.map((result, i) => { return result.id })) console.log(Paths) return { paths: Paths, fallback: false, } } // ターミナルの出力結果 [ '2727c73a-0849-479a-aeed-d500b23078ed', 'ed36a8df-806e-4507-868b-c548cc1418ee' ]

恐らく、単純に一番最初に取得したパスを渡しているだけだと思います。どちらにしろ、getStaticPaths からのパスの取得、渡し方に問題があることは間違いないです。

こちら の公式ドキュメントで確認すると、{ params: { id: 〜 } } と書いています。確かに export async function getStaticProps({params}: {params:any}) と params でデータを受け取っていました。

画像

以下のようにコードを修正していきます。

画像
return {params: {id: result.id}} // ターミナルの出力結果 [ { params: { id: '2727c73a-0849-479a-aeed-d500b23078ed' } }, { params: { id: 'ed36a8df-806e-4507-868b-c548cc1418ee' } } ]

出力結果が params というオブジェクトの中に id が保存されている形式に変わりました。

うまくいったか確認します…うまくいきました!!!

まとめ

正直、技術的な面に関してはまだまだ曖昧な部分がありますが、問題解決するためにどのようなプロセスで解決していくのか、インプットした知識をより深く理解するためにはどうしていけばいいのか?を、課題を通じて学ぶことができています。

目の前の問題、課題が大きすぎたとしてもまずはやってみる、その上でわからない部分は難易度をさげ、最小単位で確認し理解度を上げる、また問題点を解決していく。非常に学びが多い課題です。

created by

片山 真介

フッター画像
Twitter画像Facebook画像

© Shinsuke Katayama