📝

NotionをヘッドレスCMS化!Next.jsで構築するStoreサイトのBlog実装と画像有効期限の壁

に公開

はじめに

富山県井波で85-Storeという古着を中心とした衣料品店の準備を進めているエンジニアです。
基本は妻(非エンジニア)が店頭に立ち、EC運営等の実務を行いますが、その業務を少しでも楽にするため、様々なアプリ開発を行っています。
私自身はWeb開発は仕事としては行っていませんが、趣味程度に様々な開発をやっています。
普段はFA関連(PLCのラダー, ST言語)の仕事をしています。

85-Store開業経緯はこちら

https://85-store.com/blog/storeep

1. 開発環境とNotion CMSの採用背景

https://github.com/HayatoShimada/85store

環境構成

技術要素 役割
Frontend Next.js (App Router)
Hosting Vercel
CMS Notion (API)
E-commerce Shopify (Storefront APIを連携予定)

NotionをCMSに選定した理由

店舗運営は非エンジニアである妻が中心となるため、管理画面の学習コストを極力ゼロにしたいと考えました。普段から在庫管理やタスク管理に利用しているNotion上でコンテンツ作成・公開フローを完結させることが、最も効率的だと判断しました。

プロパティ名(例) 目的
Status Draft \rightarrow Published で公開を制御。
Slug 記事URLを人間が読める形式で定義。
Featured ホームページのおすすめ商品表示を制御。

2. BlogCardコンポーネントの構造と課題

BlogCardコンポーネントは、トップページや一覧ページで記事の概要を表示する役割を担います。

コンポーネントの責務

  1. 記事の基本情報(Title, Excerpt, Date, Author)を表示。
  2. Notionのデータに基づき、CategoryTagsに適切なスタイルを適用。
  3. 最も重要なカバー画像を表示し、next/imageによる最適化を行う。
  4. 記事詳細ページへのリンク(/blog/[slug])を提供する。

Notion APIと画像の有効期限問題

Notion API経由で取得できるファイル(画像)のURLは、セキュリティ上の理由から有効期限が設定されたAWS S3の署名付きURLです。これは一般的に1時間程度で期限切れとなり、期限切れのURLを使用すると画像が表示されなくなります。

この問題を解決するため、BlogCardはクライアントコンポーネントとして実装し、カスタムフックで画像URLのライフサイクルを管理します。

'use client'; // クライアントコンポーネント化

// ... 必要なimport
import { useNotionImage } from "@/hooks/useNotionImage"; // 画像管理フック

interface BlogCardProps {
  post: BlogPost;
}

export default function BlogCard({ post }: BlogCardProps) {
  const [localImageLoading, setLocalImageLoading] = useState(true);

  // 1. 画像が存在しない場合はフックを使用しない
  const hasImage = Boolean(post.coverImage && post.coverImage.trim() !== '');

  // 2. カスタムフックで画像URLを管理
  const {
    imageUrl, // 表示に使用する最新の画像URL
    handleImageLoad: swrHandleImageLoad,
    handleImageError: swrHandleImageError
  } = useNotionImage({
    url: post.coverImage || '',
    expiryTime: post.coverImageExpiryTime, // Notionから取得した有効期限
    blockId: post.coverImageBlockId,       // 画像ブロックのID
  }, {
    enabled: hasImage
  });

  // 3. 画像の読み込みエラー処理
  const handleError = () => {
    // ... エラーログを出力
    setLocalImageLoading(false);
    swrHandleImageError(); // フックにエラーを通知し、必要なら再取得を試みる
  };

  // 4. 画像の読み込み完了処理
  const handleLoad = () => {
    setLocalImageLoading(false);
    swrHandleImageLoad(); // フックに完了を通知
  };

  // 5. 表示URLの決定 (画像がない or 無効な場合はプレースホルダー)
  const displayImageUrl = (hasImage && imageUrl && imageUrl.trim() !== '') ? imageUrl : '/images/placeholder.svg';

  // 6. JSXのレンダリング (next/imageを使用)
  return (
    // ... カードの構造
    <Image
      src={displayImageUrl}
      alt={post.title}
      // ... その他のnext/image props
      onLoad={handleLoad}
      onError={handleError}
    />
    // ...
  );
}

3. useNotionImageカスタムフックによる解決

クライアント側で画像の有効期限をチェックし、期限切れが近い、または既に切れている場合にNotion APIを通じて新しい署名付きURLを再取得するロジックをuseNotionImageに実装しました。

useNotionImageの主なロジック(概念)

  1. SWR/React Queryの利用:
    • クライアント側のキャッシュと再検証にSWRやReact Queryを利用します。
    • Notionからの初回取得データ(URLと有効期限)を初期値として設定します。
  2. 有効期限のチェック:
    • フック内で定期的に、あるいはコンポーネントがマウントされた際に、現在の時刻と**post.coverImageExpiryTime**を比較します。
    • 有効期限が切れている、または残り時間が閾値(例: 5分)を下回った場合、Notion APIを叩くための新しいURLを取得する処理をトリガーします。
  3. 再取得処理:
    • サーバーレスFunction(API Route)を経由し、**notion-client**を使ってサーバー側で安全に画像ブロックの情報を再取得し、新しい有効期限付きURLをクライアントに返します。

この仕組みにより、ユーザーがページを開いたまま長時間放置しても、画像が突然表示されなくなるというUXの低下を防ぐことができます。


4. 運用と次のステップ

非エンジニア向けの運用ルール

  • 公開制御: 記事が完成したら、必ずNotionのStatusプロパティを ✅ Published に変更する。
  • 画像設定: 記事作成時に必ずCover Imageプロパティに画像をアップロードする。
  • Featured制御: おすすめ商品に表示したい場合は、Featuredチェックボックスにチェックを入れる(最大6個)。

今後の技術拡張

Next.jsとNotionの連携が安定したことで、次はECサイトとのより深い統合を進めます。

  1. Shopify Storefront API連携:
    • 商品データ(価格、在庫)をリアルタイムで取得し、Notionデータベースと照合。
    • ブログ記事内で商品埋め込みを行った際、在庫状況(Active/Sold Out)を自動表示し、購入への導線を強化します。
  2. Server Actions/API Routesの活用:
    • Notionのデータベース構造変更や、Shopifyの在庫更新をフックとした自動的なキャッシュパージの仕組みを構築し、サイトの鮮度を最大化します。

参考文献

Discussion