From fb50e331f297713102465505282a32b922144435 Mon Sep 17 00:00:00 2001 From: Darius Cepulis Date: Thu, 2 Jul 2026 10:10:35 -0700 Subject: [PATCH] =?UTF-8?q?perf(site):=20migrate=20markdown=20pipeline=20t?= =?UTF-8?q?o=20S=C3=A4tteri=20(#1733)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude --- pnpm-lock.yaml | 15 +- site/CLAUDE.md | 56 ++-- site/astro.config.mjs | 24 +- site/package.json | 5 +- .../src/components/typography/CodeFrame.astro | 26 ++ .../components/typography/MarkdownCode.astro | 17 +- site/src/components/typography/Pre.astro | 38 --- .../typography/defaultMarkdownComponents.ts | 4 +- site/src/styles/shiki-transformers.css | 11 + site/src/utils/featureReferenceModel.js | 2 +- site/src/utils/mediaReferenceModel.js | 2 +- site/src/utils/rehypePrepareCodeBlocks.js | 50 ---- site/src/utils/remarkConditionalHeadings.js | 282 ------------------ site/src/utils/remarkReadingTime.mjs | 21 -- site/src/utils/satteriAstroData.ts | 24 ++ site/src/utils/satteriCodeFrame.ts | 45 +++ site/src/utils/satteriConditionalHeadings.ts | 196 ++++++++++++ site/src/utils/satteriReadingTime.ts | 35 +++ site/src/utils/shikiStripPreStyle.ts | 19 ++ site/src/utils/shikiTransformMetadata.js | 12 - .../utils/tests/mediaReferenceModel.test.ts | 2 +- site/src/utils/tests/satteriCodeFrame.test.ts | 37 +++ .../tests/satteriConditionalHeadings.test.ts | 50 ++++ .../utils/tests/satteriReadingTime.test.ts | 35 +++ 24 files changed, 540 insertions(+), 468 deletions(-) create mode 100644 site/src/components/typography/CodeFrame.astro delete mode 100644 site/src/components/typography/Pre.astro delete mode 100644 site/src/utils/rehypePrepareCodeBlocks.js delete mode 100644 site/src/utils/remarkConditionalHeadings.js delete mode 100644 site/src/utils/remarkReadingTime.mjs create mode 100644 site/src/utils/satteriAstroData.ts create mode 100644 site/src/utils/satteriCodeFrame.ts create mode 100644 site/src/utils/satteriConditionalHeadings.ts create mode 100644 site/src/utils/satteriReadingTime.ts create mode 100644 site/src/utils/shikiStripPreStyle.ts delete mode 100644 site/src/utils/shikiTransformMetadata.js create mode 100644 site/src/utils/tests/satteriCodeFrame.test.ts create mode 100644 site/src/utils/tests/satteriConditionalHeadings.test.ts create mode 100644 site/src/utils/tests/satteriReadingTime.test.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d16c67b..edd07c69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -465,9 +465,9 @@ importers: site: dependencies: - '@astrojs/markdown-remark': - specifier: ^7.2.0 - version: 7.2.0 + '@astrojs/markdown-satteri': + specifier: ^0.3.1 + version: 0.3.1 '@astrojs/mdx': specifier: ^7.0.0 version: 7.0.0(@astrojs/markdown-satteri@0.3.1)(astro@7.0.0(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@netlify/blobs@10.7.9)(@types/node@24.12.2)(jiti@2.7.0)(rollup@4.59.0)(tsx@4.21.0)(yaml@2.9.0)) @@ -549,9 +549,6 @@ importers: marked: specifier: ^17.0.1 version: 17.0.4 - mdast-util-to-string: - specifier: ^4.0.0 - version: 4.0.0 nanostores: specifier: ^1.0.1 version: 1.1.1 @@ -567,6 +564,9 @@ importers: satori: specifier: ^0.26.0 version: 0.26.0 + satteri: + specifier: ^0.9.1 + version: 0.9.1 schema-dts: specifier: ^1.1.5 version: 1.1.5 @@ -585,9 +585,6 @@ importers: tailwindcss: specifier: ^4.3.1 version: 4.3.1 - unist-util-visit: - specifier: ^5.0.0 - version: 5.1.0 vite: specifier: ^8.0.0 version: 8.0.16(@types/node@24.12.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0) diff --git a/site/CLAUDE.md b/site/CLAUDE.md index 2a408b93..83e8a7d8 100644 --- a/site/CLAUDE.md +++ b/site/CLAUDE.md @@ -581,7 +581,7 @@ vi.mock('@/types/docs', async () => { ## Technology Stack -- **[Astro 7](https://astro.build)**: Static site generation with island architecture (Rust compiler; Markdown stays on the `unified()` remark/rehype processor — see "MDX Processing Plugins" below) +- **[Astro 7](https://astro.build)**: Static site generation with island architecture (Rust compiler; Markdown runs on the native **Sätteri** processor with custom MDAST plugins — see "MDX Processing Plugins" below) - **[Vite 8](https://vite.dev)**: Underlying dev server and bundler, via Rolldown (see "Dependency Optimization" gotcha below) - **[React 19](https://react.dev)**: Client-side interactive components (`client:load`) - **[React Compiler](https://react.dev/learn/react-compiler)**: Enabled via `babel-plugin-react-compiler` targeting React 19 @@ -696,31 +696,45 @@ OAuth and Mux integration exist to support the **video uploader** on the install ## MDX Processing Plugins -Four plugins transform MDX content during build. Registered in `astro.config.mjs`. +The Markdown pipeline uses **Sätteri** (`markdown.processor: satteri({ mdastPlugins })` +in `astro.config.mjs`), Astro 7's native Rust processor. Custom transforms are +Sätteri **MDAST plugins** (`defineMdastPlugin` from `satteri`), not remark/rehype. -> **Markdown processor:** Astro 7 makes Sätteri the default Markdown pipeline, -> which does **not** run remark/rehype plugins. The site stays on the unified -> pipeline by passing the plugins to `markdown.processor: unified({ remarkPlugins, -> rehypePlugins })` (from `@astrojs/markdown-remark`), rather than the deprecated -> top-level `markdown.remarkPlugins`/`rehypePlugins` options. `syntaxHighlight` -> and `shikiConfig` remain top-level `markdown` options. Porting these plugins to -> Sätteri MDAST/HAST for build speed is tracked separately (issue #1719). +> **Syntax highlighting is independent of the processor.** `markdown.syntaxHighlight` +> and `shikiConfig` (themes, pre-registered langs, and `transformers`) are applied by +> Astro's Shiki layer regardless of processor, so the notation transformers carry over +> unchanged. GFM and SmartyPants stay on by default (Astro's `markdown.gfm`/`smartypants` +> default true), matching the previous output. +> +> **Plugins write frontmatter via `ctx.data.astro.frontmatter.*`.** The markdown-satteri +> adapter seeds the document data bag with `{ astro: { frontmatter, headings } }` and +> surfaces `data.astro.frontmatter` back as `render().remarkPluginFrontmatter` — the same +> contract the old remark plugins used via `file.data.astro.frontmatter`. Stateful plugins +> are written as factories (`() => defineMdastPlugin(...)`) so per-document state resets. -**`remarkConditionalHeadings`** (`src/utils/remarkConditionalHeadings.js`) -Walks the MDX AST and tracks headings inside `` / `` components, attaching conditional metadata (which frameworks/styles a heading belongs to). Also reads `` and `` component props, loads the generated JSON, and injects heading entries so API reference sections appear in the table of contents. Outputs to `frontmatter.conditionalHeadings`. +**`satteriConditionalHeadings`** (`src/utils/satteriConditionalHeadings.ts`) +Collects headings (slugged with GithubSlugger in document order, so slugs match the ids the +markdown-satteri `heading-ids` plugin emits), tracking which `` / `` +each lives in via `ctx.parent()`. Reads `` / `` / +`` / `` props, loads the generated JSON, and injects heading +entries so API-reference sections appear in the TOC. Outputs `frontmatter.conditionalHeadings`. -**`remarkReadingTime`** (`src/utils/remarkReadingTime.mjs`) -Calculates reading time and injects `frontmatter.minutesRead` (text) and `frontmatter.readingTimeMinutes` (number). +**`satteriReadingTime`** (`src/utils/satteriReadingTime.ts`) +Accumulates text/code node content and injects `frontmatter.minutesRead` (text) and +`frontmatter.readingTimeMinutes` (number). -**`rehypePrepareCodeBlocks`** (`src/utils/rehypePrepareCodeBlocks.js`) -Tags `` children of `
` with a `codeBlock` property, and marks `
` blocks with `hasFrame: true` when inside a `` JSX component. This controls code block styling (framed vs. standalone).
+**`satteriCodeFrame`** (`src/utils/satteriCodeFrame.ts`)
+Wraps standalone fenced code blocks in a `` component (filename/lang header + copy
+button, reusing the `Tabs` chrome). Blocks already inside an authored `` are left
+alone. The title is read from the fence meta (e.g. ```` ```ts title="App.ts"````), which is why
+no Shiki title transformer is needed.
 
-**`shikiTransformMetadata`** (`src/utils/shikiTransformMetadata.js`)
-Shiki transformer that extracts `title="..."` from code fence metadata, enabling titled code blocks:
-
-~~~markdown
-```tsx title="Example.tsx"
-~~~
+> **Why `CodeFrame` instead of a `pre`/`code` component override:** under Sätteri the Shiki
+> highlight step rewrites each `
` into raw HTML *before* HAST plugins run, so the old
+> `pre: Pre` override never fired. Wrapping at the MDAST stage keeps a real component frame
+> while Sätteri still highlights the inner code. The raw `.astro-code` `
` gets its
+> monospace font/size from a rule in `src/styles/shiki-transformers.css` (previously supplied
+> by the `MarkdownCode` `codeBlock` branch).
 
 ## Custom Astro Integration: LLM Markdown
 
diff --git a/site/astro.config.mjs b/site/astro.config.mjs
index aacd9fb8..85b8b074 100644
--- a/site/astro.config.mjs
+++ b/site/astro.config.mjs
@@ -2,7 +2,7 @@
 
 import process from 'node:process';
 
-import { unified } from '@astrojs/markdown-remark';
+import { satteri } from '@astrojs/markdown-satteri';
 import mdx from '@astrojs/mdx';
 import netlify from '@astrojs/netlify';
 import react from '@astrojs/react';
@@ -26,11 +26,11 @@ import yaml from 'shiki/langs/yaml.mjs';
 import svgr from 'vite-plugin-svgr';
 import llmsMarkdown from './integrations/llms-markdown';
 import { PRERELEASE_URL, PRODUCTION_URL } from './src/consts.ts';
-import rehypePrepareCodeBlocks from './src/utils/rehypePrepareCodeBlocks';
-import remarkConditionalHeadings from './src/utils/remarkConditionalHeadings';
-import { remarkReadingTime } from './src/utils/remarkReadingTime.mjs';
+import { satteriCodeFrame } from './src/utils/satteriCodeFrame';
+import { satteriConditionalHeadings } from './src/utils/satteriConditionalHeadings';
+import { satteriReadingTime } from './src/utils/satteriReadingTime';
 import { shikiNotationTransformers } from './src/utils/shikiNotationTransformers';
-import shikiTransformMetadata from './src/utils/shikiTransformMetadata';
+import { shikiStripPreStyle } from './src/utils/shikiStripPreStyle';
 
 // Netlify sets CONTEXT and BRANCH for each deploy. We use them to determine
 // the correct site URL:
@@ -144,15 +144,13 @@ export default defineConfig({
         ...http,
         ...astro,
       ],
-      transformers: [shikiTransformMetadata, ...shikiNotationTransformers],
+      transformers: [...shikiNotationTransformers, shikiStripPreStyle],
     },
-    // Astro 7 makes Sätteri the default Markdown processor, which does not run
-    // remark/rehype plugins. Stay on the unified() pipeline so our plugins keep
-    // working unchanged. unified() applies GFM + SmartyPants by default (which
-    // is why the previous explicit `gfm`/`smartypants` flags were dropped).
-    processor: unified({
-      remarkPlugins: [remarkConditionalHeadings, remarkReadingTime],
-      rehypePlugins: [rehypePrepareCodeBlocks],
+    // `syntaxHighlight`/`shikiConfig` are applied by Astro's Shiki layer
+    // independently of the Markdown processor, so highlighting is configured
+    // here while the processor's custom transforms live in `mdastPlugins`.
+    processor: satteri({
+      mdastPlugins: [satteriReadingTime(), satteriConditionalHeadings(), satteriCodeFrame()],
     }),
   },
 
diff --git a/site/package.json b/site/package.json
index a1cb6d96..76da04ec 100644
--- a/site/package.json
+++ b/site/package.json
@@ -16,7 +16,7 @@
     "test:coverage": "vitest --coverage"
   },
   "dependencies": {
-    "@astrojs/markdown-remark": "^7.2.0",
+    "@astrojs/markdown-satteri": "^0.3.1",
     "@astrojs/mdx": "^7.0.0",
     "@astrojs/netlify": "^8.0.0",
     "@astrojs/react": "^6.0.0",
@@ -44,19 +44,18 @@
     "just-throttle": "^4.2.0",
     "lucide-react": "^0.546.0",
     "marked": "^17.0.1",
-    "mdast-util-to-string": "^4.0.0",
     "nanostores": "^1.0.1",
     "react": "^19.0.0",
     "react-dom": "^19.0.0",
     "reading-time": "^1.5.0",
     "satori": "^0.26.0",
+    "satteri": "^0.9.1",
     "schema-dts": "^1.1.5",
     "sharp": "^0.34.3",
     "shiki": "^4.0.2",
     "simple-git": "^3.28.0",
     "tailwind-merge": "^3.5.0",
     "tailwindcss": "^4.3.1",
-    "unist-util-visit": "^5.0.0",
     "vite": "^8.0.0"
   },
   "devDependencies": {
diff --git a/site/src/components/typography/CodeFrame.astro b/site/src/components/typography/CodeFrame.astro
new file mode 100644
index 00000000..2218d8ee
--- /dev/null
+++ b/site/src/components/typography/CodeFrame.astro
@@ -0,0 +1,26 @@
+---
+import { Tab, TabsList, TabsPanel, TabsRoot } from '@/components/Tabs.tsx';
+
+/**
+ * Injected by the `satteriCodeFrame` MDAST plugin,
+ * which wraps each standalone code block in ``.
+ * Sätteri will handle passing a `
` to the slot.
+ */
+interface Props {
+  title?: string;
+  lang?: string;
+}
+
+const { title, lang } = Astro.props;
+const label = title || lang || 'code';
+const value = 'code';
+---
+
+
+  
+    {label}
+  
+  
+    
+  
+
diff --git a/site/src/components/typography/MarkdownCode.astro b/site/src/components/typography/MarkdownCode.astro
index d7c4fbbc..9fa652e4 100644
--- a/site/src/components/typography/MarkdownCode.astro
+++ b/site/src/components/typography/MarkdownCode.astro
@@ -6,22 +6,11 @@ import { shared } from './styles';
 
 type Props = Polymorphic<{ as: Tag }> & {
   class?: string;
-  codeBlock?: string;
 };
 
-const { as: Tag = 'code', class: className, codeBlock, ...props } = Astro.props;
-
-// When codeBlock="true", render plain code tag (Shiki handles styling)
-// Otherwise, render inline code with styling
-const isCodeBlock = codeBlock === 'true';
+const { as: Tag = 'code', class: className, ...props } = Astro.props;
 ---
 
 {
-  isCodeBlock ? (
-    // prettier-ignore
-    
-  ) : (
-    // prettier-ignore
-    
-  )
-}
+  /* prettier-ignore */}
+
diff --git a/site/src/components/typography/Pre.astro b/site/src/components/typography/Pre.astro
deleted file mode 100644
index c7c9aa84..00000000
--- a/site/src/components/typography/Pre.astro
+++ /dev/null
@@ -1,38 +0,0 @@
----
-
-import type { HTMLTag, Polymorphic } from 'astro/types';
-import { Tab, TabsList, TabsPanel, TabsRoot } from '@/components/Tabs.tsx';
-
-type Props = Polymorphic<{ as: Tag }> & {
-  maxWidth?: boolean;
-  class?: string;
-  hasFrame?: boolean;
-  title?: string;
-};
-
-const { as: Tag = 'pre', maxWidth = false, class: className, style: _style, title, hasFrame, ...props } = Astro.props;
-
-const language = props['data-language'];
-const label = title || language || 'code';
-
-const value = 'code';
----
-
-{
-  hasFrame ? (
-    // prettier-ignore
-    
-  ) : (
-    
-      
-        
-          {label}
-        
-      
-      
-        {/* prettier-ignore */}
-        
-      
-    
-  )
-}
diff --git a/site/src/components/typography/defaultMarkdownComponents.ts b/site/src/components/typography/defaultMarkdownComponents.ts
index 49b000c6..131fe710 100644
--- a/site/src/components/typography/defaultMarkdownComponents.ts
+++ b/site/src/components/typography/defaultMarkdownComponents.ts
@@ -1,5 +1,6 @@
 import A from './A.astro';
 import Blockquote from './Blockquote.astro';
+import CodeFrame from './CodeFrame.astro';
 import Em from './Em.astro';
 import H1Warning from './H1Warning.astro';
 import H2Markdown from './H2Markdown.astro';
@@ -13,7 +14,6 @@ import Li from './Li.astro';
 import MarkdownCode from './MarkdownCode.astro';
 import Ol from './Ol.astro';
 import P from './P.astro';
-import Pre from './Pre.astro';
 import Strong from './Strong.astro';
 import Table from './Table.astro';
 import Tbody from './Tbody.astro';
@@ -40,8 +40,8 @@ const defaultMarkdownComponents = {
   blockquote: Blockquote,
   hr: Hr,
   img: Img,
-  pre: Pre,
   code: MarkdownCode,
+  CodeFrame,
   table: Table,
   thead: Thead,
   tbody: Tbody,
diff --git a/site/src/styles/shiki-transformers.css b/site/src/styles/shiki-transformers.css
index 3061c687..cfd392e4 100644
--- a/site/src/styles/shiki-transformers.css
+++ b/site/src/styles/shiki-transformers.css
@@ -1,4 +1,15 @@
 @layer base {
+  /* matches `shared.codeBlock`: font-mono + text-code */
+  .astro-code {
+    font-family: var(--font-mono), monospace;
+    font-variant-ligatures: none;
+  }
+
+  /* reinforcing shared.codeBlock */
+  .astro-code code {
+    font-size: var(--text-code);
+  }
+
   .astro-code .line {
     display: inline-block;
   }
diff --git a/site/src/utils/featureReferenceModel.js b/site/src/utils/featureReferenceModel.js
index 44dc9573..6cf45968 100644
--- a/site/src/utils/featureReferenceModel.js
+++ b/site/src/utils/featureReferenceModel.js
@@ -2,7 +2,7 @@
  * Centralized feature API reference subsection definitions.
  *
  * Mirrors componentReferenceModel.js for feature APIs. Produces heading/id data
- * consumed by both FeatureReference.astro and remarkConditionalHeadings.
+ * consumed by both FeatureReference.astro and satteriConditionalHeadings.
  *
  * Structure:
  *   ## API Reference (H2)
diff --git a/site/src/utils/mediaReferenceModel.js b/site/src/utils/mediaReferenceModel.js
index bf044ebc..10109128 100644
--- a/site/src/utils/mediaReferenceModel.js
+++ b/site/src/utils/mediaReferenceModel.js
@@ -2,7 +2,7 @@
  * Centralized media element API subsection definitions.
  *
  * Mirrors componentReferenceModel.js for media elements. Produces heading/id
- * data consumed by both MediaReference.astro and remarkConditionalHeadings.
+ * data consumed by both MediaReference.astro and satteriConditionalHeadings.
  */
 
 const MEDIA_REFERENCE_SUBSECTIONS = Object.freeze([
diff --git a/site/src/utils/rehypePrepareCodeBlocks.js b/site/src/utils/rehypePrepareCodeBlocks.js
deleted file mode 100644
index 00b19c7f..00000000
--- a/site/src/utils/rehypePrepareCodeBlocks.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Adapted from https://mdxjs.com/guides/syntax-highlighting/
- *
- * This plugin:
- * 1. Tags  children of 
 blocks so they know they're in a pre block
- * 2. Marks 
 blocks with hasFrame based on whether they're inside a  JSX component
- */
-
-export default function rehypePrepareCodeBlocks() {
-  return (tree) => {
-    // Process the tree with a stateful visitor
-    function visitWithContext(node, context = { hasFrame: false }) {
-      // Handle TabsPanel JSX component
-      if (node.type === 'mdxJsxFlowElement' && node.name === 'TabsPanel') {
-        // Create new context for children (inside tabs)
-        const newContext = { hasFrame: true };
-
-        // Visit children with new context
-        if (node.children) {
-          node.children.forEach((child) => visitWithContext(child, newContext));
-        }
-
-        return;
-      }
-
-      // Handle 
 elements
-      if (node.type === 'element' && node.tagName === 'pre') {
-        // Mark whether this pre block is inside tabs
-        node.properties.hasFrame = context.hasFrame;
-
-        // Tag  children
-        node.children.forEach((child) => {
-          if (child.tagName === 'code') {
-            child.properties.codeBlock = 'true';
-          }
-        });
-      }
-
-      // Recursively visit children for other node types
-      if (node.children) {
-        node.children.forEach((child) => visitWithContext(child, context));
-      }
-    }
-
-    // Start visiting from root
-    if (tree.children) {
-      tree.children.forEach((child) => visitWithContext(child));
-    }
-  };
-}
diff --git a/site/src/utils/remarkConditionalHeadings.js b/site/src/utils/remarkConditionalHeadings.js
deleted file mode 100644
index c927557d..00000000
--- a/site/src/utils/remarkConditionalHeadings.js
+++ /dev/null
@@ -1,282 +0,0 @@
-import * as fs from 'node:fs';
-import * as path from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { kebabCase } from 'es-toolkit/string';
-import GithubSlugger from 'github-slugger';
-import { resolveReferenceSlug } from './api-reference-overrides';
-import { buildComponentReferenceTocHeadings, createComponentReferenceModel } from './componentReferenceModel';
-import { buildFeatureReferenceTocHeadings, createFeatureReferenceModel } from './featureReferenceModel';
-import { buildMediaReferenceTocHeadings, createMediaReferenceModel } from './mediaReferenceModel';
-import { buildUtilReferenceTocHeadings, createUtilReferenceModel } from './utilReferenceModel';
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
-const COMPONENT_REF_DIR = path.resolve(__dirname, '../content/generated-component-reference');
-const FEATURE_REF_DIR = path.resolve(__dirname, '../content/generated-feature-reference');
-const UTIL_REF_DIR = path.resolve(__dirname, '../content/generated-util-reference');
-const MEDIA_REF_DIR = path.resolve(__dirname, '../content/generated-media-reference');
-
-function readComponentRefJson(slug) {
-  const filePath = path.join(COMPONENT_REF_DIR, `${slug}.json`);
-  try {
-    return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
-  } catch {
-    return null;
-  }
-}
-
-/**
- * Remark plugin that tracks headings wrapped in FrameworkCase or StyleCase components
- * and adds conditional metadata to them.
- *
- * Also detects `` components and injects heading metadata from
- * generated JSON, so component-rendered headings appear in the table of contents.
- */
-export default function remarkConditionalHeadings() {
-  return (tree, file) => {
-    const headingsWithMetadata = [];
-    const slugger = new GithubSlugger();
-    const reservedSlugs = new Set();
-
-    // Process the tree with a stateful visitor
-    function visitWithContext(node, context = { frameworks: null, styles: null }) {
-      // Handle FrameworkCase and StyleCase components
-      if (node.type === 'mdxJsxFlowElement') {
-        if (node.name === 'FrameworkCase') {
-          const frameworksAttr = node.attributes?.find((attr) => attr.name === 'frameworks');
-          const frameworks = extractArrayValue(frameworksAttr);
-
-          // Create new context for children
-          const newContext = { ...context, frameworks };
-
-          // Visit children with new context
-          if (node.children) {
-            node.children.forEach((child) => visitWithContext(child, newContext));
-          }
-
-          return;
-        } else if (node.name === 'StyleCase') {
-          const stylesAttr = node.attributes?.find((attr) => attr.name === 'styles');
-          const styles = extractArrayValue(stylesAttr);
-
-          // Create new context for children
-          const newContext = { ...context, styles };
-
-          // Visit children with new context
-          if (node.children) {
-            node.children.forEach((child) => visitWithContext(child, newContext));
-          }
-
-          return;
-        } else if (node.name === 'ComponentReference') {
-          injectComponentReferenceHeadings(node, headingsWithMetadata, reservedSlugs);
-        } else if (node.name === 'FeatureReference') {
-          injectFeatureReferenceHeadings(node, headingsWithMetadata, reservedSlugs);
-        } else if (node.name === 'UtilReference') {
-          injectUtilReferenceHeadings(node, headingsWithMetadata, reservedSlugs);
-          return;
-        } else if (node.name === 'MediaReference') {
-          injectMediaReferenceHeadings(node, headingsWithMetadata, reservedSlugs);
-          return;
-        }
-      }
-
-      // Handle headings
-      if (node.type === 'heading') {
-        const text = extractText(node);
-        let slug = slugger.slug(text);
-
-        // Avoid collisions with explicit API reference ids.
-        while (reservedSlugs.has(slug)) {
-          slug = slugger.slug(text);
-        }
-        reservedSlugs.add(slug);
-
-        const metadata = {
-          depth: node.depth,
-          text,
-          slug,
-        };
-
-        // Add conditional context if present
-        if (context.frameworks) {
-          metadata.frameworks = context.frameworks;
-        }
-        if (context.styles) {
-          metadata.styles = context.styles;
-        }
-
-        headingsWithMetadata.push(metadata);
-      }
-
-      // Recursively visit children for other node types
-      if (node.children) {
-        node.children.forEach((child) => visitWithContext(child, context));
-      }
-    }
-
-    // Start visiting from root
-    if (tree.children) {
-      tree.children.forEach((child) => visitWithContext(child));
-    }
-
-    // Attach to file data for retrieval via remarkPluginFrontmatter
-    if (!file.data.astro) {
-      file.data.astro = {};
-    }
-    if (!file.data.astro.frontmatter) {
-      file.data.astro.frontmatter = {};
-    }
-    file.data.astro.frontmatter.conditionalHeadings = headingsWithMetadata;
-  };
-}
-
-/**
- * Inject heading metadata from generated API reference JSON.
- *
- * For multi-part components, injects framework-conditional part headings.
- * For single-part components, injects "API reference".
- * For each, injects Props/State/Data attributes headings
- */
-function injectComponentReferenceHeadings(node, headingsWithMetadata, reservedSlugs) {
-  const componentAttr = node.attributes?.find((a) => a.name === 'component');
-  const componentName = typeof componentAttr?.value === 'string' ? componentAttr.value : null;
-  if (!componentName) return;
-
-  const json = readComponentRefJson(resolveReferenceSlug(componentName));
-  if (!json) return;
-
-  const partOrderAttr = node.attributes?.find((a) => a.name === 'partOrder');
-  const partOrder = extractArrayValue(partOrderAttr);
-
-  const componentModel = createComponentReferenceModel(componentName, json, partOrder);
-  const componentHeadings = buildComponentReferenceTocHeadings(componentModel);
-
-  headingsWithMetadata.push(...componentHeadings);
-  for (const heading of componentHeadings) {
-    reservedSlugs.add(heading.slug);
-  }
-}
-
-function readFeatureRefJson(featureName) {
-  const filePath = path.join(FEATURE_REF_DIR, `${featureName}.json`);
-  try {
-    return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
-  } catch {
-    return null;
-  }
-}
-
-function injectFeatureReferenceHeadings(node, headingsWithMetadata, reservedSlugs) {
-  const featureAttr = node.attributes?.find((a) => a.name === 'feature');
-  const featureName = typeof featureAttr?.value === 'string' ? featureAttr.value : null;
-  if (!featureName) return;
-
-  const json = readFeatureRefJson(featureName);
-  if (!json) return;
-
-  const featureModel = createFeatureReferenceModel(featureName, json);
-  const featureHeadings = buildFeatureReferenceTocHeadings(featureModel);
-
-  headingsWithMetadata.push(...featureHeadings);
-  for (const heading of featureHeadings) {
-    reservedSlugs.add(heading.slug);
-  }
-}
-
-function readUtilRefJson(slug) {
-  const filePath = path.join(UTIL_REF_DIR, `${slug}.json`);
-  try {
-    return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
-  } catch {
-    return null;
-  }
-}
-
-function injectUtilReferenceHeadings(node, headingsWithMetadata, reservedSlugs) {
-  const utilAttr = node.attributes?.find((a) => a.name === 'util');
-  const utilName = typeof utilAttr?.value === 'string' ? utilAttr.value : null;
-  if (!utilName) return;
-
-  const slugAttr = node.attributes?.find((a) => a.name === 'slug');
-  const slugValue = typeof slugAttr?.value === 'string' ? slugAttr.value : null;
-
-  const json = readUtilRefJson(slugValue ?? kebabCase(utilName));
-  if (!json) return;
-
-  const utilModel = createUtilReferenceModel(utilName, json);
-  const utilHeadings = buildUtilReferenceTocHeadings(utilModel);
-
-  headingsWithMetadata.push(...utilHeadings);
-  for (const heading of utilHeadings) {
-    reservedSlugs.add(heading.slug);
-  }
-}
-
-function readMediaRefJson(tagName) {
-  const filePath = path.join(MEDIA_REF_DIR, `${tagName}.json`);
-  try {
-    return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
-  } catch {
-    return null;
-  }
-}
-
-function injectMediaReferenceHeadings(node, headingsWithMetadata, reservedSlugs) {
-  const mediaAttr = node.attributes?.find((a) => a.name === 'media');
-  const mediaName = typeof mediaAttr?.value === 'string' ? mediaAttr.value : null;
-  if (!mediaName) return;
-
-  const json = readMediaRefJson(resolveReferenceSlug(mediaName));
-  if (!json) return;
-
-  const mediaModel = createMediaReferenceModel(mediaName, json);
-  const mediaHeadings = buildMediaReferenceTocHeadings(mediaModel);
-
-  headingsWithMetadata.push(...mediaHeadings);
-  for (const heading of mediaHeadings) {
-    reservedSlugs.add(heading.slug);
-  }
-}
-
-/**
- * Extract array value from JSX attribute like frameworks={["react", "html"]}
- */
-function extractArrayValue(attr) {
-  if (!attr || !attr.value) {
-    return null;
-  }
-
-  // Handle JSX expression
-  if (attr.value.type === 'mdxJsxAttributeValueExpression') {
-    const expression = attr.value.value;
-    try {
-      // Parse the array from the expression
-      // This is a simple approach that works for basic arrays
-      return JSON.parse(expression.trim());
-    } catch (e) {
-      console.warn(`Failed to parse JSX expression: ${expression}`, e);
-      return null;
-    }
-  }
-
-  return null;
-}
-
-/**
- * Extract text content from a heading node
- */
-function extractText(node) {
-  if (node.type === 'text') {
-    return node.value;
-  }
-
-  if (node.type === 'inlineCode') {
-    return node.value;
-  }
-
-  if (node.children) {
-    return node.children.map((child) => extractText(child)).join('');
-  }
-
-  return '';
-}
diff --git a/site/src/utils/remarkReadingTime.mjs b/site/src/utils/remarkReadingTime.mjs
deleted file mode 100644
index f4a429e5..00000000
--- a/site/src/utils/remarkReadingTime.mjs
+++ /dev/null
@@ -1,21 +0,0 @@
-// biome-ignore lint/suspicious/noShadowRestrictedNames: 🤷
-import { toString } from 'mdast-util-to-string';
-import getReadingTime from 'reading-time';
-
-/**
- * Remark plugin that calculates reading time for markdown/MDX content.
- * Injects the reading time into remarkPluginFrontmatter for use in templates.
- * adapted from https://docs.astro.build/en/recipes/reading-time/
- */
-export function remarkReadingTime() {
-  return (tree, { data }) => {
-    const textOnPage = toString(tree);
-    const readingTime = getReadingTime(textOnPage);
-
-    // Inject reading time into frontmatter
-    data.astro.frontmatter.minutesRead = readingTime.text;
-
-    // Also provide the numeric minutes value for easier access
-    data.astro.frontmatter.readingTimeMinutes = readingTime.minutes;
-  };
-}
diff --git a/site/src/utils/satteriAstroData.ts b/site/src/utils/satteriAstroData.ts
new file mode 100644
index 00000000..78a6670e
--- /dev/null
+++ b/site/src/utils/satteriAstroData.ts
@@ -0,0 +1,24 @@
+import type { MdastPluginInstance } from 'satteri';
+
+/**
+ * Sätteri doesn't export its visitor-context class, so derive it from a visitor
+ * signature. Every visitor receives the same context object.
+ */
+export type MdastVisitorContext = Parameters>[1];
+
+/**
+ * Shape of the document data bag `@astrojs/markdown-satteri` (and the MDX
+ * integration's Sätteri path) seed before running plugins. Whatever a plugin
+ * leaves on `astro.frontmatter` is surfaced to templates as
+ * `render().remarkPluginFrontmatter`.
+ */
+interface AstroData {
+  frontmatter: Record;
+  headings: Array<{ depth: number; slug: string; text: string }>;
+}
+
+/** Typed accessor for the Astro frontmatter bag a Sätteri plugin writes into. */
+export function getAstroFrontmatter(ctx: MdastVisitorContext): Record | undefined {
+  const astro = (ctx.data as { astro?: AstroData }).astro;
+  return astro?.frontmatter;
+}
diff --git a/site/src/utils/satteriCodeFrame.ts b/site/src/utils/satteriCodeFrame.ts
new file mode 100644
index 00000000..5f005b84
--- /dev/null
+++ b/site/src/utils/satteriCodeFrame.ts
@@ -0,0 +1,45 @@
+import type { MdastContent } from 'satteri';
+import { defineMdastPlugin } from 'satteri';
+import type { MdastVisitorContext } from './satteriAstroData';
+
+const TITLE_RE = /title=(?:"([^"]+)"|'([^']+)'|([^\s"']+))/;
+
+/**
+ * Wraps each standalone fenced code block in a `` component so it
+ * renders with a filename/language header and a copy button.
+ *
+ * This runs at the MDAST stage rather than as a `pre`/`code` component override
+ * because Shiki rewrites each `
` into raw HTML before any HAST plugin or
+ * component override runs — wrapping the node here keeps a real component frame
+ * around the still-highlighted code.
+ *
+ * Code blocks already inside an authored `` are left untouched: the
+ * tab group is their frame. The title comes from the fence meta
+ * (e.g. ```ts title="App.ts"```).
+ */
+export function satteriCodeFrame() {
+  return defineMdastPlugin({
+    name: 'astro-code-frame',
+    code: (node, ctx) => {
+      // Skip blocks framed by an authored tab group.
+      let ancestor: ReturnType = ctx.parent(node);
+      while (ancestor) {
+        if (ancestor.type === 'mdxJsxFlowElement' && ancestor.name === 'TabsPanel') return;
+        ancestor = ctx.parent(ancestor);
+      }
+
+      const titleMatch = node.meta?.match(TITLE_RE);
+      const title = titleMatch ? (titleMatch[1] ?? titleMatch[2] ?? titleMatch[3]) : undefined;
+
+      const attributes = [{ type: 'mdxJsxAttribute', name: 'lang', value: node.lang ?? '' }];
+      if (title) attributes.push({ type: 'mdxJsxAttribute', name: 'title', value: title });
+
+      ctx.wrapNode(node, {
+        type: 'mdxJsxFlowElement',
+        name: 'CodeFrame',
+        attributes,
+        children: [],
+      } as MdastContent);
+    },
+  });
+}
diff --git a/site/src/utils/satteriConditionalHeadings.ts b/site/src/utils/satteriConditionalHeadings.ts
new file mode 100644
index 00000000..6597511e
--- /dev/null
+++ b/site/src/utils/satteriConditionalHeadings.ts
@@ -0,0 +1,196 @@
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { kebabCase } from 'es-toolkit/string';
+import GithubSlugger from 'github-slugger';
+import type { MdastPluginInput, MdxJsxFlowElement } from 'satteri';
+import { defineMdastPlugin } from 'satteri';
+import { resolveReferenceSlug } from './api-reference-overrides';
+import { buildComponentReferenceTocHeadings, createComponentReferenceModel } from './componentReferenceModel';
+import { buildFeatureReferenceTocHeadings, createFeatureReferenceModel } from './featureReferenceModel';
+import { buildMediaReferenceTocHeadings, createMediaReferenceModel } from './mediaReferenceModel';
+import { getAstroFrontmatter, type MdastVisitorContext } from './satteriAstroData';
+import { buildUtilReferenceTocHeadings, createUtilReferenceModel } from './utilReferenceModel';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const COMPONENT_REF_DIR = path.resolve(__dirname, '../content/generated-component-reference');
+const FEATURE_REF_DIR = path.resolve(__dirname, '../content/generated-feature-reference');
+const UTIL_REF_DIR = path.resolve(__dirname, '../content/generated-util-reference');
+const MEDIA_REF_DIR = path.resolve(__dirname, '../content/generated-media-reference');
+
+interface ConditionalHeading {
+  depth: number;
+  text: string;
+  slug: string;
+  frameworks?: string[];
+  styles?: string[];
+  tocKind?: string;
+}
+
+/**
+ * Builds the conditional-heading list used for the docs table of contents.
+ *
+ * - Tracks which `` / `` a heading lives in (walking
+ *   ancestors) and attaches that context.
+ * - Reads `` / `` / `` /
+ *   `` props, loads the generated JSON, and injects heading
+ *   entries so API-reference sections appear in the TOC.
+ *
+ * Markdown headings are slugged with a plain GithubSlugger in document order so
+ * the slugs match the element ids the markdown-satteri `heading-ids` plugin
+ * generates (otherwise TOC anchors would not resolve). API-reference headings
+ * keep their model-generated slugs, which match the ids their components render.
+ *
+ * A factory resets the per-document slugger and heading list. Sätteri has no
+ * end hook, so we publish the (mutated-in-place) array reference onto the
+ * frontmatter once and keep pushing to it.
+ */
+export function satteriConditionalHeadings(): MdastPluginInput {
+  return () => {
+    const headings: ConditionalHeading[] = [];
+    const slugger = new GithubSlugger();
+    let published = false;
+
+    const publish = (ctx: MdastVisitorContext) => {
+      if (published) return;
+      const frontmatter = getAstroFrontmatter(ctx);
+      if (!frontmatter) return;
+      frontmatter.conditionalHeadings = headings;
+      published = true;
+    };
+
+    return defineMdastPlugin({
+      name: 'astro-conditional-headings',
+
+      heading: (node, ctx) => {
+        publish(ctx);
+
+        const text = ctx.textContent(node);
+        const heading: ConditionalHeading = {
+          depth: node.depth,
+          text,
+          slug: slugger.slug(text),
+        };
+
+        const { frameworks, styles } = resolveCaseContext(node, ctx);
+        if (frameworks) heading.frameworks = frameworks;
+        if (styles) heading.styles = styles;
+
+        headings.push(heading);
+      },
+
+      mdxJsxFlowElement: (node, ctx) => {
+        switch (node.name) {
+          case 'ComponentReference':
+            publish(ctx);
+            injectComponentReferenceHeadings(node, headings);
+            break;
+          case 'FeatureReference':
+            publish(ctx);
+            injectFeatureReferenceHeadings(node, headings);
+            break;
+          case 'UtilReference':
+            publish(ctx);
+            injectUtilReferenceHeadings(node, headings);
+            break;
+          case 'MediaReference':
+            publish(ctx);
+            injectMediaReferenceHeadings(node, headings);
+            break;
+        }
+      },
+    });
+  };
+}
+
+/** Walk ancestors to find the nearest enclosing FrameworkCase / StyleCase. */
+function resolveCaseContext(
+  node: Parameters[0],
+  ctx: MdastVisitorContext
+): { frameworks: string[] | null; styles: string[] | null } {
+  let frameworks: string[] | null = null;
+  let styles: string[] | null = null;
+
+  let current = ctx.parent(node);
+  while (current) {
+    if (current.type === 'mdxJsxFlowElement') {
+      const el = current as MdxJsxFlowElement;
+      if (!frameworks && el.name === 'FrameworkCase') {
+        frameworks = extractArrayAttr(el, 'frameworks');
+      } else if (!styles && el.name === 'StyleCase') {
+        styles = extractArrayAttr(el, 'styles');
+      }
+    }
+    current = ctx.parent(current);
+  }
+
+  return { frameworks, styles };
+}
+
+function getStringAttr(node: MdxJsxFlowElement, name: string): string | null {
+  const attr = node.attributes?.find((a) => a.type === 'mdxJsxAttribute' && a.name === name);
+  return attr && typeof attr.value === 'string' ? attr.value : null;
+}
+
+/** Parse a JSX expression attribute like `frameworks={["react", "html"]}`. */
+function extractArrayAttr(node: MdxJsxFlowElement, name: string): string[] | null {
+  const attr = node.attributes?.find((a) => a.type === 'mdxJsxAttribute' && a.name === name);
+  if (!attr || !attr.value || typeof attr.value === 'string') return null;
+  if (attr.value.type !== 'mdxJsxAttributeValueExpression') return null;
+  try {
+    return JSON.parse(attr.value.value.trim());
+  } catch (e) {
+    console.warn(`Failed to parse JSX expression: ${attr.value.value}`, e);
+    return null;
+  }
+}
+
+function readRefJson(dir: string, key: string): unknown {
+  try {
+    return JSON.parse(fs.readFileSync(path.join(dir, `${key}.json`), 'utf-8'));
+  } catch {
+    return null;
+  }
+}
+
+function injectComponentReferenceHeadings(node: MdxJsxFlowElement, headings: ConditionalHeading[]) {
+  const componentName = getStringAttr(node, 'component');
+  if (!componentName) return;
+  const json = readRefJson(COMPONENT_REF_DIR, resolveReferenceSlug(componentName));
+  if (!json) return;
+  const partOrder = extractArrayAttr(node, 'partOrder');
+  const model = createComponentReferenceModel(
+    componentName,
+    json as Parameters[1],
+    partOrder ?? undefined
+  );
+  headings.push(...buildComponentReferenceTocHeadings(model));
+}
+
+function injectFeatureReferenceHeadings(node: MdxJsxFlowElement, headings: ConditionalHeading[]) {
+  const featureName = getStringAttr(node, 'feature');
+  if (!featureName) return;
+  const json = readRefJson(FEATURE_REF_DIR, featureName);
+  if (!json) return;
+  const model = createFeatureReferenceModel(featureName, json);
+  headings.push(...buildFeatureReferenceTocHeadings(model));
+}
+
+function injectUtilReferenceHeadings(node: MdxJsxFlowElement, headings: ConditionalHeading[]) {
+  const utilName = getStringAttr(node, 'util');
+  if (!utilName) return;
+  const slug = getStringAttr(node, 'slug');
+  const json = readRefJson(UTIL_REF_DIR, slug ?? kebabCase(utilName));
+  if (!json) return;
+  const model = createUtilReferenceModel(utilName, json as Parameters[1]);
+  headings.push(...buildUtilReferenceTocHeadings(model));
+}
+
+function injectMediaReferenceHeadings(node: MdxJsxFlowElement, headings: ConditionalHeading[]) {
+  const mediaName = getStringAttr(node, 'media');
+  if (!mediaName) return;
+  const json = readRefJson(MEDIA_REF_DIR, resolveReferenceSlug(mediaName));
+  if (!json) return;
+  const model = createMediaReferenceModel(mediaName, json);
+  headings.push(...buildMediaReferenceTocHeadings(model));
+}
diff --git a/site/src/utils/satteriReadingTime.ts b/site/src/utils/satteriReadingTime.ts
new file mode 100644
index 00000000..5b7df15c
--- /dev/null
+++ b/site/src/utils/satteriReadingTime.ts
@@ -0,0 +1,35 @@
+import getReadingTime from 'reading-time';
+import type { MdastPluginInput } from 'satteri';
+import { defineMdastPlugin } from 'satteri';
+import { getAstroFrontmatter, type MdastVisitorContext } from './satteriAstroData';
+
+/**
+ * Calculates reading time and injects it into the Astro frontmatter bag for
+ * templates (read via `remarkPluginFrontmatter`).
+ *
+ * Returned as a factory so the text accumulator resets per document. Sätteri
+ * has no end-of-document hook, so text is accumulated across literal nodes and
+ * the reading time is recomputed as it grows; the final visit leaves the
+ * correct value on the frontmatter.
+ */
+export function satteriReadingTime(): MdastPluginInput {
+  return () => {
+    let text = '';
+
+    const accumulate = (value: string, ctx: MdastVisitorContext) => {
+      text += `${value} `;
+      const frontmatter = getAstroFrontmatter(ctx);
+      if (!frontmatter) return;
+      const readingTime = getReadingTime(text);
+      frontmatter.minutesRead = readingTime.text;
+      frontmatter.readingTimeMinutes = readingTime.minutes;
+    };
+
+    return defineMdastPlugin({
+      name: 'astro-reading-time',
+      text: (node, ctx) => accumulate(node.value, ctx),
+      inlineCode: (node, ctx) => accumulate(node.value, ctx),
+      code: (node, ctx) => accumulate(node.value, ctx),
+    });
+  };
+}
diff --git a/site/src/utils/shikiStripPreStyle.ts b/site/src/utils/shikiStripPreStyle.ts
new file mode 100644
index 00000000..798dcc9c
--- /dev/null
+++ b/site/src/utils/shikiStripPreStyle.ts
@@ -0,0 +1,19 @@
+import type { ShikiTransformer } from 'shiki';
+
+/**
+ * Strip the inline `style` Astro's Shiki highlighter writes onto the `
`
+ * (the theme `background-color`/`color` and a trailing `overflow-x: auto`).
+ *
+ * Shiki should only highlight the text; the code container's background and
+ * scrolling are owned by `CodeFrame` and the `.astro-code` rules. Token colors
+ * live on the inner spans, so removing the pre's style leaves them untouched.
+ *
+ * Astro adds its built-in `pre` transformer before user transformers, so this
+ * one runs last and sees the fully-assembled style to remove.
+ */
+export const shikiStripPreStyle: ShikiTransformer = {
+  name: 'strip-pre-inline-style',
+  pre(node) {
+    delete node.properties.style;
+  },
+};
diff --git a/site/src/utils/shikiTransformMetadata.js b/site/src/utils/shikiTransformMetadata.js
deleted file mode 100644
index 97085387..00000000
--- a/site/src/utils/shikiTransformMetadata.js
+++ /dev/null
@@ -1,12 +0,0 @@
-const shikiTransformMetadata = {
-  pre(hast) {
-    // get stuff out of this.options.meta?.__raw;
-    // for now, let's start with just title="abc" or title='abc' or title=abc
-    const raw = this.options.meta?.__raw || '';
-    const titleMatch = raw.match(/title=(?:"([^"]+)"|'([^']+)'|([^\s"']+))/);
-    if (titleMatch) {
-      hast.properties.title = titleMatch[1] || titleMatch[2] || titleMatch[3];
-    }
-  },
-};
-export default shikiTransformMetadata;
diff --git a/site/src/utils/tests/mediaReferenceModel.test.ts b/site/src/utils/tests/mediaReferenceModel.test.ts
index 2d9ae5a6..52eb105a 100644
--- a/site/src/utils/tests/mediaReferenceModel.test.ts
+++ b/site/src/utils/tests/mediaReferenceModel.test.ts
@@ -1,4 +1,4 @@
-// @ts-nocheck — the model is plain JS shared with remarkConditionalHeadings
+// @ts-nocheck — the model is plain JS shared with satteriConditionalHeadings
 import { describe, expect, it } from 'vitest';
 import { buildMediaReferenceTocHeadings, createMediaReferenceModel } from '../mediaReferenceModel';
 
diff --git a/site/src/utils/tests/satteriCodeFrame.test.ts b/site/src/utils/tests/satteriCodeFrame.test.ts
new file mode 100644
index 00000000..d5574bed
--- /dev/null
+++ b/site/src/utils/tests/satteriCodeFrame.test.ts
@@ -0,0 +1,37 @@
+// @vitest-environment node
+// Sätteri's native binding builds typed-array buffers that fail against jsdom's
+// patched ArrayBuffer/DataView globals; run these against the real node realm.
+import { mdxToJs } from 'satteri';
+import { describe, expect, it } from 'vitest';
+import { satteriCodeFrame } from '../satteriCodeFrame';
+
+function compile(source: string): string {
+  const data = {
+    astro: {
+      frontmatter: {},
+      headings: [],
+      localImagePaths: new Set(),
+      remoteImagePaths: new Set(),
+    },
+  };
+  const { code } = mdxToJs(source, { mdastPlugins: [satteriCodeFrame()], data });
+  return code;
+}
+
+describe('satteriCodeFrame', () => {
+  it('wraps a standalone code block in CodeFrame', () => {
+    const code = compile('```ts\nconst a = 1;\n```');
+    expect(code).toContain('CodeFrame');
+  });
+
+  it('passes the fence title and language as props', () => {
+    const code = compile('```ts title="App.ts"\nconst a = 1;\n```');
+    expect(code).toContain('App.ts');
+    expect(code).toContain('ts');
+  });
+
+  it('does not wrap a code block already inside a TabsPanel', () => {
+    const code = compile('\n\n```bash\nnpm i\n```\n\n');
+    expect(code).not.toContain('CodeFrame');
+  });
+});
diff --git a/site/src/utils/tests/satteriConditionalHeadings.test.ts b/site/src/utils/tests/satteriConditionalHeadings.test.ts
new file mode 100644
index 00000000..fc1b6b18
--- /dev/null
+++ b/site/src/utils/tests/satteriConditionalHeadings.test.ts
@@ -0,0 +1,50 @@
+// @vitest-environment node
+// Sätteri's native binding builds typed-array buffers that fail against jsdom's
+// patched ArrayBuffer/DataView globals; run these against the real node realm.
+import { mdxToJs } from 'satteri';
+import { describe, expect, it } from 'vitest';
+import { satteriConditionalHeadings } from '../satteriConditionalHeadings';
+
+interface Heading {
+  depth: number;
+  text: string;
+  slug: string;
+  frameworks?: string[];
+  styles?: string[];
+}
+
+function collect(source: string): Heading[] {
+  const data = {
+    astro: {
+      frontmatter: {} as Record,
+      headings: [],
+      localImagePaths: new Set(),
+      remoteImagePaths: new Set(),
+    },
+  };
+  mdxToJs(source, { mdastPlugins: [satteriConditionalHeadings()], data });
+  return (data.astro.frontmatter.conditionalHeadings ?? []) as Heading[];
+}
+
+describe('satteriConditionalHeadings', () => {
+  it('collects headings with github-style slugs in document order', () => {
+    const headings = collect('## Hello World\n\n### Nested Heading');
+    expect(headings).toEqual([
+      { depth: 2, text: 'Hello World', slug: 'hello-world' },
+      { depth: 3, text: 'Nested Heading', slug: 'nested-heading' },
+    ]);
+  });
+
+  it('attaches framework context from an enclosing FrameworkCase', () => {
+    const headings = collect(
+      '## Shared\n\n\n\n## React Only\n\n'
+    );
+    expect(headings.find((h) => h.text === 'Shared')?.frameworks).toBeUndefined();
+    expect(headings.find((h) => h.text === 'React Only')?.frameworks).toEqual(['react']);
+  });
+
+  it('attaches style context from an enclosing StyleCase', () => {
+    const headings = collect('\n\n## CSS Only\n\n');
+    expect(headings.find((h) => h.text === 'CSS Only')?.styles).toEqual(['css']);
+  });
+});
diff --git a/site/src/utils/tests/satteriReadingTime.test.ts b/site/src/utils/tests/satteriReadingTime.test.ts
new file mode 100644
index 00000000..1df757e5
--- /dev/null
+++ b/site/src/utils/tests/satteriReadingTime.test.ts
@@ -0,0 +1,35 @@
+// @vitest-environment node
+// Sätteri's native binding builds typed-array buffers that fail against jsdom's
+// patched ArrayBuffer/DataView globals; run these against the real node realm.
+import { markdownToHtml } from 'satteri';
+import { describe, expect, it } from 'vitest';
+import { satteriReadingTime } from '../satteriReadingTime';
+
+function render(source: string) {
+  const data = {
+    astro: {
+      frontmatter: {} as Record,
+      headings: [],
+      localImagePaths: new Set(),
+      remoteImagePaths: new Set(),
+    },
+  };
+  markdownToHtml(source, { mdastPlugins: [satteriReadingTime()], data });
+  return data.astro.frontmatter;
+}
+
+describe('satteriReadingTime', () => {
+  it('injects reading time into the frontmatter bag', () => {
+    const words = Array.from({ length: 500 }, (_, i) => `word${i}`).join(' ');
+    const frontmatter = render(`# Title\n\n${words}`);
+
+    expect(frontmatter.minutesRead).toMatch(/min read/);
+    expect(typeof frontmatter.readingTimeMinutes).toBe('number');
+    expect(frontmatter.readingTimeMinutes as number).toBeGreaterThan(0);
+  });
+
+  it('counts code and inline code toward the total', () => {
+    const withCode = render('# Title\n\nSome `inline` text\n\n```ts\nconst a = 1;\n```');
+    expect(withCode.minutesRead).toMatch(/min read/);
+  });
+});