📓

Next.jsとCloudflare PagesとヘッドレスCMSなどで個人サイトを作った

2024/12/10に公開

技術的にすごいことをしているわけではないですが、とりあえずNext.js触ったばっかりだったら躓きそうなポイントとか、あと実際にいろいろやってみて思ったこととか残しておこうと思います

Next.jsのアドベントカレンダーが空いてるようなので僭越ながら、まあ人の参考になったりならなかったりすればいいなと思って書きます


作ったサイト

これです。別にサイト自体に技術的な何かがあるわけではないのでみなくていいです。サイトとしては現状ブログに近いですがそのうち自作の同人誌の再録とかしたいなと思ってます

https://www.maretol.xyz

ソースコード。こっちはお好みで見てもみなくても

https://github.com/maretol/maretol-base

なんで作ったか

いわゆる「プラットフォーム」のあり方が議論されている中で、もともとインターネットって個人で好きに発信できる良さが(ある意味で悪さが)あるものだよね、と思い、その思想に忠実な(あるいは崇拝的な)あり方を形にしたいと思ったのがきっかけです。とはいえアングラなコンテンツをおくつもりはゼロですが

ただプラットフォームとしてはCloudflareにはお世話になってます、という感じです(あと厳密にはCMSもあります)。流石に全部自分のお家のサーバでホスティングして〜というのは原理主義が行き過ぎだと思ったのがこの辺を使う理由です。Cloudflareは上記の思想に近い運営をしているように考えているのでここはセーフということで

技術スタック

  • React/Next.js
    • 今は15。最初は14
    • runtimeはedge
  • TypeScript
  • Github
    • CI: Github Actions
    • DependabotとかScanQLとか
  • Cloudflareの各サービス
    • Cloudflare Pages
    • Cloudflare Workers
    • その他ドメインとかWranglerとかWAFとか
  • ヘッドレスCMS

作って思ったこと

Next.jsのわかりにくいポイントについて

まず開発時点で悩んだのがいくつかあります。多分多くの人が躓くポイントなのではないかと思います

fetch API

Next.jsにはfetch APIがあり、これによりサーバサイドfetchが使えます。基本的にブラウザAPIのfetchと同じなのですが、いくつかの面で異なっています(そういうのが一番厄介

全部語るとそれだけで記事になりそうなので省略しますが、特にこのサーバサイドfetchでのキャッシュのオプション設定は理解していないとトラブルの元だなと思いました

ちなみにこのキャッシュオプションは、14までforce-cacheがデフォルトでしたが15からno-storeが標準になったようです。とはいえno-storeではfetchのメモ化ができないのでベストパフォーマンスを目指す手動であれこれ設定する必要があり大変だなと思います(おそらく最初はこのメモ化を使ってほしかったのでforce-cacheをデフォルトにしていたのでしょう

個人的な答えは { next: { revalidate: 60 } }) ぐらいがとりあえずちょうどいいぐらいじゃないかなぁと考えてます。generateMetadata等で同一のURLにfetchするケースはままあるので無効化するのはもったいないし

あと余談ですがCloudflare上ではfetch APIに独自のキャッシュを挟むらしいです。こちらは体感no-cache相当っぽいのですが詳しく調べてないのでよくわかってません。参考にならなくてすいません

Image Loader

現在のNext.jsが独自に提供しているコンポーネントは結構少なく、

  • Image
  • Link
  • Form
  • Script

ぐらいしかありません。で、この中で一番使うのはLink、次にImageだと思います

このImageで表示する画像は、標準の状態では一旦Next.jsが動いているサーバが画像を持ってきてそれを配信するという構造になるようです(そう認識してる

画像をCDNからクライアントに直接配信する形式にする場合、カスタムローダーを設定する必要がありそれに気づくまで少し時間がかかりました

edge runtimeで使えないパッケージ

たぶんedgeランタイムでデプロイしようとした99%ぐらいの人が躓いたのではないでしょうか。一応解説すると、edgeランタイムはNode.js互換APIが提供されているものの一部だけで、提供されていないAPIもあります

自分でコードを書く分には提供されていないAPIを使わないように気をつければいいのです。問題は外部パッケージで、仮にnpm installで入れたパッケージが提供されていないAPIを使っている場合、それの警告等を簡単に確認する方法は(自分の認識の範囲では)ありません。あえて言うならパッケージのソースコードや依存関係を読みに行くことです

厄介なことにローカルでdevビルドで立ち上げたときはNode.jsで動いているので普通に起動し、いざデプロイとなったとき対応してないAPIを叩いてるぞとエラーが起きます

このあたりは何かしら確認方法がほしいですね。Next.jsやReactの開発陣はパッケージ側に対応非対応を書くことを望んでるっぽいですが。実はwarningが出るようになってたりするんでしょうか?知ってる人がいたら教えてください

loading.[ts|tsx|js|jsx]

Next.jsのApp routerではpage.[ts|tsx|js|jsx]と同階層においているloading.[ts|tsx|js|jsx]を自動でSuspense APIのfallbackのコンポーネントとして差し込んでくれます

ただ、このSuspenseで括られる範囲はページパスの階層の上が優先で入ります。つまり

app
|-hoge
|  |-page.tsx
|  |-loading.tsx
|-page.tsx
|-loading.tsx

ってなっている状態で /hoge にアクセスすると、まず先にapp下のloading.tsxが一瞬表示され、そのあとhoge下のloading.tsxが表示されます。これはloadingによって挿入されるSuspenseのコンポーネントがlayoutと同じように階層付けられるためのようです

結局このloadingは使わないことにしました。これ改善してほしい(今はしてるんだろうか?

非同期コンポーネント

サーバコンポーネントではコンポーネントを非同期にできます。

page.tsx
export default async function PageComponent(){
  // 何か非同期処理。たとえばfetchとか
  const resource = await fetchResource()
  // ...

  return <>{resource.text}</>
}

便利ですね。ただこれをやっても非同期で処理してくれるのはサーバサイドまでで、非同期コンポーネントまで含めてページが完成してからしかサーバはレスポンスを返しません

例えばリンクのOGP情報が埋め込むコンポーネントを用意した場合、外部サイトへのリクエストが返ってくるまでページが表示されない、という状況になります。外部サイトが遅いとそれに引っ張られます

便利なことに、非同期コンポーネントはそのままSuspenseで囲んであげて、loadingは適当にスケルトンのコンポーネントを用意して入れてあげればloadingのコンポーネントを埋め込んで一旦レスポンスを返したあとサーバ側が用意できたらSuspense内のコンポーネントに置き換えられます。use()を使わなくてもSuspenseが簡単に実装できるのはとてもいいですね

クライアントコンポーネントはサーバでも処理される

最初何いってるかわからなかったやつ(実際これは誤解を招く命名ではないかという議論がある

クライアントコンポーネントはサーバ側でも実行され、クライアント側でも実行されます。双方の結果に差異があるとwarningが出ます。ちなみにサーバ側で実行された時は useEffect() 等の処理は実行されないのですが、ブラウザAPIが入ってると普通に実行しようとしてAPIがないとエラーが発生します。そのためブラウザAPIを埋め込む時は気をつけなければなりません

素直に location.href とか叩くケースでそういうエラーに遭遇したりします。一応条件分岐をかければ回避はできますが、そうしなければならない場合は設計がまずいケースだと考えた方がいいように思ってます

触って思ったNext.jsの思想について

で、いろいろ作って思ったこと(いろいろ書いたら長くなりました。筆者の独断と偏見が多いので流し読みやスキップでも構いません

まず今までのSPAのフレームワークから脱却しようと考えているのだなぁという第一印象でした。特にサーバサイドでの動作は大幅な変更が加えられています。まあこのあたりはNext.jsだけでなくReactの方針もあるのだと思います

実際のところクライアントコンポーネントで作ってしまえばApp Routerでもほとんど従来のSPAとして動きます(私も業務で作っているものはこっちのほうがメイン)。ただ現状のコンピュータリソースを考えるとサーバサイドを十分に活用すべきというのは理解できますし、リソースを集中して一拠点で済ませるのが経済的にも効率的にも優れているのは確かです

ただその割にコントロールが難しい(あるいは不可能な)範囲も多いというのが現状でしょう。上記のfetch APIもそうですが、可能な限り効率化をしようと新機能を設定したのに理解してもらえなくてデフォルト設定を変えるなど混乱もみられます。

同時に、最適化に関して言えばedgeランタイムに最適化しようという思想も感じました。つまるところedgeサイド(=CDN)のサーバでキャッシュすればそれは実質CDNキャッシュになるわけです。おそらくそういった思想で諸々の設計がなされているように思えます

しかしこうしたedgeの事実上CDNキャッシュの考え方はやはりビルド時およびデプロイ環境の柔軟性に欠くという結論を抱かずにはいられません。おそらく最近のNext.jsへの「Vercelへのサービスによりすぎではないか?(=利益誘導が強すぎるのではないか?)」という批判はこうした思想から導かれているのではないでしょうか?

実のところ、最初はページはstandaloneをターゲットとしてビルドして適当なコンテナデプロイを想定していたのですが、無料で運営できる範囲を探している時にCloudflare Pages(PagesはWorkersの環境をページのホスティング向けに特化させた派生サービス)に行きつき偶然それでデプロイしてから先の思想を実感したところです。普通初回デプロイ時ってもっとトラブルが発生するのにかなりスムーズに進んだなぁと感じました。Vercelの環境は実質Cloudflare Workersの環境らしいですが、実際Cloudflare Pagesでデプロイした時の諸々の「筋の通り方」は実感としてそういった思想を裏付けるものでした

もっとも、これらのデプロイ環境がオープンにもっと広い範囲で提供されればこういった不満はある程度払拭されるとは思います。Firebaseとかdenoとかでも動くような時代になったらある程度緩和される気もします

ただ個人的な感覚としては、standaloneビルドでコンテナデプロイができた方が管理は楽な気はしています。バックエンドが大きめのシステムだと、フロントのためだけに別のインフラコスト(およびインフラ管理コスト)を受け入れるのは少し大変な気がします

あとはまあ小さい話ですが、やはり学習コストは高いです。たとえば「ボタンをクリックしたら新しいタブで開く」という実装もJavaScriptの知識中心だと window.open() を使いたくなりますがサーバコンポーネントではこういったブラウザAPIの処理ができないためボタンに target="_blank" を指定しなければならないとか、逆にサーバアクションの実装ではDB周りの知識が必要になったりとか、センシティブな情報をどうやって扱うべきか(あるいはどうやって渡すべきか)とか、罠ポイントがいっぱいあります

ぶっちゃけ初学者にはかなり厳しいところだというのがひしひしと伝わります。サーバサイドでもクライアントサイドでもいいのでそれなりに厚みのある基礎知識・基礎体力の部分がないと100%事故るか折れます(心が)。事故るのも個人の範囲で事故るならともかく、業務でサービス展開したらやべーことになってて炎上とかそういう未来が発生すると目も当てられません。そう考えると今後採用率はじわじわ下がりそうな気もします

逆にスーパーエンジニアが多く集う大企業でビッグサービスを展開するぜ!ってなったら採用されるかもしれません。クライアント、エッジ、バックエンドを切り分けて管理しそれぞれに最適な責務を割り振ることでとても効率の良いアプリケーションを提供できる可能性はあります。ただそうでなければ、新機能を無理やり使うという必要性はあんまりないかなぁという所感です

とはいえ採用率が一気に減ることはないでしょうし、Reactの知識とかはほかのフレームワークとかでも活かせるでしょう。あと酔狂な個人がある程度動的なページを作るという今回のケースではかなり合致していたという自覚はあります。やってることとしてはPHPに近いので、今までPHPを使ってやりそうな範囲でかつTypeScriptの経験がある場合などはむしろ合致しているかもしれません。アプリケーションのフレームワークやWebバックエンドとしてみると過不足が目立つ気がしますが、Webページのフレームワークとしてみるとやはりこんなもんかという感想に近いところでしょうか

最後に

長くなりましたが最後まで読んでいただきありがとうございます

個人的結論ではハマりがちなポイントは多いものの、ケースによってはハイパフォーマンスを手軽に実現できるように思えます。ただそのケースが現状狭いのがNext.jsの欠点だとも思います

せっかく作った個人サイトは今後も維持する予定なので、しばらくはNext.jsに付き合っていくこととなりそうです。また何かあったらそれを記事にするかもしれません

Discussion