• SanityのPortable Textで画像が表示されない原因は?文字は出るのに画像だけ反映されないときの解決方法

    AI

    SanityのPortable Textで画像が表示されない原因は?文字は出るのに画像だけ反映されないときの解決方法

    Listen & Subscribe

    Sanityでpublishした画像がフロントに表示されない

    Sanityを使ってブログサイトを構築していると、Portable Textの本文内に画像を埋め込んでpublishしたにもかかわらず、フロントエンド側では文字だけ表示され、画像だけ表示されないことがあります。今回実際に遭遇したのもこのケースでした。Sanity上ではbody内に画像ブロックが存在し、publishも完了しているのに、Next.jsで構築したブログ詳細ページでは画像だけが出てこない状態です。

    このような現象が起きると、まずは「Sanityのpublishが反映されていないのでは」「CDNキャッシュの影響では」「Next.jsの再生成が走っていないのでは」と考えがちです。実際、今回の調査でも最初はキャッシュまわりが有力候補として挙がっていました。

    よくある原因として疑ったこと

    今回、最初に疑ったのは主に次の2点です。

    スポンサーリンク

    1. Sanity CDNのキャッシュ

    client.ts 側で useCdn: true になっている場合、publish直後の変更がすぐに反映されないことがあります。そのため、開発中の確認では useCdn: false にして、最新データを直接取得する構成がよく使われます。今回もまずこの点が確認されました。

    2. Next.jsの静的生成キャッシュ

    記事詳細ページで generateStaticParams を使っている場合、ビルド時に生成された内容がそのまま残るため、Sanity側で記事本文を更新しても、すぐには反映されないことがあります。このため、revalidate を設定して一定時間ごとに再取得する構成も候補になりました。

    実際に、調査の途中では以下のような対応が行われました。

    export const client = createClient({
     projectId: "6pe29pg1",
     dataset: "production",
     apiVersion: "2024-01-01",
     useCdn: false,
    });
    export const revalidate = 60;

    これにより、SanityのキャッシュやNext.jsの再生成待ちによる影響は切り分けやすくなります。ただし、今回のケースではこれが根本原因ではありませんでした。

    Portable Textのimageコンポーネントは定義されていた

    次に確認したのが、Portable Textのレンダリング処理です。Sanityのbodyフィールドで画像を表示するには、フロント側で image タイプに対するレンダラーが必要です。

    今回のコードでは、PortableTextComponents.tsx にすでに types.image の定義が存在していました。つまり、「画像用コンポーネントを書いていなかった」という単純なミスではありませんでした。さらに、urlFor の設定や、POST_QUERYbody を正しく取得していること、next.config.tscdn.sanity.io が許可されていることも確認されました。

    この時点で、設定上は一見問題がないように見えます。

    Sanityのデータ自体は正常だった

    次に、Sanity側のデータ構造を直接確認したところ、body フィールド内には以下のように text block と image block が正しく格納されている ことが確認されました。

    • 1つ目のブロック: h1スタイルのテキスト
    • 2つ目のブロック: image
    • 3つ目のブロック: 通常テキスト

    さらに画像ブロックには、asset._ref もきちんと存在していました。つまり、Sanity Studioで画像を埋め込んだ時点の保存やpublish処理には問題がなかったということです。

    この確認によって、「Sanityのデータがおかしい」「画像アセットが壊れている」といった可能性はかなり低くなりました。

    本当の原因はServer ComponentとClient Componentの境界だった

    調査を進めた結果、最終的に原因として特定されたのは、Next.jsのServer ComponentとClient Componentの境界で、Portable Text用の関数オブジェクトが正しく扱われていなかったことでした。

    もともとの構成では、page.tsx 側から portableTextComponents を使って <PortableText /> を描画していました。しかし、portableTextComponents は見た目以上に重要で、内部に image ハンドラのような関数を含んでいます。

    Server ComponentとClient Componentの境界をまたぐ際、こうした関数を含むオブジェクトはそのまま安全に受け渡せないことがあります。その結果、image handlerが機能せず、文字は描画されても画像だけ表示されない状態になっていました。

    これは一見わかりにくいポイントですが、Sanity + Next.js + Portable Text の組み合わせでは十分に起こりうる落とし穴です。

    解決方法: Portable Textの描画をClient Component内で完結させる

    今回の解決策は、Portable Textのレンダリング処理を専用コンポーネントにまとめ、Client Component側で完結させることでした。

    修正後のイメージ

    import { PortableText, type PortableTextComponents } from "@portabletext/react";
    import Image from "next/image";
    import Link from "next/link";
    import { urlFor } from "@/sanity/lib/image";
    
    const portableTextComponents: PortableTextComponents = {
     block: {
      h1: ({ children }) => (
       <h1 className="text-3xl font-bold mt-8 mb-4">{children}</h1>
      ),
     },
     types: {
      image: ({ value }) => {
       if (!value?.asset) return null;
    
       return (
        <figure className="my-8">
         <Image
          src={urlFor(value).url()}
          alt={value.alt || ""}
          width={800}
          height={450}
          className="rounded-lg"
         />
        </figure>
       );
      },
     },
    };
    
    export function PortableTextRenderer({ value }: { value: any[] }) {
     return <PortableText value={value} components={portableTextComponents} />;
    }

    そして、記事ページ側では PortableText を直接呼ばず、以下のようにラップしたコンポーネントを使う形に変更します。

    {post.body && <PortableTextRenderer value={post.body} />}

    著者プロフィールのbioなど、Portable Textを使っている他の箇所も同じように差し替えることで、描画処理を統一できます。

    なぜ文字だけ表示されて画像だけ表示されなかったのか

    今回の現象がやや厄介だったのは、完全に何も表示されないわけではなく、テキスト部分は正常に見えていたことです。

    そのため、最初は「Portable Text自体は動いている」と判断しやすく、imageコンポーネントの設定ミスやSanity側のpublish漏れに意識が向きがちです。しかし実際には、Portable Text全体が壊れていたわけではなく、関数として定義された画像ハンドラだけが境界の問題で効いていなかった、というのが今回の本質でした。

    このようなケースでは、以下の順番で切り分けると原因にたどり着きやすくなります。

    1. Sanityの本文データに本当に画像ブロックが入っているか確認する
    2. asset._ref が取得できているか確認する
    3. types.image の定義があるか確認する
    4. urlFornext.config の画像設定を確認する
    5. それでも問題がなければ、Server/Client境界を疑う

    Sanity + Next.jsで画像が表示されないときのチェックポイント

    同じトラブルに遭遇したときは、次の観点を順番に確認するのがおすすめです。

    Sanity側

    • 画像を含む本文がpublishされているか
    • Portable Text内のimageブロックに asset._ref が入っているか

    フロントエンド側

    • @portabletext/reacttypes.image が定義されているか
    • @sanity/image-urlurlFor が正しく設定されているか
    • next.config.tscdn.sanity.io が許可されているか
    • useCdnrevalidate による反映遅延が起きていないか
    • Portable Textの描画処理がServer/Client境界で壊れていないか

    特にApp Router構成では、Server ComponentとClient Componentの責務をどう分けるかで挙動が変わるため、Portable Textのように関数ベースのレンダラーを扱う場面では注意が必要です。

    まとめ

    SanityのPortable Textで「文字は表示されるのに画像だけ表示されない」場合、原因は必ずしもpublish漏れやCDNキャッシュとは限りません。今回のケースでは、Sanityのデータも、画像アセット参照も、Next.jsの画像設定も問題がなく、最終的な原因はPortable TextのレンダラーをServer ComponentとClient Componentの境界をまたいで扱っていたことにありました。

    解決には、Portable Textの描画処理を専用の PortableTextRenderer に切り出し、Client Component側で完結させる方法が有効でした。結果として、publish済みの画像も正常にフロントへ反映されるようになりました。

    SanityとNext.jsを組み合わせた開発では、画像表示の不具合を単なるデータ反映の問題と決めつけず、レンダリング構成そのものまで視野に入れて調査することが大切です。