ネットソリューション

【Astro × Mdx】mdxで構成されたページに独自のCSS、JavaScriptを適用する

お疲れ様です。
ネットソリューション事業部の山下です。

今回は、本メディアを構築しているフレームワークAstroの記事ページ(mdxファイル)に対して、ページごとのCSSやJavaScriptを適用する方法を考案しましたので、その手法を解説します。

前提条件

今回の実装では、以下の条件を満たす方法をゴールとしました。

  • 1つのmdxファイル内で完結すること
    • 記事ごとにpages.csspages.jsのような個別のファイルを作成せず、mdxファイル内に直接CSSやJavaScriptを記述できる形にする

技術解説系の記事になると「記事ごとに専用のCSS/JSを適用したいけど、いちいちファイルを増やしたくない」という考えです。

実装

mdx ファイルの Frontmatter で CSS・JavaScript を定義

まず、記事ごとに固有のCSSやJavaScriptを適用するために、FrontmatterにappendCssappendJsというキーを追加しました。
これにより、記事内で独自のスタイルやスクリプトを直接記述できるようになります。

また、| を使うことで複数行のコードを記述できるため、改行を含むCSSやJavaScriptをそのまま定義できます。

mdx-add-cssjs.mdx
---
title: "【Astro × Mdx】mdxで構成されたページに独自のCSS、JavaScriptを適用する"
(省略)
appendCss: |
  .md-editor h2 {color: #ff0000}
appendJs: |
  console.log("ページ固有のスクリプトが発火しています");
---

レイアウトファイルでFrontmatterのデータを受け取る

mdx側で定義したFrontmatterの値をPropsとして受け取り、HTMLにも適用できるようにします。

PostLayout.astro
const { title, (省略) appendJs, appendCss } = Astro.props;

JavaScript (appendJs) の適用

Propsとして受け取った値(ここではただの文字列)をJavaScriptとして適用するため、以下のような処理を行います。

  1. appendJs の内容を data-script 属性として div に格納
  2. DOMContentLoaded イベント時に data-script の値を取得
  3. Function コンストラクタを使用してスクリプトを実行
PostLayout.astro
{
  appendJs && (
    <>
      <div data-script={appendJs} id="inlineScript" />
      <script is:inline>
        document.addEventListener('DOMContentLoaded', function() {
          const scriptContent = document.getElementById('inlineScript').getAttribute('data-script');
          const dynamicFunction = new Function(scriptContent);  // Functionコンストラクタを使用
          dynamicFunction();  // 実行
        });
      </script>
    </>
  )
}

Functionコンストラクタとは?

Functionコンストラクタは、JavaScriptで動的に関数を作成する方法の一つです。
通常の function 宣言や ()=>{} とは異なり、文字列から関数を生成して実行することができます。

参考ページ:Function() コンストラクター

ここではFunctionコンストラクタを使用することで、受け取った文字列をeval()を使わずにスクリプトとして実行しています。
ただし、外部からの不正なスクリプトの挿入を防ぐため、信頼できるデータのみを扱うようにしましょう。

CSS (appendCss) の適用方法

JavaScript の場合と同様に、記事ごとに固有の CSS を適用するため、以下の処理を行います。

  • appendCssの内容をdata-style属性としてdivに格納
  • DOMContentLoadedイベント時にdata-styleの値を取得
  • styleタグを生成し、CSSをdocument.headに追加
PostLayout.astro
{
  appendCss && (
    <hr>
      <div data-style={appendCss} id="inlineStyle" />
      <script is:inline>
        document.addEventListener('DOMContentLoaded', function() {
          const styleContent = document.getElementById('inlineStyle').getAttribute('data-style');
          
          // 動的に style タグを作成
          const styleSheet = document.createElement("style");
          styleSheet.textContent = styleContent;  // appendCss の内容を style タグに挿入
          document.head.appendChild(styleSheet);  // head に追加
        });
      </script>
    </>
  )
}

この方法により、記事ごとのCSSをmdxファイルに記述することができました。

参考ページ:フロントマター変数をスクリプトに渡す

AstroのViewTransitionsを使っている場合

もしもAstro内でViewTransitionsを使っている場合、ページ遷移時にdiv#inlineScriptなどのHTML要素は削除されますが、一度作成されたscriptタグは残ってしまいます。そのため、以下のような実装をして対処しました。

PostLayout.astro
{
  appendJs && (
    <>
      <div data-script={appendJs} id="inlineScript" />
      <script is:inline id="appendJS">
        document.addEventListener('astro:page-load', () => {
          const scriptContent = document.getElementById('inlineScript')?.getAttribute('data-script');
          const dynamicFunction = new Function(scriptContent);  // Functionコンストラクタを使用
          dynamicFunction();  // 実行
        });
        document.addEventListener('astro:after-swap', () => {
          document.getElementById('appendJS')?.remove();
        });
      </script>
    </>
  )
}
{
  appendCss && (
    <>
      <div data-style={appendCss} id="inlineStyle" />
      <script is:inline id="appendCss">
        document.addEventListener('astro:page-load', () => {
          const styleContent = document.getElementById('inlineStyle')?.getAttribute('data-style');
          
          // 動的にstyleタグを作成
          const styleSheet = document.createElement("style");
          styleSheet.textContent = styleContent;  // appendCssの内容をstyleタグに挿入
          document.head.appendChild(styleSheet);  // headに追加
        });
        document.addEventListener('astro:after-swap', () => {
          document.getElementById('appendCss')?.remove();
        });
      </script>
    </>
  )
}

今回は試しにH2見出しを赤くし、コンソールログにメッセージを出力してみました。

今回のようなmdxファイル内で完結するCSS・JavaScriptの適用方法について解説されているものを見つけることができませんでした。
そこで、自分なりに試行錯誤してこの方法を考えてみました。

今後の運用の中で実装方法を見直すこともあるかと思いますが、ひとまず「こんなアプローチもあるよ!」という形で紹介させていただきました。
もし、もっと適した方法や改善案があれば、ぜひ教えていただけると嬉しいです!

Webの進化の速さにアップアップですがなんとか喰らいついていきたい。

山下 X@Frencel_ns

フロントエンドエンジニア
フレームワーク頑張りたい人
モンハンワイルズを楽しんでいます。

好きなモンスターハンターの武器

双剣・狩猟笛

mail お問い合わせ

ご覧いただきありがとうございます。
当メディアへのご質問や各事業部へのお仕事のご相談がありましたら、お気軽にお問い合わせください。

article 過去記事