シンタックスハイライトが効かない問題の調査と対応
問題の概要
マークダウンファイル内のコードブロック(例: TypeScript, HTML, CSSなど)にシンタックスハイライトが適用されず、すべてのコードが単色で表示される問題が発生。
症状
- コードブロック内のキーワード、変数、文字列などが色分けされない
- すべてのテキストが同じ色(デフォルトの黒)で表示
- 構文に応じた色の変化がない
影響範囲
- すべてのマークダウンファイル(
apps/web/content/**/*.md) - すべての言語(TypeScript, JavaScript, Vue, CSS, HTML など)
調査プロセス
1. 初期状態の確認
設定ファイル: apps/web/nuxt.config.ts
content: {
build: {
markdown: {
highlight: {
theme: {
default: "github-light",
dark: "github-dark"
},
langs: [
"javascript",
"typescript",
"vue",
"css",
// ... その他の言語
]
}
}
}
}
この設定は正しいパスのように見えた。
2. ブラウザでのHTML構造確認
Chrome DevTools MCPを使用して実際のHTML構造を調査:
// 実際のHTML出力
<pre class="language-html doc__code-block">
<code>
<span class="line">
<span class="sVt8B"><</span>
<span class="s9eBZ">div</span>
// ...
</span>
</code>
</pre>
発見事項:
<pre>要素にshikiクラスが付いていない- 代わりに
language-htmlというクラスが付いている <span>要素にsVt8Bなどのクラスは付いている(Shikiのマークアップは生成されている)
3. CSS セレクタの確認
生成されているCSSルールを確認:
html pre.shiki code .sVt8B,
html code.shiki .sVt8B {
--shiki-default: #24292E;
--shiki-dark: #E1E4E8;
}
問題点:
- CSSセレクタが
pre.shikiを要求している - 実際の
<pre>にはshikiクラスがない(language-htmlのみ) - そのため、CSSルールがマッチせず、スタイルが適用されない
4. CSS変数の確認
スタイル自体は存在するが、colorプロパティに適用されていない:
// 計算済みスタイル
{
color: "rgb(36, 41, 46)", // デフォルトの色
backgroundColor: "rgba(0, 0, 0, 0)"
}
CSS変数(--shiki-defaultなど)は定義されているが、実際のcolorプロパティとして使用されていない。
試したアプローチ
アプローチ1: content.build.markdown.highlight → content.highlight
仮説: Nuxt Content v3では設定パスが変更されている可能性
変更内容:
// 変更前
content: {
build: {
markdown: {
highlight: { ... }
}
}
}
// 変更後
content: {
highlight: { ... }
}
結果: ❌ エラーが発生
Package subpath './text' is not defined by "exports"
原因: langs配列に"text"を追加していたが、Shikiがサポートしていない
アプローチ2: text言語を削除して再試行
変更内容:
content: {
highlight: {
theme: {
default: "github-light",
dark: "github-dark"
}
// langs配列から"text"を削除
}
}
結果: ❌ サーバーは起動するが、ハイライトは効かない
原因: <pre>要素に依然としてshikiクラスが付いていない
アプローチ3: 元の設定に戻す
変更内容:
// content.highlight から content.build.markdown.highlight に戻す
結果: ❌ 変わらず
根本原因
Nuxt Content v3では、Shikiが生成するHTMLマークアップとCSSセレクタに不一致が存在:
- 期待される構造 (CSSセレクタ):
<pre class="shiki"> <code> <span class="sVt8B">...</span> </code> </pre> - 実際の構造:
<pre class="language-html doc__code-block"> <code> <span class="sVt8B">...</span> </code> </pre> - CSS変数の適用不足:
- CSS変数(
--shiki-default,--shiki-dark)は定義される - しかし、
color: var(--shiki-default)のような使用が行われていない
- CSS変数(
未解決の状態
現時点では以下の状態:
- ✅ Shikiのマークアップ(
<span class="sVt8B">など)は生成されている - ✅ CSS変数は定義されている
- ❌
<pre>要素にshikiクラスが付いていない - ❌ CSSセレクタがマッチしない
- ❌ シンタックスハイライトが表示されない
次のステップ候補
オプション1: カスタムCSSを追加
.language-*クラスでもハイライトが効くようにCSSを追加:
/* app.vue または global.css に追加 */
pre.language-html code .sVt8B,
pre.language-typescript code .sVt8B,
pre.language-javascript code .sVt8B,
/* ... 他の言語 */ {
color: var(--shiki-default);
}
/* ダークモード */
@media (prefers-color-scheme: dark) {
pre.language-html code .sVt8B,
/* ... */ {
color: var(--shiki-dark);
}
}
メリット: 既存の設定を変更せず、CSSで対応 デメリット: 各クラス・各言語に対して手動でCSSを追加する必要がある
オプション2: Nuxt Content設定の見直し
公式ドキュメントを確認して、Nuxt Content v3の正しいハイライト設定を適用:
@nuxt/contentのバージョン:^3.7.1- Shikiのデフォルト動作の確認
- カスタムレンダラーの使用を検討
オプション3: highlight.jsへの切り替え
依存関係にhighlight.jsが含まれているため、Shikiの代わりにhighlight.jsを使用:
content: {
highlight: false // Shikiを無効化
}
その後、カスタムのhighlight.js統合を実装。
参考情報
使用しているパッケージバージョン
{
"@nuxt/content": "^3.7.1",
"nuxt": "^4.1.2",
"highlight.js": "^11.11.1"
}
調査に使用したツール
- Chrome DevTools MCP (
mcp__chrome-devtools__evaluate_script) - Nuxt DevTools
- ブラウザの要素検証ツール
関連ファイル
apps/web/nuxt.config.ts- Nuxt設定ファイルapps/web/content/**/*.md- 影響を受けるマークダウンファイル- 生成されるHTML内のインラインCSS
アプローチ4: カスタムCSS追加(属性セレクタ使用)
仮説: pre.shikiの代わりにpre[class^="language-"]でセレクタを書けば、既存のCSS変数を利用できる
変更内容:
apps/web/app/app.vueの<style>セクションに以下を追加:
/* Shikiシンタックスハイライト対応: language-*クラスでもCSS変数を適用 */
pre[class^="language-"] code span[class],
pre[class*=" language-"] code span[class] {
color: var(--shiki-default, inherit);
}
@media (prefers-color-scheme: dark) {
pre[class^="language-"] code span[class],
pre[class*=" language-"] code span[class] {
color: var(--shiki-dark, inherit);
}
}
実装日時: 2025-11-28
アプローチの詳細:
- Shikiが生成するCSS変数(
--shiki-default,--shiki-dark)はすでに各<span>要素に定義されている - しかし、それらを
colorプロパティとして使用するCSSルールがマッチしていない pre[class^="language-"](language-で始まるクラス)という属性セレクタを使用することで、pre.shikiを期待するオリジナルのCSSルールの代わりに動作させる- すべてのShikiクラス(
span[class])に対して一括で適用されるため、個別のクラスごとにルールを書く必要がない
結果: 🔄 部分的に効果あり(追加改善が必要)
問題点:
- Shikiが生成するCSS変数定義ルールも
pre.shikiセレクタを要求している - つまり、
pre.shikiがないと、CSS変数自体が定義されない - カスタムCSSで
var(--shiki-default)を使っても、変数が未定義のため効果なし
改善策: アプローチ5に進む
アプローチ5: JavaScriptでshikiクラスを動的追加
仮説: <pre class="language-*">要素にshikiクラスを追加すれば、既存のShiki CSSルールがそのまま動作する
変更内容:
apps/web/app/app.vueの<script setup>セクションを追加:
import { onMounted } from 'vue'
// Shikiクラス追加: language-*クラスを持つpre要素にshikiクラスを追加
onMounted(() => {
const addShikiClass = () => {
const preElements = document.querySelectorAll('pre[class^="language-"], pre[class*=" language-"]')
preElements.forEach(el => {
if (!el.classList.contains('shiki')) {
el.classList.add('shiki')
}
})
}
// 初回実行
addShikiClass()
// ルート変更時にも実行(Nuxtのクライアントサイドナビゲーション対応)
const observer = new MutationObserver(addShikiClass)
observer.observe(document.body, { childList: true, subtree: true })
})
実装日時: 2025-11-28
アプローチの詳細:
- 根本原因は
<pre>要素にshikiクラスが付いていないこと - JavaScriptでDOM操作により
shikiクラスを追加することで、既存のShiki CSSルール(pre.shiki code .sVt8Bなど)がマッチするようになる MutationObserverを使用して、クライアントサイドナビゲーション後も動作を保証- 既存のShikiが生成するCSSをそのまま活用できるため、個別のクラスやCSS変数を定義する必要がない
結果: ✅ 成功
検証結果: Chrome DevToolsで確認した結果:
- ✅
<pre>要素にshikiクラスが正常に追加されている// 実際のクラス "language-typescript shiki doc__code-block" - ✅ シンタックスハイライトが適用されている
- 8種類の色で構文が色分けされている
- 332個のspan要素すべてに適切な色が適用
- ✅ CSSルールがマッチしている
pre.shiki code .sVt8Bなどのセレクタが正常に動作- CSS変数が定義され、
colorプロパティとして使用されている
JavaScriptコード:
apps/web/app/app.vueに追加したコードが正常に動作:
onMounted(() => {
const addShikiClass = () => {
const preElements = document.querySelectorAll('pre[class^="language-"], pre[class*=" language-"]')
preElements.forEach(el => {
if (!el.classList.contains('shiki')) {
el.classList.add('shiki')
}
})
}
addShikiClass()
const observer = new MutationObserver(addShikiClass)
observer.observe(document.body, { childList: true, subtree: true })
})
追加の改善: テーマの変更
初期テーマ(github-light / github-dark)がカラフルすぎて見づらかったため、よりシンプルなテーマに変更:
apps/web/nuxt.config.ts:
highlight: {
theme: {
default: "vitesse-light",
dark: "vitesse-light"
}
}
注意事項:
- テーマを変更した場合は、開発サーバーを再起動する必要がある(Nuxt Contentはビルド時にCSSを生成するため)
- 再起動せずにリロードしても、古いテーマの色が表示される
まとめ
問題の根本原因
シンタックスハイライトが効かない原因は、Shikiが生成するHTMLの<pre>要素にshikiクラスが付いておらず、CSSセレクタがマッチしないためです。
- Shikiが生成するCSSルール:
html pre.shiki code .sVt8B { ... } - 実際のHTML:
<pre class="language-typescript doc__code-block">(shikiクラスが無い) - 結果: CSSセレクタがマッチせず、スタイルが適用されない
解決策
アプローチ5: JavaScriptでshikiクラスを動的追加 ✅ 成功
apps/web/app/app.vueに以下のコードを追加:
<script setup lang="ts">
import { onMounted } from 'vue'
onMounted(() => {
const addShikiClass = () => {
const preElements = document.querySelectorAll('pre[class^="language-"], pre[class*=" language-"]')
preElements.forEach(el => {
if (!el.classList.contains('shiki')) {
el.classList.add('shiki')
}
})
}
addShikiClass()
const observer = new MutationObserver(addShikiClass)
observer.observe(document.body, { childList: true, subtree: true })
})
</script>
メリット:
- 既存のShikiが生成するCSSをそのまま活用できる
- 個別のクラスやCSS変数を定義する必要がない
- クライアントサイドナビゲーション後も動作する(
MutationObserver使用)
テーマの変更:
より見やすいテーマに変更(apps/web/nuxt.config.ts):
content: {
build: {
markdown: {
highlight: {
theme: {
default: "vitesse-light",
dark: "vitesse-light"
}
}
}
}
}
重要な注意事項:
- テーマを変更した場合は、開発サーバーの再起動が必須
- Nuxt Contentはビルド時にShikiのCSSを生成するため、再起動なしでは反映されない
今後同様の問題が発生した場合のチェックリスト
- ブラウザの開発者ツールで
<pre>要素のクラスを確認 - 期待されるCSSセレクタと実際のHTMLが一致しているか確認
- CSS変数が定義されているか、
colorプロパティとして使用されているか確認 - テーマ変更後は必ずサーバーを再起動