📚

備忘録:Next.jsで作ったブログサイトでmicroCMSのembed linkが表示されない問題への対応

2024/11/29に公開

はじめに

こんにちは、生成AIを活用しながら留学中の空いた時間を使って開発の勉強をしているニートです。

細かい自己紹介は初回の記事に書いてあります。
今回の記事はかなり備忘録的な側面が強いですが、同じ問題にぶつかった人の参考になれば幸いです。

初回の記事で作ったブログサイトを継続的に更新するとともに
サイトの作りについても改修・改善をしています。
https://aus-blog.sloperiver.com/

今回はその中で、ブログ記事内のembed linkが初回訪問時になぜか表示されないという問題にあたり、
親友(chatGPT)や友達(v0)に相談しながら解決していった流れを備忘録的に残しておこうと思います。

前提

私 : Next.jsというかフロントエンドは完全未経験
ブログサイト : Next.jsで構築、headlessCMSであるmicroCMSでコンテンツを管理
コンテンツ取得処理 : APIはサーバーアクションで実施
コンテンツ表示処理 : dangerouslySetInnerHTML={{ __html: content }}で表示

今回の問題

1.問題の概要

  • 現象
    • ブログ記事に埋め込まれた embed link が初回訪問時に画面に表示されない。
  • 現象の詳細:
    • 初回訪問時には embed link が 空白(白紙) になる。
    • ページをリロードすると正常に表示される。

2.調査のポイント

問題を解決するために、以下のポイントを調査しました。

  • どのように表示しようとしているか?
    そもそもブログコンテンツはmicroCMSで管理しており、こちらとしてはコンテンツ内容をAPIで受け取って
    そのまま表示しているような感じの処理だったので、どのようなHTMLで管理されているか全く把握していませんでした。

まずはそこから調査します。

結果: iframely-embedというものを利用して表示しようとしているが、ここが上手く動いていないということがわかりました。

→親友(chatGPT)や友達(v0)に質問すると、window.iframely が正しく初期化されていないんじゃない? という回答がありました。
私の浅い理解でも、今回のブログサイトは基本的にサーバーサイドレンダリングしているためブラウザのwindow.iframelyがうまく動かないことは想像に難くないです。

ということでクライアントサイドでwindow.iframely.load() を適切に呼び出す修正を実施することにしました。

※親友(chatGPT)はサーバーサイドレンダリングに拘るなら、iframely APIを利用してembed linkをサーバーサイド側で処理するようにすることもできると教えてくれましたが、iframely APIの利用は無料枠が小さいため断念しました。

解決策

以下の4つのファイルを新規で作成するとともに、元々利用していたブログ記事のpage.tsxをクライアント処理の内容を反映するように修正する。

1.blog-post-client.tsx

  • 役割: 記事コンテンツをクライアントサイドで安全に処理する。
    • →parse-content.tsxに渡す
    • →iframely-embed.tsxに渡す
code
blog-post-client
'use client';

import IframelyEmbed from '@/components/IframelyEmbed';
import { parseContent, ParsedContent } from '@/utils/parse-content';

interface BlogPostClientProps {
  content: string;
}

export default function BlogPostClient({ content }: BlogPostClientProps) {
  const parsedContent: ParsedContent[] = parseContent(content);

  return (
    <div className="prose prose-sm lg:prose-base max-w-none">
      {parsedContent.map((item, index) =>
        item.type === 'embed' ? (
          <IframelyEmbed key={index} content={item.content} />
        ) : (
          <div key={index} dangerouslySetInnerHTML={{ __html: item.content }} />
        )
      )}
    </div>
  );
}

2.iframely-embed.tsx

  • 役割: Iframely埋め込み要素専用のカスタムコンポーネントとして動作し、Iframelyスクリプトのロードおよび埋め込みリンクの初期化を行う。
    • Iframelyスクリプトが動作するために必要な要素をレンダリング。
code
iframely-embed
'use client';

import { useEffect } from 'react';

interface IframelyEmbedProps {
  content: string;
}

export default function IframelyEmbed({ content }: IframelyEmbedProps) {
  useEffect(() => {
    // スクリプトのロード確認
    const script = document.createElement('script');
    script.src = 'https://cdn.iframe.ly/embed.js';
    script.async = true;

    script.onload = () => {
    if (window.iframely) {
      window.iframely.load();
    } else {
        console.error('Iframely script loaded but window.iframely is not defined.');
      }
    };

    script.onerror = () => {
      console.error('Failed to load Iframely script.');
    };

    document.head.appendChild(script);

    // クリーンアップ処理
    return () => {
      document.head.removeChild(script);
    };
  }, []);

  return <div dangerouslySetInnerHTML={{ __html: content }} />;
}

3.parse-content.tsx

  • 役割: HTMLコンテンツを解析し、埋め込み部分と通常のテキスト部分を分離するためのユーティリティ関数を提供。
    • サーバーサイドから渡された生HTMLをパースし、Iframely埋め込み要素を特定。
code
parse-content
'use client';

export interface ParsedContent {
  type: 'text' | 'embed';
  content: string;
}

// HTMLを解析して埋め込みとテキストを分ける関数
export function parseContent(html: string): ParsedContent[] {
  const parser = new DOMParser();
  const doc = parser.parseFromString(html, 'text/html');
  const parsed: ParsedContent[] = [];

  doc.body.childNodes.forEach((node) => {
    if (node.nodeType === Node.ELEMENT_NODE) {
      const element = node as HTMLElement;

      // iframely埋め込みを判定
      if (element.classList.contains('iframely-embed')) {
        parsed.push({ type: 'embed', content: element.outerHTML });
      } else {
        parsed.push({ type: 'text', content: element.outerHTML });
      }
    } else if (node.nodeType === Node.TEXT_NODE) {
      parsed.push({ type: 'text', content: node.textContent || '' });
    }
  });

  return parsed;
}

4.global.d.ts

  • 役割: TypeScript環境で window.iframely を型安全に利用するため、Window インターフェースを拡張する。
    • iframely オブジェクトの存在を明示的に定義し、型チェックを強化。
code
global.d
interface Window {
    iframely?: {
      load: () => void;
    };
  }

上記で初回訪問時においても適切にiframelyのembedが表示されるようになりました。

おわりに

上記に加えて、親友(chatGPT)と相談しながら
Next.js(React)プロジェクトにおけるディレクトリ構成やファイル、変数等の命名規則についても議論して修正したりしました。

サーバーサイドとクライアントサイドの処理フローがまだまだごっちゃになります。。。
これに慣れることはできるのでしょうか、、、
どのような順序でレンダリングが行われるのか具体的なイメージを持つ必要がありそうです。

以上です。ありがとうございました。

Discussion