iframe要素の中身のコンテンツに応じて属性を自動設定できるAstroコンポーネントを作る

前回のブログを作成するにあたって、デモページをいくつも用意する必要があった。これまではCodePenで作成したものを埋め込んで表示していたが、ソースコードがブログと別の場所で管理されていることが煩わしかったので、代わりにデモページを同サイトに配置してiframe要素から読み込んで表示することにした。

これをシンプルに実装すると次のようになる。

<iframe
  src="/2025-09-12-click-friendly-target-areas/navbar-bad.html"
  title="ナビゲーションバーのデモ(悪い例)"
  height="128"
  style="width: 100%"
></iframe>

このうち、title属性にはそのドキュメントのタイトルを設定する。そして、iframe要素をそのドキュメントとピッタリ同じ高さにするために、ドキュメントの高さをheight属性に設定する。これらを手動で設定するのはやや手間なので、次のようにして自動化した。

src/content/blog/2025-09-12-click-friendly-target-areas.mdx:

import SmartIframe from '../../components/SmartIframe.astro';

<SmartIframe src="/2025-09-12-click-friendly-target-areas/navbar-bad.html" />

src/components/SmartIframe.astro:

---
import type { HTMLAttributes } from 'astro/types';
import { chromium } from 'playwright';
import invariant from 'tiny-invariant';

interface Props extends HTMLAttributes<'iframe'> {
  src: string;
}

const { src, ...attrs } = Astro.props;

invariant(typeof src === 'string', 'SmartIframe requires a src prop');

const url = new URL(src, Astro.url).href;

const browser = await chromium.launch();
const page = await browser.newPage();

await page.setViewportSize({ width: 756, height: 720 }); // iframeを表示するページのコンテンツ幅を指定
await page.goto(url);
await page.waitForLoadState('load');

const [title, height] = await page.evaluate(() => [
  document.title,
  document.documentElement.offsetHeight,
]);

await browser.close();
---

<iframe src={src} title={title} height={height} style={{ width: '100%' }} {...attrs}></iframe>;

Playwrightを使ってドキュメントを実際に描画し、そのタイトルと高さを取得してiframe要素に割り当てている。これによって、src属性さえ指定すれば、それ以外の属性の手動指定は不要になる。

また、iframe要素のなかのドキュメントの高さは、iframe要素が描画される際の幅によって変化することがある。iframe要素の高さをこれに応じたものにするために、クライアントサイドでの動的な処理を追加する。ResizeObserverを使ってドキュメントの高さの変化を監視し、それをiframe要素に反映する。

<iframe
  src={src}
  title={title}
  height={height}
  style={{ width: '100%' }}
  {...attrs}
  data-smart-iframe></iframe>

<script>
  for (const iframe of document.querySelectorAll<HTMLIFrameElement>('[data-smart-iframe]')) {
    iframe.addEventListener('load', () => {
      const resizeObserver = new ResizeObserver(([entry]) => {
        const { blockSize } = entry.contentBoxSize[0];
        iframe.height = blockSize.toString();
      });

      if (iframe.contentDocument) {
        resizeObserver.observe(iframe.contentDocument.documentElement);
      }
    });
  }
</script>

これによって、ドキュメントの実際の高さに応じて動的に変化するレスポンシブなiframe要素を実現できた。