I built an Astro component called CodeToggle.astro for my experimental site. The idea was to create a simple wrapper around a React (or other interactive component) in an MDX file so that the source of that rendered component could be nicely displayed as a highlighted code block on the click of a toggle. Usage looks like this:

import { default as TailwindCalendarV1 } from "./components/TailwindCalendar.v1";
import TailwindCalendarV1Source from "./components/TailwindCalendar.v1?raw";

<CodeToggle source={TailwindCalendarV1Source}>
  <TailwindCalendarV1 client:load />
</CodeToggle>

The implementation of CodeToggle.astro looked like this

---
import { Code } from "astro/components";
import { Code as CodeIcon } from "lucide-react";

interface Props {
  source: string;
  lang?: string;
  children: astroHTML.JSX.Element;
}

const { source, lang = "tsx" } = Astro.props;
---

<div class="relative">
  <div class="mb-4">
    <slot />
  </div>

  <div class="not-prose">
    <details class="group">
      <summary
        class="flex items-center gap-2 font-mono text-xs px-2 py-1 rounded-md
            bg-[var(--color-bg-code)] text-[var(--color-ink-light)] opacity-80
            hover:opacity-100 hover:text-[var(--color-ink)]
            cursor-pointer transition-all duration-200 w-fit"
      >
        <CodeIcon className="w-3 h-3" />
        <span class="select-none group-open:hidden">Show Source</span>
        <span class="select-none hidden group-open:block">Hide Source</span>
      </summary>
      <div class="mt-3 rounded-md overflow-hidden">
        <Code code={source} lang={lang as any} theme="monokai" />
      </div>
    </details>
  </div>
</div>

This approach was relatively straightforward and I thought I was using the <Code> component in a sensible way. I actually published my first post for the site using this component and thought things were going well.

Right after I attempted to publish a new post, I started running into strange build issues

02:37:59   ├─ /notes/2025/llm-tailwind-react/index.html
highlighter.codeToHtml is not a function
  Hint:
    This issue often occurs when your MDX component encounters runtime errors.
  Stack trace:
    at file:///vercel/path0/dist/chunks/index_C97_OQzq.mjs:69:34
Error: Command "npm run build" exited with 1

I reverted a few commits locally, but the problem persisted.

From some debugging, it seemed the issue stemmed from my attempts to use shiki, a syntax highlighting package, in multiple ways. Why this all of a sudden became a problem, I was still unsure.

After some more poking around, searching GitHub and experimenting with different LLM outputs, I came up with these changes:

diff --git a/src/components/prose/CodeToggle.astro b/src/components/prose/CodeToggle.astro
index 88a758d..166c324 100644
--- a/src/components/prose/CodeToggle.astro
+++ b/src/components/prose/CodeToggle.astro
@@ -1,6 +1,7 @@
 ---
-import { Code } from "astro/components";
 import { Code as CodeIcon } from "lucide-react";
+import { createHighlighter } from "shiki";
+import type { BundledLanguage } from "shiki";

 interface Props {
   source: string;
@@ -9,6 +10,16 @@ interface Props {
 }

 const { source, lang = "tsx" } = Astro.props;
+
+const highlighter = await createHighlighter({
+  themes: ["monokai"],
+  langs: [lang],
+});
+
+const html = highlighter.codeToHtml(source, {
+  lang: lang as BundledLanguage,
+  theme: "monokai",
+});
 ---

 <div class="relative">
@@ -28,9 +39,7 @@ const { source, lang = "tsx" } = Astro.props;
         <span class="select-none group-open:hidden">Show Source</span>
         <span class="select-none hidden group-open:block">Hide Source</span>
       </summary>
-      <div class="mt-3 rounded-md overflow-hidden">
-        <Code code={source} lang={lang as any} theme="monokai" />
-      </div>
+      <div class="mt-3 rounded-md overflow-hidden" set:html={html} />
     </details>
   </div>
 </div>

With that approach, the build issues resolved. I can’t say I quite understand why I started having this issue but hopefully this post helps anyone who runs into the same.