技術 約4分で読めます

AstroでYouTube埋め込みを軽量化:rehypeプラグインで遅延読み込みを実装

問題:YouTube埋め込みが重い

YouTubeの公式埋め込みコードをそのまま使うと、ページ読み込み時に大量のリソースを取得する。動画を見るかどうかわからないのに、JavaScriptやサムネイル画像が先読みされてしまう。

さらに、固定サイズ(width="560" height="315")なのでスマホではみ出る。

<iframe width="560" height="315"
  src="https://www.youtube-nocookie.com/embed/VIDEO_ID"
  title="YouTube video player"
  frameborder="0"
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
  allowfullscreen></iframe>

既存ライブラリの検討

Astro向けのYouTube埋め込みライブラリとして astro-embed がある。遅延読み込みにも対応している。

しかし、使い方がこう:

---
import { YouTube } from 'astro-embed';
---

<YouTube id="VIDEO_ID" />

これは MDXでしか使えない.mdファイルに <YouTube id="..." /> と書いても動かない。

既存の記事を全部MDXに書き換えるのは現実的ではない。Markdownの書き方はそのままで、自動的に遅延読み込みに対応させたい。

解決策:rehypeプラグインで自動変換

Astroのmarkdown処理はrehype(HTML用のプラグインシステム)に対応している。rehypeプラグインを作れば、ビルド時にiframeを検出して遅延読み込み用の構造に変換できる。

動作フロー

  1. ビルド時:rehypeプラグインがYouTubeのiframeを検出
  2. 変換:サムネイル画像 + 再生ボタンのHTML構造に置換
  3. 表示時:軽量なサムネイルのみ表示(YouTubeのJS読み込みなし)
  4. クリック時:JavaScriptでiframeを動的生成、autoplayで即再生

実装

1. 依存パッケージをインストール

pnpm add unist-util-visit rehype-raw
pnpm add -D @types/hast
  • unist-util-visit:AST(抽象構文木)を走査するユーティリティ
  • rehype-raw:Markdown内の生HTMLをASTとして処理するために必要

2. rehypeプラグインを作成

import type { Root, Element } from 'hast';
import { visit } from 'unist-util-visit';

export function rehypeYouTubeEmbed() {
  return (tree: Root) => {
    visit(tree, 'element', (node: Element) => {
      if (node.tagName !== 'iframe') return;

      const src = node.properties?.src as string | undefined;
      if (!src) return;

      // YouTube URLからビデオIDを抽出
      const match = src.match(
        /(?:youtube\.com|youtube-nocookie\.com)\/embed\/([a-zA-Z0-9_-]+)/
      );
      if (!match) return;

      const videoId = match[1];

      // 遅延読み込み用の構造に変換
      node.tagName = 'div';
      node.properties = {
        className: ['youtube-embed'],
        'data-video-id': videoId,
      };
      node.children = [
        {
          type: 'element',
          tagName: 'img',
          properties: {
            src: `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`,
            alt: 'YouTube動画サムネイル',
            loading: 'lazy',
          },
          children: [],
        },
        {
          type: 'element',
          tagName: 'button',
          properties: {
            className: ['youtube-play-button'],
            'aria-label': '動画を再生',
          },
          children: [],
        },
      ];
    });
  };
}

YouTubeのサムネイル画像は https://i.ytimg.com/vi/{VIDEO_ID}/maxresdefault.jpg で取得できる。

3. Astro設定にプラグインを追加

import { rehypeYouTubeEmbed } from './src/lib/rehype-youtube-embed';
import rehypeRaw from 'rehype-raw';

export default defineConfig({
  markdown: {
    rehypePlugins: [rehypeRaw, rehypeYouTubeEmbed],
    // ... 他の設定
  },
});

rehype-raw を先に実行することで、Markdown内の生HTMLがASTに変換され、その後 rehypeYouTubeEmbed で処理できるようになる。

4. CSSでスタイリング

.youtube-embed {
  position: relative;
  width: 100%;
  max-width: 560px;
  aspect-ratio: 16 / 9;
  margin: 1.5rem 0;
  border-radius: 0.5rem;
  overflow: hidden;
  cursor: pointer;
  background: var(--secondary);
}

.youtube-embed img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: opacity 0.2s;
}

.youtube-embed:hover img {
  opacity: 0.8;
}

.youtube-play-button {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 68px;
  height: 48px;
  background: rgba(255, 0, 0, 0.9);
  border: none;
  border-radius: 12px;
  cursor: pointer;
}

.youtube-play-button::before {
  content: '';
  border-style: solid;
  border-width: 10px 0 10px 18px;
  border-color: transparent transparent transparent white;
  margin-left: 4px;
}

.youtube-embed iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border: none;
}

aspect-ratio: 16 / 9 でレスポンシブ対応。スマホでもはみ出さない。

5. クリック時のJavaScript

function initYouTubeEmbeds() {
  const embeds = document.querySelectorAll('.youtube-embed');

  embeds.forEach((embed) => {
    if (embed.querySelector('iframe')) return;
    if (embed.dataset.initialized) return;
    embed.dataset.initialized = 'true';

    embed.addEventListener('click', () => {
      const videoId = embed.getAttribute('data-video-id');
      if (!videoId) return;

      const iframe = document.createElement('iframe');
      iframe.src = `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1`;
      iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';
      iframe.allowFullscreen = true;
      iframe.title = 'YouTube video player';

      embed.innerHTML = '';
      embed.appendChild(iframe);
    });
  });
}

initYouTubeEmbeds();
document.addEventListener('astro:page-load', initYouTubeEmbeds);

クリックすると autoplay=1 付きでiframeを生成するので、すぐに再生が始まる。

youtube-nocookie.com ドメインを使うことで、埋め込み時のトラッキングCookieを抑制できる。

結果

  • 既存の記事を書き換える必要なし:iframeをそのまま書けばOK
  • 初期読み込みが軽量化:YouTubeのJSを読み込まず、サムネイル画像のみ
  • レスポンシブ対応aspect-ratio でスマホでもきれいに表示
  • プライバシー配慮youtube-nocookie.com を使用

Markdownの書き方を変えずに、ビルド時の処理だけで最適化できるのがrehypeプラグインの強み。