前回のブログを作成するにあたって、デモページをいくつも用意する必要があった。これまでは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
要素を実現できた。