iframe 要素を使ってヘッダー、フッターを共通管理する

## はじめに

静的なウェブサイトを構築して配信するとき、ヘッダーやフッターなどのサイト共通パーツの管理が問題になる。

レガシーな手法だと Apache の Server Side Include (SSI) をもちいたり、PHP を導入したりして解決する。このような手法は手軽で要件が整えばとても簡単に導入できるのが利点だ。一方で配信環境が限られる問題がある。

ファイルインクルードを Gulp タスクに任せたり、静的サイトジェネレータ―を使うやりかたも一通り試してきた。状況が許すならば静的サイトジェネレータ―を使うのが一番いいだろう。実際のところ [Eleventy](https://www.11ty.dev/) の使い心地はかなりいい。

しかしそのための環境構築や技術的キャッチアップは手軽とはなかなかいいづらい。不慣れな人にとってはひとつのハードルになる。

そこで、 iframe を共通パーツのインクルードがわりに使うのはどうなのか？　と思って試してみた。HTML 初学者ならば誰でも当たり前に思いつき、まともなウェブ開発者ならアホかと一蹴する手法だが、実際のところ本当に価値のない手法なのだろうか？

結論としては悪くはないかもしれない。うまく実装すれば、サーバーサイドの助けを得ることなくフロントエンドだけでパーツのインクルードができる。JavaScript が無効な環境でも動作する。

[iframe を共通パーツの管理に使うデモ](https://tsmd.jp/assets/posts/iframe-common-parts/test.html)

## iframe 要素を使ってヘッダー、フッターを共通管理する方法

### 動作概要

- ヘッダーやフッターなど、共通パーツをそれぞれ個別のファイルに分割する
- iframe を使って共通パーツを読み込む
- JavaScript が有効な場合、JS をつかって iframe 要素を iframe の内容で置き換える。結果的に読み込み元ページには、iframe がなくなり「共通パーツが最初からそこにあった」かのように表示される
- JavaScript が無効な環境では、iframe がそのまま表示される

### ファイルを分割する

最初に次のような HTML があったとする。`header` 要素が共通管理したいパーツだ。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Iframe Common Parts</title>
    <link rel="stylesheet" href="/test.css" />
  </head>
  <body>
    <header class="Header">
      <!-- 中略 -->
    </header>
    <main>
      <h1>Hello, World!</h1>
      <!-- 中略 -->
    </main>
  </body>
</html>
```

`header` 要素を別ファイルに分割する。DOCTYPE 宣言から全部書いているのは、iframe として読み込まれるため独立した HTML ファイルの体を成している必要があるから。

```html:common/header.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>Header</title>
    <link rel="stylesheet" href="/test.css" />
  </head>
  <body>
    <header class="Header">
      <!-- 中略 -->
    </header>
  </body>
</html>
```

読み込み元は iframe 要素に置き換える。`id` 属性をつけているのは JS で要素を取得するため。`role` 属性はつけておくと良いっぽかった（スクリーンリーダーで「フレームコンテンツ」と読み上げなくなった）

```html:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Iframe Common Parts</title>
    <link rel="stylesheet" href="/test.css" />
  </head>
  <body>
    <iframe
      id="HeaderIframe"
      class="HeaderIframe"
      role="presentation"
      src="/common/header.html"
    ></iframe>
    <main>
      <h1>Hello, World!</h1>
      <!-- 中略 -->
    </main>
  </body>
</html>
```

CSS は雑だけどこんな感じ。

```css:test.css
.HeaderIframe {
  display: block;
  width: 100%;
  height: 60px; /* 固定値で入れとく必要あり */
  border: 0;
}
```

### JavaScript で iframe 要素と iframe の中身を置き換える

JS で iframe の内容を読み取り、読み込み元ページに挿入する。この操作をすることで、読み込み元のページには「iframe がもともとなかった」かのような状況をつくれる。

そのための JS 関数は以下。関数を呼び出したときの iframe の状態によらず、関数を呼び出せばうまく動くようにつくってある。

```javascript
function inlineIframe(iframe) {
  var onLoad = function () {
    iframe.insertAdjacentHTML('afterend', iframe.contentDocument.body.innerHTML)
    iframe.parentNode.removeChild(iframe)
    iframe.onload = null
  }
  if (
    iframe.contentWindow.location.href === 'about:blank' ||
    iframe.contentDocument.readyState === 'loading'
  ) {
    iframe.onload = onLoad
  } else {
    onLoad()
  }
}
```

呼び出し方はこう。`inlineIframe` の引数には `iframe` の Element を渡す。

```javascript
 
inlineIframe(document.querySelector('#HeaderIframe'))
```

### 完成品

[iframe を共通パーツの管理に使うデモ](https://tsmd.jp/assets/posts/iframe-common-parts/test.html)

## 実装時のポイント

- 読み込み後に画面のがたつきが起きないよう、iframe の実寸サイズを iframe 要素の高さに指定する
- iframe 内部からのリンクには `target="_top"` を忘れないようにする
- iframe 要素には `role="presentation"` を付けてアクセシビリティロールを削除する（スクリーンリーダーが「フレームコンテンツ」と読み上げなくなった）
- 共通パーツのレスポンスに `Cache-Control` などキャッシュを制御するヘッダーを適切に設定するとよい

## 手法の考察

この手法のいちばん気になる点は、表示時のディレイと、iframe の高さを固定値で入れておかなくてはいけない点だ。特に表示時のディレイはユーザー体験におおきくかかわる部分なので慎重にならざるを得ない。

とはいえ、冒頭にあげた課題を解決する手段としては一定の価値がみとめられると思う。なにせほかの手段のハードルがどれも高いのだ。いや、今回紹介の手法もお手軽とはいいがたいけど……。

HTML から別の HTML を読み込む手法は歴史的にも不遇な一途をたどってきた。Frameset DTD の廃止にはじまり、HTML5 策定時に導入されかけた iframe の `seamless` 属性もついぞ導入されず、Web Components の一部分だった HTML Imports もなくなってしまった。ブラウザの実装上いろいろ困難はあるんだとおもうが、もうちょっと何とかならないのかな、と思う。

### そのほかの懸念事項

- `iframe.contentDocument.body.innerHTML` をまるごと読み込み元に挿入している。やりかたが雑かもしれない
- 共通パーツに JS による振る舞いが指定されているばあい、ちょっと工夫が必要そう（なんとでもなるが）