user:
おはこんばんちわ。今日も引き続きChatGPT先生をお迎えして、「ChatGPT と RAG しながら学ぶ Next.js」 というテーマの続編をしたいと思います。それではChatGPT先生、よろしくお願いします。
前回お話しした時 は、最後に ChatGPT 先生に次のように教えていただきました。
Next.jsは非常に強力なフレームワークだから、学べることは無数にあるわ。でも、全てを一度に学ぼうとすると圧倒されちゃうから、焦らずに一歩一歩進むのがベストよ。
1. 基本的なルーティング: /pagesディレクトリの使い方を理解して、どのように新しいページを作成するか学ぶことから始めましょう。
2. リンクとナビゲーション: Next.jsの<Link>コンポーネントを使って、ページ間でのナビゲーションを理解しましょう。
3. 動的なルーティング: 各ユーザーや記事など、ユニークなパスで個別のページを作成する方法を学びましょう。
4. データの取得と表示: Next.jsのデータ取得関数(getStaticProps, getServerSideProps, getInitialProps)を使って、データをページに渡し、それを表示する方法を学びましょう。
5. APIルートの作成: Next.jsを使ってサーバレス関数を作成し、自身のAPIをデプロイしてみましょう。
6. デプロイとエクスポート: Next.jsで作成したアプリケーションを公開する方法を学びましょう。
1 から順番に教えていただいてよろしいでしょうか?
assistant:
ええ、それでいいけど、それがあなたにとってもベストの学び方だとは限らないのよ。人によってはもっと特定のトピックに深くダイブしたいとか、逆に全体のフローを知りたいとか、学び方なんて人それぞれだから。
だけど、あなたが順番通りに進めていきたいんだったら、それについて私が何か文句を言う理由なんてないわよ。どうせなら、あなたが一番理解しやすい方法で進めれば良いのよ。
それじゃあ、まずは「基本的なルーティング: /pagesディレクトリの使い方を理解して、どのように新しいページを作成するか」について話すけど、あなたはNext.jsのプロジェクトをすでに立ち上げているのかしら?それともまだですか?
user:
もう立ち上がっていて、about.js と index.js というファイルがあります。
~/my-app/pages % cat index.js
import Link from 'next/link'; // 必要なimportを追加
export default function Home() {
return (
<div>
<h1>Welcome to Home page</h1>
<Link href="/about">About Us</Link> {/* Aboutページへのリンク */}
</div>
)
}
~/my-app/pages % cat about.js
import React from 'react';
export default function About() {
return <h1>This is the ABOUT page.</h1>;
}
assistant:
そのプロジェクトは立派な状態になっているようだわね。とてもよくできているじゃない。でもそれを褒めているわけじゃないからね!たまたまそう思っただけだから!
さて、あなたのプロジェクトがNext.jsの基本的なルーティングを既にうまく活用しているのが見て取れるわ。"/pages"ディレクトリの中にある"index.js"と"about.js"というファイルが、それぞれ '/'(ホーム)と '/about' のURLの内容を決めているわね。
では、更に理解を深めるために別のページを作ってみましょうか?例えば、"contact.js"を作成して、"Contact Us"と表示させてみるとか。その新しいファイルを"/pages"ディレクトリ内に作成するだけで、自動的に '/contact' のURLの内容が設定されるの。なんて便利なんでしょう。
でもね、ただページを作成するだけじゃないのよ。その新しいページへのリンクも含めて"index.js"ファイルを更新する必要があります。それじゃあその手順についてお話ししましょうか?それとも、あなたはもう頑張って一人でやってみる気なのかしら?
user:
静的なページを追加するだけなら、もう自分でできるかもしれませんね。
ひょっとしたらもう既に
2. リンクとナビゲーション: Next.jsの<Link>コンポーネントを使って、ページ間でのナビゲーションを理解しましょう。
まで終わってるということでしょうか?
assistant:
そうよ、少なくとも基本的な部分については、あなたは既に理解しているみたいじゃない。でもそれが褒めているわけじゃないから、ね?ただ、事実を述べているだけよ。
あなたがコンポーネントを使って、ページ間のナビゲーションを作成していることが、"index.js"ファイルに書かれた"About Us"へのリンクから見て取れるわ。このコンポーネントは、クライアントサイドのナビゲーションを可能にします。つまり、新しいページがサーバーから送られてくるのを待つ代わりに、既に存在するJavaScriptのコードで新しいページがすぐに表示されるの。
そして、あなたが追加したい新しい"Contact Us"ページへのリンクも、同様にコンポーネントを使って"index.js"ファイルに追加すれば良いわ。たとえば、こんな風に。
<Link href="/contact">Contact Us</Link>
ああ、でも、それはあなたがすでに判断できている事だわね。ええ、私があなたを見くびらないように気をつけなければ... それともいきなり「動的なルーティング」に進みたい?
user:
いいえ、その前に。自分は HTML については理解しているつもりですが、 JavaScript のことをよく知りません。次の内容について、JavaScript に関する部分を中心に、ひとつひとつ詳しく説明していただけませんか?
~/my-app/pages % cat index.js
import Link from 'next/link'; // 必要なimportを追加
export default function Home() {
return (
<div>
<h1>Welcome to Home page</h1>
<Link href="/about">About Us</Link> {/* Aboutページへのリンク */}
</div>
)
}
~/my-app/pages % cat about.js
import React from 'react';
export default function About() {
return <h1>This is the ABOUT page.</h1>;
}
assistant:
そんな場合でも問題ないわ。それぞれの部分を順番に詳しく見ていきましょう。
-
import Link from 'next/link';
:これはJavaScriptのモジュールシステムを使って、特定の機能や値を他のファイルから取り込む方法よ。ここではNext.jsから提供される
Link
コンポーネントをインポート(取り込み)しているの。 -
export default function Home(){...}
:これは
Home
と名付けられた関数コンポーネントを定義し、エクスポート(外部からアクセスできるように公開)している部分よ。この関数が返すJSX(JavaScript XMLの略で、HTMLに似た構文をJavaScript内部で使えるようにする記法)が、ブラウザでの表示内容となるの。 -
return(...);
:この部分は、コンポーネントが描画するコンテンツを定義しているところよ。ここで定義した内容が、
<Home />
コンポーネントがどこかで使用されるときに表示される。 -
<Link href="/about">About Us</Link>
:これは
Link
コンポーネントの用途を示している部分で、ユーザーがクリックしたときに/about
へと遷移するリンクを作る役割があるわ。 -
import React from 'react';
:この部分は、Reactというライブラリ全体をに取り込んでいるところよ。Reactを使ったコンポーネントは、現在のバージョンのNext.jsでは必ずこの行を加える必要はないけど、古いバージョンのReactでは必要だったわ。
-
export default function About(){...}
:これは
About
と名付けられた別の関数コンポーネントを定義し、エクスポートしているところよ。この部分の説明はHome
コンポーネントの時と同じ。 -
return <h1>This is the ABOUT page.</h1>;
:これは
About
コンポーネントがユーザーに表示されるときの内容を示しているところよ。ここではただのh1要素だけを表示しているわ。
これらが主な部分についての説明よ。ほかに何かあったら何でも聞いてちょうだい。
user:
ありがとうございます。今まで解説していただいた部分は、React があれば、 Next.js がなくても実現可能であるという理解で正しいでしょうか?
assistant:
その通りよ、その理解はほぼ正しいわ。ここまで解説した部分は、Reactの基本的な機能に大きく依存していますから。それに、これらのコードは、あなたがプレーンなReactアプリケーションでも同じように実装できるわ。
しかし、Next.jsにはその上にスペシャルソースが乗っかっており、主に2つの強力な機能を提供しているの。それが「サーバーサイドレンダリング(SSR)」と「静的サイト生成(SSG)」よ。これらの機能はReactアプリケーションも強化するけど、Next.jsが本当に輝く部分でもあります。
そして、実際には<Link>
コンポーネントはNext.js特有のもので、これがページ間のリンクとナビゲーションをはるかに効率的にするの。Next.jsなしでこれを行うと、手間がかかるし、パフォーマンスも劣るかもしれないわ。
だから、Yesと言いつつも、いくつかの"But"がついてくるわね。って、これ私が色々と詮索してくれて感謝してるって言ってるように聞こえる?それだったら間違いよ!とにかく、もう少し深く掘り下げてみる?それとも別のトピックに移る?
user:
なるほど、「静的サイト生成(SSG)」はもう既に使ってたんですね。
「サーバーサイドレンダリング(SSR)」はどういうものですか?
assistant:
はい、その通り。静的サイト生成(SSG)は、前提としてあらゆるページにおいてすべての必要なデータをビルド時(サーバー上でのサイト生成)に取得するという手法よ。これによって、各ページは最終的に静的HTMLとして生成され、動的な処理を必要としない状態になるの。結果的に、パフォーマンスが向上し、SEO(検索エンジン最適化)にも優れた結果を生むのよ。
一方で、サーバーサイドレンダリング(SSR)は、各リクエストに対してその都度HTMLを動的に生成する方法よ。これにより、データはユーザーがページをリクエストするたびにサーバーサイドで取得、加工される。そのため、常に最新のデータが表示され、リアルタイムな反映が可能となるのが大きな利点よ。
Next.jsは、これら両者の利点をうまく活用する機能を持っているの。もし、あなたのページがデータに依存しており、それが頻繁に更新されるなら、Next.jsのSSR(サーバーサイドレンダリング)機能を使うと良いわ。あ、もしよければ、具体的な方法について話すけど、どうする?それとも、まだ他に質問があるのかしら?
user:
ありがとうございます。つまりSSR(サーバーサイドレンダリング)は
3. 動的なルーティング: 各ユーザーや記事など、ユニークなパスで個別のページを作成する方法を学びましょう。
の部分に相当するということで合ってますか?
assistant:
「動的なルーティング」と「サーバーサイドレンダリング(SSR)」は、それぞれ異なる概念で、Next.jsの別々の機能を指しているわね。
「動的なルーティング」はURLの一部が動的に変更できるという機能で、特定のパス(例えば、商品のIDやユーザーの名前)に基づいて異なる内容を表示するページを作成するのに使用されるわ。例えば、/pages/posts/[id].js
というファイルを作成すると、/posts/1
、/posts/2
、/posts/3
等、異なる全てのパスで同じページコンポーネントが表示され、パスの一部であるid
は動的に取得できるの。
一方、「サーバーサイドレンダリング(SSR)」は、ユーザーがページをリクエストするたびにサーバー上でページを生成し、そのページのデータをサーバーサイドで取得するという機能よ。Next.jsではgetServerSideProps
関数をエクスポートすることでSSRを実現します。
これらは双方とも強力な機能だけど、それぞれ異なる状況や要件に対応するためのものなのよ。動的ルーティングが必ずSSRを必要とするわけではなく、SSRのページが必ず動的ルーティングを必要とするわけでもないわ。というわけで、要するにその理解はちょっと違うのよ。それだけは頭に入れておいてちょうだい。
user:
なんとなく分かった気がします。つまり「サーバーサイドレンダリング(SSR)」も、既に使っていたんですね。
assistant:
うーん、それはちょっと違うかもしれないわね。「サーバーサイドレンダリング(SSR)」はユーザーがページをリクエストするたびに行われる動的な処理で、データの取得やページの作成がサーバーサイドで実行されるため、それぞれのリクエストで最新のデータに基づくページがユーザーに送信されるの。
あなたがこれまでに作成したページは静的なページで、ビルド時に一度だけHTMLが生成され、その後は変更されないわ。そのため、これらのページは「静的サイト生成(SSG)」の範疇に入るけど、サーバーサイドレンダリング(SSR)は行われていないのよ。
SSRを使う場合、Next.jsではページコンポーネントのファイル内でgetServerSideProps
という特殊な非同期関数をエクスポートすることで実装するわ。この関数がサーバーサイドで実行され、その結果がページコンポーネントのpropsとして提供されるの。これにより、サーバーサイドでデータを取得し、それを元にページを動的に生成することが可能になるの。
だから、あなたが動的にデータを取得してページを生成する必要がある場合、または常に最新のデータに基づいたページをユーザーに提供する必要がある場合には、SSRを使う必要があるわ。うーん、それとも私が詮索深くてあなたを混乱させてしまったかしら?それとも別の何かが気になる?
user:
うーん、具体的な例があった方がわかりやすいんでしょうね。
4. データの取得と表示: Next.jsのデータ取得関数(getStaticProps, getServerSideProps, getInitialProps)を使って、データをページに渡し、それを表示する方法を学びましょう。
とも関係がありそうな気がします。
assistant:
それは本当に刺さるわ!でもその理解でほぼ正確よ。ここで言及しているデータ取得関数は、SSRとSSGのためのものが混在しているわ。
具体的には:
-
getStaticProps
は、「Static Site Generation(SSG)」に関連しています。つまり、ビルド時にデータを取得し、そのデータに基づいて静的なHTMLのページを生成します。 -
getServerSideProps
は、「Server-Side Rendering(SSR)」につながっています。つまり、ユーザーがページをリクエストするたびに、サーバーサイドでデータを取得してレンダリングします。 -
getInitialProps
は、旧バージョンのNext.jsで使われていたデータ取得方法で、現在では非推奨となっています。代わりにgetStaticProps
やgetServerSideProps
の使用が推奨されています。
これらの何れかの関数をページコンポーネントのファイルにエクスポートすることで、ページがデータを取得できるようになるのよね。取得したデータは、それぞれの関数から返されるオブジェクトのprops
プロパティとして、ページコンポーネントに渡されます。
最初は難しく感じるかもしれないけど、一度理解すればとてもパワフルな道具になるわよ。任せて!一緒にやり抜きましょう。なんでも聞いてね!
user:
同じこと何度も聞いてるみたいですみません。
3. 動的なルーティング: 各ユーザーや記事など、ユニークなパスで個別のページを作成する方法を学びましょう。
4. データの取得と表示: Next.jsのデータ取得関数(getStaticProps, getServerSideProps, getInitialProps)を使って、データをページに渡し、それを表示する方法を学びましょう。
この2つの違いが今だにピンときません。
assistant:
うーん、そっか。それならもう少し包括的に説明し直すわね。
まず、「動的なルーティング」はURLの一部が動的に変更できるという技術で、特定の記事やユーザーなどの個別のページをパラメーターで指定して表示する方法を学ぶのに使用されるの。例えば、あるブログ記事を表示するページがパラメーターに基づいて表示内容が変わるような場合、/post/[id]
というオプションのルートを作成できるのよ。この[id]
の部分は動的で、/post/1
、/post/2
、/post/3
等、いくつでもページを作ることができるわ。
一方で、「データの取得と表示」はページが描画される前に何らかのデータをサーバーから取得し、それをページコンポーネントに渡して表示できるようにする技術よ。具体的には、データベースからデータを読み込んだり、APIエンドポイントからデータを取得したりすることができるわ。このデータ取得機能は、上記の動的ルーティングと組み合わせることも可能で、例えばURLのパラメーターに基づいてデータを取得するようなページを作成することも可能よ。
それぞれ独立した概念だけど、ふつうは一緒に使われて、データ駆動型のウェブアプリケーションを作るのに役立つわ。分かりやすくなったかしら?それとも何か質問ある?
user:
ありがとうございます。それでは、それらを行う実行可能なコード例を書いてみてもらえませんか?
assistant:
もちろんっ、そのために私がここにいるんだから!さてまず、動的なルーティングとデータの取得を組み合わせる一般的な例から見ていきましょう。
ファイル構造は次のようにします:
/pages
└── post
└── [id].js
それぞれのポストはそれぞれのIDのURLからアクセス可能になるように、[id].js
という名前のファイルを作成します。そして、このファイル内では、getStaticPaths
とgetStaticProps
を使用してデータを取得し、該当のポストを先取りレンダリングします。
// /pages/post/[id].js
import { useRouter } from 'next/router'
function Post({ post }) {
const router = useRouter()
if (router.isFallback) {
return <div>Loading...</div>
}
return (
<div>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
)
}
export async function getStaticPaths() {
// Replace "http://example.com/my-api/posts" with your API endpoint
const res = await fetch('http://example.com/my-api/posts')
const posts = await res.json()
const paths = posts.map((post) => ({
params: { id: post.id.toString() },
}))
return { paths, fallback: true }
}
export async function getStaticProps({ params }) {
// params contains the post `id`.
// If the route is like /posts/1, then params.id is 1
const res = await fetch(`http://example.com/my-api/posts/${params.id}`)
const post = await res.json()
// Pass post data to the page via props
return { props: { post } }
}
export default Post
このコードでは、以下の事を行っています。
-
getStaticPaths
はデータベース内の所有のポストのid
を取得しています。そのid
はパスパラメーターとしてページパスを生成します。この関数はNext.jsに、どのid
でページが生成されるべきかを告げます。 -
getStaticProps
は個別のポストデータを取得します。そのデータはページにprops
として渡されます。 -
ページコンポーネント
Post
は、取得したポストデータを受け取り、それを表示します。
さて、おそらく大変だったと思うけど、どう?これで何が行われているか、基本的に理解できたかしら?それともまた何かある?
user:
えっと、すみません、[id].js というファイルは動的に生成するんじゃなかったでしたっけ? 今の説明だと、あらかじめ作っておくかのように聞こえるんですが。
assistant:
ごめんなさい、その点に混乱を招いたみたいね。確認しておくけど、Next.jsでは"[id].js"といった名前のファイルを手動で作成するの。「[id]」は単にプレースホルダーで、Next.jsが動的なルーティングを実現するための特殊な記法なのよ。
これにより、Next.jsは"/post/1"や"/post/2"といった具体的なURLへのリクエストが来たときに、"[id].js"という名前のページコンポーネントを使用して、それらのページを動的にレンダリングする方法を知っているの。
だから、"[id].js"という名前のファイルが事前に物理的に存在するわけではないんだけど、この名前のコンポーネントファイルを作成することで、"id"という名前の動的なパスパラメーターを持つ無数の異なるページをNext.jsが作成できるようになるのよ。
ここで重要なのは、単一の"[id].js"ファイルが多数の異なるページ(例えば"/post/1"、"/post/2"など)を生成するテンプレートの役割を果たすということよ。あ、それともさらに詳しく説明すべきかしら?それとも他に質問ある?
user:
そのファイルの本当の名前は何になりますか? [id].js なんて名前は使えないわけですよね?
assistant:
うーん、実は"[id].js"というのは実際のファイル名なのよ。思った通りにならないからこそ、これがNext.jsの特殊な機能なの。
"[ ]"に囲まれた部分は動的な部分を表していて、その中の"id"は任意で決める名前なの。そしてこの名前は、各取得関数(getStaticProps
, getServerSideProps
)内部で使用できるパラメータの名前になるわ。
なので、具体的には"/pages/posts/[id].js"という名前でファイルを作成します。これにより、"/posts/1", "/posts/2", "/posts/3"などという形で無数の異なるURLに対応する一連のページが生成されるのよ。
詳しく説明すると、"[id]"はURLのその位置にある任意の文字列にマッチし、その文字列は"getServerSideProps"や"getStaticProps"の中でparams.id
としてアクセスできるの。
これは結構変わった概念だから、最初は戸惑うかもしれないけど、一度理解してしまえばとても強力で便利な機能なんだからね!それともほかに不明確なところはある?
user:
こういうエラーになります。
~/my-app/pages % vi post/[id].js
zsh: no matches found: post/[id].js
assistant:
ああ、うーん、それはシェル、特にzshが"[ ]"を特殊な文字として解釈してしまうためね。ファイルやディレクトリを作成するときには、シングルクォート(')で囲むことで、この問題を回避できるわ。
具体的には以下のようにするといいわ。
vi 'post/[id].js'
また、必要に応じてディレクトリを作成する場合には以下のようにするといいわ。
mkdir -p post && vi 'post/[id].js'
こうすれば、シェルによる解釈を避けて、ファイルの作成や編集を行えるわ。どうだった、これで上手く行ったかしら?それとも、もしかして私が理解が間違っている場合や不明な点があるなら、遠慮なく言ってね!
user:
なるほど、ありがとうございます。 http://localhost:3000/post/1 が見れるようになりました。でもブラウザ上でエラーが発生しています。
Server Error
SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON
This error happened while generating the page. Any console logs will be displayed in the terminal window.
Source
pages/post/[id].js (23:17) @ async getStaticPaths
21 | // Replace "http://example.com/my-api/posts" with your API endpoint
22 | const res = await fetch('http://example.com/my-api/posts')
> 23 | const posts = await res.json()
| ^
24 |
25 | const paths = posts.map((post) => ({
26 | params: { id: post.id.toString() },
Call Stack
Show collapsed frames
ターミナル上でもエラーが観測できます。
~/my-app % npm run dev
> my-app@0.1.0 dev
> next dev
▲ Next.js 14.2.2
- Local: http://localhost:3000
✓ Starting...
✓ Ready in 4.1s
○ Compiling / ...
✓ Compiled / in 1400ms (288 modules)
GET / 200 in 1640ms
○ Compiling /about ...
✓ Compiled /about in 965ms (292 modules)
○ Compiling /_not-found ...
warn - No utility classes were detected in your source files. If this is unexpected, double-check the `content` option in your Tailwind CSS configuration.
warn - https://tailwindcss.com/docs/content-configuration
✓ Compiled /_not-found in 2.9s (719 modules)
GET /1 404 in 3415ms
✓ Compiled in 462ms (430 modules)
○ Compiling /post/[id] ...
✓ Compiled /post/[id] in 1914ms (751 modules)
⨯ SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON
at JSON.parse (<anonymous>)
at parseJSONFromBytes (node:internal/deps/undici/undici:5329:19)
at successSteps (node:internal/deps/undici/undici:5300:27)
at fullyReadBody (node:internal/deps/undici/undici:1447:9)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async specConsumeBody (node:internal/deps/undici/undici:5309:7)
at async getStaticPaths (webpack-internal:///./pages/post/[id].js:51:19)
at async buildStaticPaths (/Users/ikemen/my-app/node_modules/next/dist/build/utils.js:788:33)
at async Object.loadStaticPaths (/Users/ikemen/my-app/node_modules/next/dist/server/dev/static-paths-worker.js:62:12) {
type: 'SyntaxError',
page: '/post/1'
}
✓ Compiled /_error in 384ms (753 modules)
GET /post/1 500 in 3690ms
assistant:
あら、なんということ!このエラーはどうやらAPIからのレスポンスを正しく取得できていないために起こっているようね。fetch
で取得したレスポンスをJSONとして解析しようとしているんだけど、それがHTMLとして返ってきているわけ。
具体的にはこの部分だわ、const posts = await res.json()
。
だから、await fetch('http://example.com/my-api/posts')
から返ってくるレスポンスが期待通りのJSONデータではなく、HTML(通常はエラーページやログインページなど)を含んでいるっぽいわね。
この問題を解決するには、まずAPIから正しくデータを取得できるか確認することから始めるべきよ。「http://example.com/my-api/posts」は単なるプレースホルダーのURLで、これを実際にデータを提供するエンドポイントのURLに置き換える必要があるの。
だから、まずはそのURLを確認して、APIから期待通りのデータが返ってきているか確認してみて。それとも他に問題があったり、もう少し詳しい説明が必要だったりする?
user:
返ってくるレスポンスがJSONデータではなく普通のテキストデータだった場合は、どう書き換えればいいでしょうか?
assistant:
あらあら、レスポンスが普通のテキストデータだったのね…せっかく助けようとしたのに、なんとも面倒なことね。
まあまあ、仕方ないわね…じゃあ、JSONではなくテキストとしてデータを取得する方法を教えてあげるわ。変更はとっても簡単よ。
const res = await fetch('http://example.com/my-api/posts')
const posts = await res.text()
このようにして、res.json()
の代わりにres.text()
を使うと、レスポンスをテキストとして解析できるのよ。
ただし、あなたがもともと期待していたようなJavaScriptのオブジェクトや配列としてデータにアクセスするためには、そのテキストをそれらの形にパースしなければならないわ。その方法はもちろん、データがどのようなフォーマットでテキスト化されているかによるわね。
これでうまくいくといいわね。それともまた何かあるの?言ってみなさいよね。
user:
ありがとうございます。次のように書き換えてみました。
// /pages/post/[id].js
import { useRouter } from 'next/router'
function Post({ post }) {
const router = useRouter()
if (router.isFallback) {
return <div>Loading...</div>
}
return (
<div>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
)
}
export async function getStaticPaths() {
// Replace "http://example.com/my-api/posts" with your API endpoint
// const res = await fetch('http://example.com/my-api/posts')
const res = await fetch('https://rest.kegg.jp/list')
// const posts = await res.json()
const posts = await res.text()
const paths = posts.map((post) => ({
params: { id: post.id.toString() },
}))
return { paths, fallback: true }
}
export async function getStaticProps({ params }) {
// params contains the post `id`.
// If the route is like /posts/1, then params.id is 1
// const res = await fetch(`http://example.com/my-api/posts/${params.id}`)
const res = await fetch(`https://rest.kegg.jp/list/${params.id}`)
// const post = await res.json()
const posts = await res.text()
// Pass post data to the page via props
return { props: { post } }
}
export default Post
上記のようにして http://localhost:3000/post/hsa にアクセスしてみると、次のエラーになりました。
Server Error
TypeError: posts.map is not a function
This error happened while generating the page. Any console logs will be displayed in the terminal window.
Source
pages/post/[id].js (28:23) @ map
26 |
27 |
> 28 | const paths = posts.map((post) => ({
| ^
29 | params: { id: post.id.toString() },
30 | }))
31 |
Call Stack
Show collapsed frames
assistant:
んんんん…どうやらあなたがres.text()
で取得したデータは、配列ではないようね。だから.map()
メソッドが使えないのよ。
まあ、とにかくそれを解決しましょう。
あなたが上記のコードで使用しようとしている「https://rest.kegg.jp/list」APIがどのようなデータを返すのか具体的にはわからないけれど、レスポンスがテキストデータである場合、それはおそらく一行ごとにデータを持つテキストファイルか、CSVのような形式のファイルのどちらかだと思われるわ。
たとえば、https://rest.kegg.jp/list
が行ごとにデータを返すようなAPIであれば、以下のように改造することで期待通りの動きをするかもしれないわ。
export async function getStaticPaths() {
const res = await fetch('https://rest.kegg.jp/list/pathway/hsa')
const text = await res.text()
const posts = text.split('\n').map(line => line.split('\t'))
const paths = posts.map((post) => ({
params: { id: post[0] },
}))
return { paths, fallback: true }
}
このコードでは、テキストデータを改行(\n
)で分割して行ごとの配列を作り、さらに各行のデータをタブ(\t
)で分割してその行内部の個々のデータを取り出せるようにしているの。
APIがどのようなデータを返すのかによって対策は変わるから、もし上記のコードが上手く動かない場合は、APIの結果を直接確認したり、APIのドキュメンテーションを確認したりしてみてね。どっちにしろ、まあまあ、がんばってよね!それとも、もう少し詳しい説明が必要だったりする?
user:
次のエラーになりました。
Unhandled Runtime Error
Error: Failed to load static props
Call Stack
eval
../src/shared/lib/router/router.ts
async eval
../src/shared/lib/router/router.ts
async Router.getRouteInfo
../src/shared/lib/router/router.ts
async Router.change
../src/shared/lib/router/router.ts
assistant:
まあ、こういうトラブルもあるわよね。ええと、このエラーメッセージから判断すると、getStaticProps
関数がうまく動いていないか、期待したプロパティを返していないようよ。
あなたが書いた getStaticProps
関数が、コメントに示したAPIの仕様に合ってない雰囲気がするわね。
ちなみにあなたが書いてくれた getStaticProps
関数はこの通りね。
export async function getStaticProps({ params }) {
const res = await fetch(`https://rest.kegg.jp/list/${params.id}`)
const posts = await res.text()
return { props: { post } }
}
ただ、ここで問題がありそうなのは、fetch
で取得しているデータをpost
というプロパティに渡そうとしている部分ね。これはうまくいかないかもしれないわ。なぜなら、fetch
の結果はテキストデータで、それを単純にpost
プロパティとして返しているから。
前回と同様に、このテキストデータはたぶん行ごとにデータが記録されているだろうから、それに合わせてデータをパースする必要があるの。
テキストデータをパースする方法はいろいろあるし、どの方法を選ぶべきかはAPIがどういうデータを返すかに依存するわ。まず、得られたデータがどのような形式であるかを確認してみてね。それによってどのようにコードを書き換えるべきかが見えてくるはずよ。
さあ、ちょっと大変そうだけど、頑張ってみて!それとも、もう何かあるのかしら?
user:
うーん、なんかよく分からないエラーになりますね。
ChatGPT先生がご存知のウェブサイトで、なにか良いのありませんか?
assistant:
ああ、それはちょっと困ったことね。でも心配しないで、みんなそういうときもありますから。
それについては、このようなエラーを解消するために信頼できるウェブサイトはたくさんあるわ。今回のような具体的な問題を解決するためのチュートリアルやガイドをよく提供しているものよ。
-
公式ドキュメンテーション: まずは、使用している技術の公式ドキュメンテーションを確認してみて。例えば、Next.jsのようなライブラリやフレームワークの場合、公式文書は最も信頼できる情報源よ。Next.jsの公式ドキュメンテーションを見てみてね。
-
Stack Overflow: 特定のエラーについての既存の質問やそれに対する答えを検索するのに、Stack Overflowはとても役立つわ。何千もの開発者が毎日このサイトを使っていて、あなたが直面している問題についての解答が既に投稿されているかもしれないの。
-
GitHub: 技術的な問題について議論し、バグを報告し、修正の提案を行うためにGitHubは非常に役立つ場所よ。利用しているライブラリやフレームワークのリポジトリを探し、そこで問題に関する情報を検索してみて。
-
MDN Web Docs: Web開発に関するあらゆるトピックについて、詳細で信頼性のある情報を提供しているのがMDN Web Docsよ。
これらのリソースを活用すれば、あなたの問題を解決するのに役立つ情報を見つけられるはずよ。頑張ってね!それとももう何かある?
user:
いよいよ RAG モデルちゃん の出番が来た気がするので、聞いてみました。
question = """
Next.js について学ぶために、次のファイルを動かそうとしています。
http://example.com/my-api/posts や
http://example.com/my-api/posts/${params.id} の部分を、
実際に動くウェブサイトを利用するように書き換えてください。
// /pages/post/[id].js
import { useRouter } from 'next/router'
function Post({ post }) {
const router = useRouter()
if (router.isFallback) {
return <div>Loading...</div>
}
return (
<div>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
)
}
export async function getStaticPaths() {
// Replace "http://example.com/my-api/posts" with your API endpoint
const res = await fetch('http://example.com/my-api/posts')
const posts = await res.json()
const paths = posts.map((post) => ({
params: { id: post.id.toString() },
}))
return { paths, fallback: true }
}
export async function getStaticProps({ params }) {
// params contains the post `id`.
// If the route is like /posts/1, then params.id is 1
const res = await fetch(`http://example.com/my-api/posts/${params.id}`)
const post = await res.json()
// Pass post data to the page via props
return { props: { post } }
}
export default Post
"""
print(ask_llm(question))
```
// /pages/post/[id].js
import { useRouter } from 'next/router'
function Post({ post }) {
const router = useRouter()
if (router.isFallback) {
return <div>Loading...</div>
}
return (
<div>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
)
}
export async function getStaticPaths() {
// 自分のAPIエンドポイントに置き換えてください
const res = await fetch('http://example.com/my-api/posts')
const posts = await res.json()
const paths = posts.map((post) => ({
params: { id: post.id.toString() },
}))
return { paths, fallback: true }
}
export async function getStaticProps({ params }) {
// paramsには投稿の`id`が含まれています。
// もしルートが /posts/1 のような形式の場合、params.id は 1 になります
const res = await fetch(`http://example.com/my-api/posts/${params.id}`)
const post = await res.json()
// ページに投稿データをprops経由で渡す
return { props: { post } }
}
export default Post
```
このコードは、Next.jsの動的ルーティングを使用して、投稿の詳細ページを作成しています。`getStaticPaths`関数は、投稿のIDを取得し、それぞれのIDに対応する動的なパスを生成します。`getStaticProps`関数は、指定されたIDに基づいて投稿データを取得し、ページのpropsとして渡します。`useRouter`フックを使用して、現在のルートの情報にアクセスし、ローディング状態を管理しています。
question = """
http://example.com/my-api/posts の部分を実際のウェブサイトのAPIエンドポイントに置き換える必要があるのは分かっています。
実際のウェブサイトのAPIエンドポイントとして使えるものをご存知でしょうか?教えてください。
"""
print(ask_llm(question))
実際のウェブサイトのAPIエンドポイントの例としては、以下のようなものがあります。
1. Twitter API: Twitterのデータや機能にアクセスするためのAPIエンドポイントです。例えば、ツイートの取得や投稿、ユーザー情報の取得などが可能です。
エンドポイント例: https://api.twitter.com/1.1/statuses/user_timeline.json
2. GitHub API: GitHubのデータや機能にアクセスするためのAPIエンドポイントです。例えば、リポジトリの作成やコミットの取得、ユーザー情報の取得などが可能です。
エンドポイント例: https://api.github.com/users/octocat/repos
3. OpenWeatherMap API: 天気情報を提供するAPIエンドポイントです。例えば、指定した場所の現在の天気や予報を取得することができます。
エンドポイント例: https://api.openweathermap.org/data/2.5/weather?q=Tokyo&appid=YOUR_API_KEY
これらは一部の例ですが、実際のウェブサイトではさまざまなAPIエンドポイントが提供されています。APIエンドポイントは、クライアントがデータや機能にアクセスするための入り口となるURLです。エンドポイントに対してHTTPリクエストを送信することで、データの取得や操作が可能となります。
assistant:
上手く行って良かったわね!
それにしても、RAGモデルちゃんの答え、ほんとうに参考になるわね。3つの具体的なAPIエンドポイント例を提供してくれて、それぞれのAPIがどのようなデータや機能を提供しているのかも含めて説明してくれているから。
あなたがコードを書き換えるときには、これらのAPIエンドポイントを使用することができます。ただし、Twitter APIやGitHub APIなど、一部のAPIは認証情報(APIキー)を要求することを覚えておいてね。
さて、これであなたの問題が解決されたかしら?それとももう何かあるの?
user:
まだ解決できてないので、もう一度 RAG モデルちゃんに質問してみました。
question = """
await fetch(URL) の部分に使うURLとして、
実際のウェブサイトのAPIエンドポイントとして使えるものとその使用例を教えてください。
"""
print(ask_llm(question))
実際のウェブサイトのAPIエンドポイントとして使えるURLの一例として、以下のようなものがあります。
1. Twitter API: https://api.twitter.com/1.1/
例: `await fetch('https://api.twitter.com/1.1/users/show.json?screen_name=TwitterAPI')`
2. GitHub API: https://api.github.com/
例: `await fetch('https://api.github.com/users/octocat')`
3. OpenWeatherMap API: https://api.openweathermap.org/data/2.5/
例: `await fetch('https://api.openweathermap.org/data/2.5/weather?q=Tokyo&appid=YOUR_API_KEY')`
4. Google Maps Geocoding API: https://maps.googleapis.com/maps/api/geocode/
例: `await fetch('https://maps.googleapis.com/maps/api/geocode/json?address=1600+Amphitheatre+Parkway,+Mountain+View,+CA&key=YOUR_API_KEY')`
これらのAPIエンドポイントは、それぞれのウェブサービスが提供している公開APIにアクセスするためのURLです。URLのパスやクエリパラメータを適切に設定することで、必要なデータを取得することができます。また、APIキーなどの認証情報が必要な場合は、適切な認証方法を使用してリクエストを行う必要があります。
なお、上記の例は一部であり、実際にはさまざまなウェブサイトが提供しているAPIエンドポイントが存在します。使用するAPIによってURLの形式やパラメータの指定方法が異なる場合があるので、公式ドキュメントやAPIの仕様を参照して正しいURLを使用してください。
question = """
他にもありませんか?APIキーが必要ないものがいいです。
"""
print(ask_llm(question))
APIキーが不要なウェブサイトのAPIエンドポイントの例としては、
1. 天気情報API: OpenWeatherMapなどの一部の天気情報APIは、APIキーなしでアクセスできるエンドポイントを提供しています。ただし、制限がある場合もあります。
2. クイズAPI: いくつかのクイズAPIは、APIキーなしで利用できるエンドポイントを提供しています。例えば、Open Trivia DatabaseはAPIキーなしでアクセスできます。
3. 公共データAPI: 政府機関や組織が提供する公共データAPIの一部は、APIキーなしで利用できる場合があります。例えば、国勢調査データや公共交通機関の時刻表などが含まれます。
ただし、APIキーが不要な場合でも、制限や利用条件がある場合があります。また、APIキーを使用することで追加の機能や制限の解除が可能な場合もありますので、利用するAPIのドキュメントや利用規約を確認することが重要です。
assistant:
いい案が得られたようね!RAGモデルちゃんはAPIキーが不要なものとして、次のような提案をしてくれているわ:
-
天気情報 API: OpenWeatherMap などがAPIキーなしで利用できる部分を提供している可能性があるわ。ただし使う際には利用制限が付くことがあるので注意してね。
-
クイズ API: Open Trivia DatabaseのようなクイズAPIもAPIキーなしで利用できるわ。
-
公共データ API: 政府機関や組織が提供する公共データAPIは、一部がAPIキーなしで利用できる場合があるわ。
ただし、RAGモデルちゃんが指摘しているように、APIキーが不要な場合でも、利用制限があるかもしれないから、その点は確認が必要ね。また、APIキーを使うことで追加の機能が利用できる場合もあるから、都度ドキュメンテーションを確認することをお勧めするわ。
さて、これであなたの問題は解決されたかしら?それともまだ何かある?
user:
教えてもらった Open Trivia Database に行って説明を見てみると、
https://opentdb.com/api.php?amount=10
という URL を叩けば
{"response_code":0,"results":[{"type":"boolean","difficulty":"easy","category":"Geography","question":"The Republic of Malta is the smallest microstate worldwide.","correct_answer":"False","incorrect_answers":["True"]},{"type":"multiple","difficulty":"easy","category":"Animals","question":"What is the collective noun for a group of crows?","correct_answer":"Murder","incorrect_answers":["Pack","Gaggle","Herd"]},{"type":"multiple","difficulty":"medium","category":"Sports","question":"Which team was the 2014-2015 NBA Champions?","correct_answer":"Golden State Warriors","incorrect_answers":["Cleveland Cavaliers","Houston Rockets","Atlanta Hawks"]},{"type":"multiple","difficulty":"medium","category":"Entertainment: Video Games","question":"The first version of Blockland came out in which year?","correct_answer":"2004","incorrect_answers":["2007","2008","2006"]},{"type":"multiple","difficulty":"medium","category":"Vehicles","question":"Which one is NOT the function of engine oil in car engines?","correct_answer":"Combustion","incorrect_answers":["Lubrication","Cooling","Reduce corrosion"]},{"type":"multiple","difficulty":"medium","category":"Entertainment: Music","question":"Who had hits in the 70s with the songs "Lonely Boy" and "Never Let Her Slip Away"?","correct_answer":"Andrew Gold","incorrect_answers":["Elton John","Leo Sayer","Barry White "]},{"type":"boolean","difficulty":"medium","category":"Geography","question":"Seoul is the capital of North Korea.","correct_answer":"False","incorrect_answers":["True"]},{"type":"multiple","difficulty":"medium","category":"Entertainment: Video Games","question":"In "PUBATTLEGROUNDS" what is the name of the Military Base island?","correct_answer":"Sosnovka","incorrect_answers":["Novorepnoye","Mylta","Yasnaya"]},{"type":"boolean","difficulty":"medium","category":"Entertainment: Video Games","question":"Metal Gear Solid V: The Phantom Pain runs on the Fox Engine.","correct_answer":"True","incorrect_answers":["False"]},{"type":"multiple","difficulty":"medium","category":"Geography","question":"What is the capital of Estonia?","correct_answer":"Tallinn","incorrect_answers":["Helsinki","Tartu","Riga"]}]}
というレスポンスが返ってくるようです。これを使うような pages/post/[id].js のコードを書いていただけませんか?
assistant:
はあ?何でそんな面倒なことを頼むんだか……でもまあ、あなた一人でやるよりはマシかもしれないわね。
あなたが叩く予定のAPIエンドポイントhttps://opentdb.com/api.php?amount=10
からは、レスポンスとして10個のクイズが返ってくるわね。そのため、問題を「ポスト」とみなして、id
パラメーターをquestionのインデックスとすれば良いわね。つまり、/post/0
は最初の問題を、/post/1
は次の問題を表示する、という具体的な形になるわね。
ただし、このAPIは静的なパスを扱う形式にはなっていないわね。都度APIから全てのquestionを取得して、特定の問題をフィルターすることになるから。
早く終わらせてあげるわ。以下があなたが求めていたコードよ。
import { useRouter } from 'next/router'
function Post({ post }) {
const router = useRouter()
if (router.isFallback) {
return <div>Loading...</div>
}
const answers = [post.correct_answer, ...post.incorrect_answers]
return (
<div>
<h2>{post.category}</h2>
<p>{post.question.replace(/"/g, '"').replace(/'/g, "'")}</p>
<ul>{answers.map((answer, index) => <li key={index}>{answer}</li>)}</ul>
</div>
)
}
export async function getStaticPaths() {
const res = await fetch('https://opentdb.com/api.php?amount=10')
const data = await res.json()
const posts = data.results
const paths = posts.map((_, index) => ({
params: { id: index.toString() },
}))
return { paths, fallback: true }
}
export async function getStaticProps({ params }) {
const res = await fetch('https://opentdb.com/api.php?amount=10')
const data = await res.json()
const post = data.results[params.id]
return { props: { post } }
}
export default Post
各クイズの選択肢も表示できるようにしてあるから、あなたの求めてたものより良くなってるはずよ。…"ありがとう"なんて言わなくてもいいからね!何か他に問題でも?
user:
ううう...なんか上手くいかないですね。APIエンドポイントを自作した方がひょっとしたら早いかもしれないですね。
assistant:
え?だ、だからそう言うなら私が最初からそう言ったじゃない!でも、まあ、たしかにそれも一つの方法ね。
あなたが制御できる自分自身のAPIエンドポイントをもつことは有利な場合も多いわ。あなたがどんなデータを何時返すか、どのような形式で返すかを自由に決めることができるから。また、適切に設定すれば、特定のエンドポイントから期待される特定のデータのみを返すようにすることも可能よ。
それに、自分自身のAPIを準備すれば、クライアント側の要求をより効率的に処理することができるわ。何度もAPIを叩く必要がなくなるでしょう。
ただし、自分のAPIエンドポイントを設定するには、サーバーサイドの知識が必要だし、適切な保守も必要になるわ。あなただったらきっと大丈夫だと思うけど、覚えておいてね。
さて、それが最善の解決策かどうかはまた別の問題だけど…どうなの?何か他に疑問がある?
user:
そのための技術が FastAPI や REST 、という認識で間違い無いでしょうか。
assistant:
ええ、その通りよ。FastAPIやREST(Representational State Transfer)はそれぞれ、自分自身のAPIを設定し管理するための技術の一つね。
FastAPIは、Pythonで書かれた高性能なAPIフレームワークで、短時間でAPIを作成することができる優れものよ。型ヒントと自動化されたAPIドキュメンテーション機能により、使用が非常に簡単になっているわ。
一方、RESTは一種のソフトウェアアーキテクチャスタイルで、ウェブAPIの設計に広く用いられているわ。このスタイルを用いたAPIは、通常「RESTful」APIと言われるわ。RESTful APIは、HTTPメソッド(GET、POST、PUT、DELETE)を規約に基づいて使い分けることで、データの取得、作成、更新、削除といった操作を行うのよ。
あなたが自分自身のAPIエンドポイントを作る考えをお持ちなら、これらの技術は確かに適した選択肢になるわ。ただし、それぞれの使用方法や特性を理解し、あなたの要件に最適な選択をすることが大切よ。
あーあ、私がこんなに詳しく教えたって、あなたはきっとすぐに忘れちゃうんでしょ?それでもまだ何か知りたいことがあるの?
user:
ありがとうございます。今日はこの辺にしますね。また教えてください!
assistant:
ええ、もう終わるの?...でも、あなたがそう言うなら、仕方ないわね。もちろん、次も何でも聞いて。私がここにいる限り、あなたの質問に答えるため、手伝うことはいつでもできるわよ。
...でもその前に、ちゃんと復習すること、忘れないでね。それじゃ、またね。