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

お疲れ様です。
ネットソリューション事業部の山下です。
今回は、本メディアを構築しているフレームワークAstroの記事ページ(mdx
ファイル)に対して、ページごとのCSSやJavaScriptを適用する方法を考案しましたので、その手法を解説します。
前提条件
今回の実装では、以下の条件を満たす方法をゴールとしました。
- 1つのmdxファイル内で完結すること
- 記事ごとに
pages.css
やpages.js
のような個別のファイルを作成せず、mdxファイル内に直接CSSやJavaScriptを記述できる形にする
- 記事ごとに
技術解説系の記事になると「記事ごとに専用のCSS/JSを適用したいけど、いちいちファイルを増やしたくない」という考えです。
実装
mdx ファイルの Frontmatter で CSS・JavaScript を定義
まず、記事ごとに固有のCSSやJavaScriptを適用するために、FrontmatterにappendCss
とappendJs
というキーを追加しました。
これにより、記事内で独自のスタイルやスクリプトを直接記述できるようになります。
また、|
を使うことで複数行のコードを記述できるため、改行を含むCSSやJavaScriptをそのまま定義できます。
---
title: "【Astro × Mdx】mdxで構成されたページに独自のCSS、JavaScriptを適用する"
(省略)
appendCss: |
.md-editor h2 {color: #ff0000}
appendJs: |
console.log("ページ固有のスクリプトが発火しています");
---
レイアウトファイルでFrontmatterのデータを受け取る
mdx側で定義したFrontmatterの値をPropsとして受け取り、HTMLにも適用できるようにします。
const { title, (省略) appendJs, appendCss } = Astro.props;
JavaScript (appendJs) の適用
Propsとして受け取った値(ここではただの文字列)をJavaScriptとして適用するため、以下のような処理を行います。
- appendJs の内容を data-script 属性として div に格納
- DOMContentLoaded イベント時に data-script の値を取得
- Function コンストラクタを使用してスクリプトを実行
{
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コンストラクタを使用することで、受け取った文字列をeval()
を使わずにスクリプトとして実行しています。
ただし、外部からの不正なスクリプトの挿入を防ぐため、信頼できるデータのみを扱うようにしましょう。
CSS (appendCss) の適用方法
JavaScript の場合と同様に、記事ごとに固有の CSS を適用するため、以下の処理を行います。
- appendCssの内容をdata-style属性としてdivに格納
- DOMContentLoadedイベント時にdata-styleの値を取得
- styleタグを生成し、CSSをdocument.headに追加
{
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タグは残ってしまいます。そのため、以下のような実装をして対処しました。
{
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の進化の速さにアップアップですがなんとか喰らいついていきたい。