Next.js の ISR/SSR で Contentful から直接 Netlify にデプロイしないブログ運用

目次

    Contentful からの デプロイ を無くし Netlify のビルドリソースを節約することで、無料枠を使い切る心配を減らすといった、ブログ記事執筆のモチベを損なわない方法です。

    前置き

    ブログのように記事を API として Contentful から受け取り Netlify で SSG としてビルドする際に、 Contentful の Netlify 用 Webhook を作成 -> publish するとデプロイ といった運用を行うことがあります。

    このような運用において

    • 記事を新たに作成
    • 誤字脱字の修正
    • 内容の更新

    などの編集を繰り返すと Webhook による デプロイのため、Netlify のビルドリソースを使い切りそうになるような状況が起こり得ます。特にブログ自体を作成している途中であれば、コード自体の push 量もそこそこにあるため、 Netlify のビルド上限には注意する必要があります。

    この状況を Next.js の ISR によって対策することを試みたので、その結果と得た Netlify での ISR についての知見をまとめました。

    対策の結果

    • Contentful の Deploy Webhook は必要なくなり、 Netlify のビルドリソースの節約は達成
    • しかし、問題として Netlify では ISR が行われない ことがわかった
      • 前回のアクセスから revalidate で指定した時間後再度アクセスがあると、取得されたデータを元にページ全体の再レンダリングが行われる
      • Netlify では ISR が SSR と似た挙動になる
      • ISR による UX 改善という名目ではほとんど効果がない

    以上のように見た目はエラーなく動作はしていますが、思った ISR の挙動と違ったためそのことについても触れていきたいと思います。

    実装

    Netlify での ISR の挙動に触れる前に、このブログにおける実装についてです。

    Incremental Static Regeneration のとおり、 getStaticProps 内でrevalidate を指定します。僕は revalidate: 60 で設定しています、あまり短すぎるとサーバーへの過負荷の原因となる場合があるので適宜長さは調節してください。

    ブログのトップページのコンポーネントです。

    src/pages/index.tsx

    export const getStaticProps = async () => {
      // Contentful から記事のデータを取得
      const entries = await client.getEntries<Slug>();
    
      if (entries != null) {
        // revalidate を設定
        return { props: { entries }, revalidate: 1 };
      } else throw new Error();
    };
    

    続いてブログ記事のページとなるコンポーネントです。こちらでも同様に revalidate を指定しつつ、getStaticPaths の fallback を 'blocking' にします。

    src/pages/entry/[slug].tsx

    // -- 省略 --
    
    export const getStaticProps = async () => {
      const entries = await client.getEntries<Slug>();
    
      if (entries != null) {
        return { props: { entries }, revalidate: 1 };
      }
    };
    
    export const getStaticPaths = async () => {
      const entries = await client.getEntries<Slug>();
    
      if (entries != null) {
        const paths = entries.items.map((item) => ({
          params: {
            slug: item.fields.slug,
          },
        }));
        // fallback は blocking に
        return { paths, fallback: 'blocking' };
      } else throw new Error();
    };
    

    fallback: 'blocking' を指定するとエラーページを返さなくなるので、同時に関数コンポーネント内にエラーページを返す処理を記述します

    src/pages/entry/[slug].tsx

    const BlogPost: React.FC<Props> = (props) => {
      const { entries } = props;
      const router = useRouter();
      const { slug } = router.query;
    
      const article = entries?.items.reduce((prev, cur) => {
        if (cur.fields.slug === slug) return cur;
        return prev;
      });
    
      if (!article || !article?.sys.id)
        return <ErrorPage statusCode={404} message="not found" />;
    
      return (
        <>
          <Head>
            <script
              async
              src="<https://platform.twitter.com/widgets.js>"
              charSet="utf-8"
            />
          </Head>
          <Template
            {...article}
            metadata={((article as unknown) as { metadata: Metadata }).metadata}
          />
        </>
      );
    };
    

    コード全体

    その他設定など

    Netlify 側の準備は特に必要はありません。 Next.js のデプロイの際、今までは next-on-netlify をインストールする必要があったのですが、Essentials Next.js として自動でインストールされるようになりました。

    Try the new Essential Next.js plugin, now with auto-install!

    ビルドコマンドもそのまま next build で、公開ディレクトリも out のままで大丈夫です。

    裏では Serverless として Lambda ベースの Netlify Functions に保存され、ISR という名の SSR のような処理が行われ始めます。Contentful の Deploy Webhook は必要なくなるので削除しましょう。

    これで Contentful から publish を行っても Netlify でビルドは走らず、前回のアクセスから1秒以上あとにブログへアクセスすると内容が更新されます。画面全体ごと...

    余談

    なぜ Netlify で ISR が出来ないのか

    Netlify における Next.js の ISR が完全に行われないことについてです。

    少し過去の記事にはなりますが Netlify のブログにおいて 、ISR は SSR と同じ処理が行われると明言されていました。

    For Incremental Static Regeneration, we currently server-side render those routes. We have some development in the works for caching those pages and including fallback pages, as well. When the additional functionality is added, you won’t have to make any code changes to see the benefits.

    Announcing one-click install Next.js Build Plugin on Netlify

    またこの仕様の明言と合わせて、ISR でページ全体を更新する理由について Netlify は2021年3月に公開された記事内で以下のように言及しています。

    Currently, ISR is built in to Next.js, and we serve those unbuilt pages via Netlify Functions, rendering them new every time, to avoid that caching problem. This isn’t the spirit of ISR, yes, but we are strongly in favor of atomic and immutable deploys. There are better ways to approach your sites than with this type of caching.

    Incremental Static Regeneration: Its Benefits and Its Flaws

    Netlify は ISR やそれのベースの手法である stale-while-revalidate のメリット・デメリットを挙げ、その上で Next.js で ISR を有効にしてもページ全体をレンダリングし直す対応を選んでいます。

    Netlify には Netlify の考えがあるように、結局 Vercel 以外で Next.js の ISR を行うのは現状難しそうだな...という感じがしています。Vercel 以外で ISR は行えるのかについて議論されている zenn の記事を見つけたので、気になる方は目を通してみてください。

    Vercel以外でNext.jsのISRをできるのか問題

    また Netlify について、後者(最新の方)の Netlify のブログにあるように、今後 ISR 周りでの対応(もしかすると ISR とは関係のない方法かもしれません)を新たに行うようなので動向に注目といったところです。

    おわりに

    Contentful の Deploy Webhook を使わないことで、ビルド待ち時間の削減やリソースの節約に繋がり開発する側にとってはプラスになります。その一方で Next.js の機能はやはり Vercel でのホスティングを前提とするため、他のサービスでは期待したように動かないかもしれないということを考慮する必要がありました。

    僕はしばらく Netlify でこのブログをホスティングするつもりなので、もし Next.js + Netlify でページのレンダリングを動的に、1秒でも速くする方法知ってるよ!って方は教えていただけると嬉しいです。

    この記事を共有する