From efd3db03ce2bf065d6b89ee38e765cfe7985862d Mon Sep 17 00:00:00 2001 From: fretchen Date: Thu, 30 Jul 2026 19:18:29 +0200 Subject: [PATCH 1/4] Cuter answers --- scw_js/llm_service.ts | 11 ++++++++++- website/components/AssistantChat.tsx | 10 +++++++++- website/layouts/styles.ts | 8 +++++++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/scw_js/llm_service.ts b/scw_js/llm_service.ts index df69166ee..2d045b115 100644 --- a/scw_js/llm_service.ts +++ b/scw_js/llm_service.ts @@ -111,7 +111,16 @@ export async function callLLMAPI( choices: [ { index: 0, - message: { role: "assistant", content: "I am a placeholder for the LLM response" }, + message: { + role: "assistant", + content: + "**Placeholder response**\n\n" + + "This is a *mock* answer used for local/testnet testing. It includes:\n\n" + + "- a list item\n" + + "- some `inline code`\n" + + "- a [link](https://example.com)\n\n" + + "```js\nconsole.log('mock code block');\n```", + }, finish_reason: "stop", }, ], diff --git a/website/components/AssistantChat.tsx b/website/components/AssistantChat.tsx index 822238ce9..37a7cae5c 100644 --- a/website/components/AssistantChat.tsx +++ b/website/components/AssistantChat.tsx @@ -6,6 +6,8 @@ */ import React, { useState, useMemo, useEffect } from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; import { AgentInfoPanel } from "./AgentInfoPanel"; import { AgentSelector } from "./AgentSelector"; import * as styles from "../layouts/styles"; @@ -291,7 +293,13 @@ export function AssistantChat() { }`} >
{message.role === "user" ? youLabel : assistantLabel}
-
{message.content}
+
+ {message.role === "assistant" ? ( + {message.content} + ) : ( +
{message.content}
+ )} +
)) diff --git a/website/layouts/styles.ts b/website/layouts/styles.ts index 0390d6c89..f28095cee 100644 --- a/website/layouts/styles.ts +++ b/website/layouts/styles.ts @@ -2593,10 +2593,16 @@ export const messageRole = css({ }); export const messageContent = css({ - whiteSpace: "pre-wrap", lineHeight: "1.5", }); +// Plain-text messages (user input) preserve literal newlines/spacing. +// Markdown-rendered messages (assistant output) skip this — Markdown's own +// block spacing would otherwise double up with pre-wrap. +export const messageContentPlain = css({ + whiteSpace: "pre-wrap", +}); + // Loading message export const loadingMessage = css({ margin: "md 0", From 6c89183f4f7e3f5af5a23ed1f8c2de8b8afa7011 Mon Sep 17 00:00:00 2001 From: fretchen Date: Thu, 30 Jul 2026 21:54:54 +0200 Subject: [PATCH 2/4] modernized syntax highlighting --- website/components/CodeBlock.tsx | 10 ++++- website/components/MdxCodeBlock.tsx | 45 +++++++++++++++++++++++ website/components/Post.tsx | 6 ++- website/pages/x402/+Page.tsx | 41 +++++++-------------- website/test/MdxCodeBlock.test.tsx | 57 +++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+), 31 deletions(-) create mode 100644 website/components/MdxCodeBlock.tsx create mode 100644 website/test/MdxCodeBlock.test.tsx diff --git a/website/components/CodeBlock.tsx b/website/components/CodeBlock.tsx index 2ae60c496..2742c3810 100644 --- a/website/components/CodeBlock.tsx +++ b/website/components/CodeBlock.tsx @@ -1,7 +1,7 @@ /** * CodeBlock — the shared docs code block: syntax-highlighted, copyable. * - * Highlighting runs through highlight.js's *core* build with only the three languages we + * Highlighting runs through highlight.js's *core* build with only the languages we * actually use registered. The full `highlight.js` bundle registers ~190 grammars and would * push a client chunk past the 700 kB ceiling enforced by utils/checkChunkSizes.ts — never * import the default entry point here. @@ -18,15 +18,21 @@ import hljs from "highlight.js/lib/core"; import typescript from "highlight.js/lib/languages/typescript"; import json from "highlight.js/lib/languages/json"; import bash from "highlight.js/lib/languages/bash"; +import javascript from "highlight.js/lib/languages/javascript"; +import python from "highlight.js/lib/languages/python"; +import yaml from "highlight.js/lib/languages/yaml"; import "highlight.js/styles/vs2015.css"; import { css } from "../styled-system/css"; hljs.registerLanguage("typescript", typescript); hljs.registerLanguage("json", json); hljs.registerLanguage("bash", bash); +hljs.registerLanguage("javascript", javascript); +hljs.registerLanguage("python", python); +hljs.registerLanguage("yaml", yaml); /** `plaintext` opts out of highlighting — use it for terminal transcripts and annotated output. */ -export type CodeLang = "typescript" | "json" | "bash" | "plaintext"; +export type CodeLang = "typescript" | "json" | "bash" | "javascript" | "python" | "yaml" | "plaintext"; const wrapper = css({ position: "relative", mt: "1", mb: "2" }); diff --git a/website/components/MdxCodeBlock.tsx b/website/components/MdxCodeBlock.tsx new file mode 100644 index 000000000..b5d5e6520 --- /dev/null +++ b/website/components/MdxCodeBlock.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { CodeBlock, CodeLang } from "./CodeBlock"; + +/** Fence languages that don't map 1:1 onto a registered hljs grammar. */ +const LANG_ALIASES: Record = { + shell: "bash", + sh: "bash", +}; + +function resolveLang(className?: string): CodeLang { + const match = /language-(\w+)/.exec(className ?? ""); + const raw = match?.[1]; + if (!raw) return "plaintext"; + const aliased = LANG_ALIASES[raw] ?? raw; + switch (aliased) { + case "typescript": + case "json": + case "bash": + case "javascript": + case "python": + case "yaml": + return aliased; + default: + return "plaintext"; + } +} + +/** + * MDX renders fenced code blocks as `
...
`. + * This adapter, passed to `MDXProvider` as the `pre` override, redirects that markup through + * CodeBlock for highlighting + copy support. Falls back to a plain `
` for any `pre` that
+ * doesn't have the expected single `` child (defensive — MDX always produces this shape
+ * for fenced blocks, but `pre` can in principle appear standalone in hand-written MDX/HTML).
+ */
+export function MdxPre({ children, ...rest }: React.HTMLAttributes) {
+  const child = React.isValidElement(children) ? children : null;
+  const childProps = child?.props as { className?: string; children?: React.ReactNode } | undefined;
+  const code = childProps?.children;
+
+  if (!child || typeof code !== "string") {
+    return 
{children}
; + } + + return {code.replace(/\n$/, "")}; +} diff --git a/website/components/Post.tsx b/website/components/Post.tsx index bdc61fcf0..d99302c95 100644 --- a/website/components/Post.tsx +++ b/website/components/Post.tsx @@ -3,6 +3,7 @@ import { PostProps } from "../types/components"; import MetadataLine from "./MetadataLine"; import { Link } from "./Link"; import { NFTFloatImage } from "./NFTFloatImage"; +import { MdxPre } from "./MdxCodeBlock"; import { post, titleBar } from "../layouts/styles"; import { loadLazyModuleFromDirectory } from "../utils/lazyGlobRegistry"; import { isSupportedDirectory, getSupportedDirectories } from "../utils/supportedDirectories"; @@ -22,7 +23,8 @@ const ReactPostRenderer: React.FC<{ contentRef: React.RefObject; onReady?: () => void; }> = ({ componentPath, tokenID, contentRef, onReady }) => { - const [Component, setComponent] = React.useState(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- MDX components accept an extra `components` override prop; plain TSX posts ignore it. + const [Component, setComponent] = React.useState | null>(null); const [error, setError] = React.useState(null); const [loading, setLoading] = React.useState(true); @@ -139,7 +141,7 @@ const ReactPostRenderer: React.FC<{ return (
{tokenID && } - +
); }; diff --git a/website/pages/x402/+Page.tsx b/website/pages/x402/+Page.tsx index 4f9cd028c..4aed09555 100644 --- a/website/pages/x402/+Page.tsx +++ b/website/pages/x402/+Page.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react"; import { css } from "../../styled-system/css"; import MermaidDiagram from "../../components/MermaidDiagram"; import { FacilitatorApproval } from "../../components/FacilitatorApproval"; +import { CodeBlock } from "../../components/CodeBlock"; import { titleBar } from "../../layouts/styles"; import * as styles from "../../layouts/styles"; import { Link } from "../../components/Link"; @@ -327,8 +328,7 @@ export default function Page() { Replace 0xYourMerchantAddress with your wallet address and set amount to your price in USDC (6 decimals — 100000 = $0.10).

-
-            {`// HTTP 402 response body:
+          {`// HTTP 402 response body:
 {
   "x402Version": 2,
   "accepts": [{
@@ -341,8 +341,7 @@ export default function Page() {
     "extra": { "name": "USD Coin", "version": "2" }
   }],
   "facilitatorUrl": "https://facilitator.fretchen.eu"
-}`}
-          
+}`}
@@ -364,8 +363,7 @@ export default function Page() { When a client sends a request with a PAYMENT-SIGNATURE header, verify the payment before delivering the resource, then settle it on-chain:

-
-            {`// 1. Verify payment (before delivering resource)
+          {`// 1. Verify payment (before delivering resource)
 const verifyRes = await fetch("https://facilitator.fretchen.eu/verify", {
   method: "POST",
   headers: { "Content-Type": "application/json" },
@@ -386,8 +384,7 @@ await fetch("https://facilitator.fretchen.eu/settle", {
     network: "eip155:10", payload, details })
 });
 
-return new Response(JSON.stringify(result), { status: 200 });`}
-          
+return new Response(JSON.stringify(result), { status: 200 });`}

That's it — your service now accepts crypto payments. See the{" "} agent onboarding guide for a complete walkthrough. @@ -500,8 +497,7 @@ return new Response(JSON.stringify(result), { status: 200 });`} Validates a signed payment off-chain. Checks signature validity, sufficient balance, correct recipient, and expiration. Call this before delivering your resource.

-
-            {`curl -X POST https://facilitator.fretchen.eu/verify \\
+          {`curl -X POST https://facilitator.fretchen.eu/verify \\
   -H "Content-Type: application/json" \\
   -d '{
     "x402Version": 2,
@@ -515,8 +511,7 @@ return new Response(JSON.stringify(result), { status: 200 });`}
       "asset": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
       "payTo": "0xYourMerchantAddress"
     }
-  }'`}
-          
+ }'`}

Response: {`{ "valid": true }`} or {`{ "valid": false, "invalidReason": "..." }`}

@@ -528,8 +523,7 @@ return new Response(JSON.stringify(result), { status: 200 });`} Executes the payment on-chain via EIP-3009 transferWithAuthorization. Call this{" "} after successful verification and resource delivery.

-
-            {`curl -X POST https://facilitator.fretchen.eu/settle \\
+          {`curl -X POST https://facilitator.fretchen.eu/settle \\
   -H "Content-Type: application/json" \\
   -d '{
     "x402Version": 2,
@@ -543,8 +537,7 @@ return new Response(JSON.stringify(result), { status: 200 });`}
       "asset": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
       "payTo": "0xYourMerchantAddress"
     }
-  }'`}
-          
+ }'`}

Response: {`{ "success": true, "txHash": "0x..." }`}

@@ -553,9 +546,7 @@ return new Response(JSON.stringify(result), { status: 200 });`}

GET /supported

Returns supported networks, payment schemes, and fee configuration.

-
-            {`curl https://facilitator.fretchen.eu/supported`}
-          
+ {`curl https://facilitator.fretchen.eu/supported`}

Returns a JSON object with kinds (supported network/scheme pairs), extensions{" "} (advertised extension keys), signers (facilitator addresses per network), and{" "} @@ -579,8 +570,7 @@ return new Response(JSON.stringify(result), { status: 200 });`}

Using the official @x402/fetch SDK, a client can pay for any x402 resource automatically:

-
-          {`import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
+        {`import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
 import { registerExactEvmScheme } from "@x402/evm/exact/client";
 import { privateKeyToAccount } from "viem/accounts";
 
@@ -602,13 +592,11 @@ const response = await fetchWithPayment(
 
 const result = await response.json();
 console.log("Image:", result.imageUrl);
-console.log("NFT:", result.tokenId);`}
-        
+console.log("NFT:", result.tokenId);`}

Your server (resource server)

Full example of a Node.js endpoint protected by x402. Adapt the resource generation to your use case:

-
-          {`// Express / Node.js example
+        {`// Express / Node.js example
 app.post("/api/resource", async (req, res) => {
   const paymentHeader = req.headers["payment-signature"];
 
@@ -658,8 +646,7 @@ app.post("/api/resource", async (req, res) => {
   });
 
   return res.json(result);
-});`}
-        
+});`} {/* ── 7. Supported networks ────────────────────────────────────── */} diff --git a/website/test/MdxCodeBlock.test.tsx b/website/test/MdxCodeBlock.test.tsx new file mode 100644 index 000000000..9de0895d4 --- /dev/null +++ b/website/test/MdxCodeBlock.test.tsx @@ -0,0 +1,57 @@ +/** + * MdxPre tests. + * + * MDX (compiled without `providerImportSource`, as this repo's vite.config.ts does) reads + * component overrides from a `components` *prop* passed to the compiled component, not from + * React context/MDXProvider — verified against @mdx-js/mdx's actual compile output. Post.tsx + * passes `components={{ pre: MdxPre }}` directly to the loaded MDX component for that reason. + * These tests render MdxPre the same way MDX itself would call it: as the `pre` element with a + * single `` child. + */ +import React from "react"; +import { describe, it, expect } from "vitest"; +import { render } from "@testing-library/react"; +import { MdxPre } from "../components/MdxCodeBlock"; + +const PY = `def add(a, b):\n return a + b`; + +function renderFence(lang: string, code: string) { + return render( + + {code} + , + ); +} + +describe("MdxPre", () => { + it("routes a python fence through CodeBlock with highlighting", () => { + const { container } = renderFence("python", PY); + + expect(container.querySelectorAll("[class^='hljs-']").length).toBeGreaterThan(0); + expect(container.querySelector("code")?.textContent).toBe(PY); + }); + + it("maps the shell alias onto the bash grammar", () => { + const { container } = renderFence("shell", "echo hello"); + + expect(container.querySelectorAll("[class^='hljs-']").length).toBeGreaterThan(0); + }); + + it("falls back to plaintext for an unregistered fence language", () => { + const { container } = renderFence("solidity", "contract Foo {}"); + + expect(container.querySelectorAll("[class^='hljs-']").length).toBe(0); + expect(container.querySelector("code")?.textContent).toBe("contract Foo {}"); + }); + + it("leaves a bare
 (no single string-child ) untouched", () => {
+    const { container } = render(
+      
+        not a code fence
+      ,
+    );
+
+    expect(container.querySelector("pre")).not.toBeNull();
+    expect(container.querySelector("[class^='hljs-']")).toBeNull();
+  });
+});

From 57c16c97edcf759bf63bcbc8519c58319047c680 Mon Sep 17 00:00:00 2001
From: fretchen 
Date: Thu, 30 Jul 2026 22:10:09 +0200
Subject: [PATCH 3/4] Cleaner imports

---
 website/components/MdxCodeBlock.tsx           | 23 +++++++++---
 website/package-lock.json                     | 37 +++++++++++++++++++
 website/package.json                          |  1 +
 .../quantum/hardware/quantum_hardware_102.mdx |  2 +-
 website/test/MdxCodeBlock.test.tsx            | 26 +++++++++++++
 website/vite.config.ts                        |  5 +++
 6 files changed, 88 insertions(+), 6 deletions(-)

diff --git a/website/components/MdxCodeBlock.tsx b/website/components/MdxCodeBlock.tsx
index b5d5e6520..db176c778 100644
--- a/website/components/MdxCodeBlock.tsx
+++ b/website/components/MdxCodeBlock.tsx
@@ -7,6 +7,17 @@ const LANG_ALIASES: Record = {
   sh: "bash",
 };
 
+/**
+ * `remark-math` (configured in vite.config.ts) also renders through `
`
+ * — the same shape as a fenced code block, but it's not one. `useKaTeXRenderer.ts` finds these via
+ * `querySelectorAll("code.language-math")` and replaces them with real KaTeX markup client-side.
+ * MdxPre must leave this class untouched, or that selector stops matching and math silently stops
+ * rendering (this exact regression happened once already — hence the dedicated check and test).
+ */
+function isMathFence(className?: string): boolean {
+  return /(^|\s)language-math(\s|$)/.test(className ?? "");
+}
+
 function resolveLang(className?: string): CodeLang {
   const match = /language-(\w+)/.exec(className ?? "");
   const raw = match?.[1];
@@ -27,17 +38,19 @@ function resolveLang(className?: string): CodeLang {
 
 /**
  * MDX renders fenced code blocks as `
...
`. - * This adapter, passed to `MDXProvider` as the `pre` override, redirects that markup through - * CodeBlock for highlighting + copy support. Falls back to a plain `
` for any `pre` that
- * doesn't have the expected single `` child (defensive — MDX always produces this shape
- * for fenced blocks, but `pre` can in principle appear standalone in hand-written MDX/HTML).
+ * This adapter, passed to the compiled MDX component as its `components.pre` prop (see Post.tsx —
+ * MDX here is compiled without `providerImportSource`, so it reads overrides from `props.components`,
+ * not from `MDXProvider` context), redirects that markup through CodeBlock for highlighting + copy
+ * support. Falls back to a plain `
` for a `language-math` fence (see isMathFence above) or for
+ * any `pre` that doesn't have the expected single `` child (defensive — MDX always produces
+ * this shape for fenced blocks, but `pre` can in principle appear standalone in hand-written MDX/HTML).
  */
 export function MdxPre({ children, ...rest }: React.HTMLAttributes) {
   const child = React.isValidElement(children) ? children : null;
   const childProps = child?.props as { className?: string; children?: React.ReactNode } | undefined;
   const code = childProps?.children;
 
-  if (!child || typeof code !== "string") {
+  if (!child || typeof code !== "string" || isMathFence(childProps?.className)) {
     return 
{children}
; } diff --git a/website/package-lock.json b/website/package-lock.json index 0ae948ba5..8c1ef649f 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -24,6 +24,7 @@ "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.8", "react-markdown": "^10.1.0", + "rehype-mdx-import-media": "^1.4.0", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", @@ -10468,6 +10469,23 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-properties-to-mdx-jsx-attributes": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/hast-util-properties-to-mdx-jsx-attributes/-/hast-util-properties-to-mdx-jsx-attributes-1.1.1.tgz", + "integrity": "sha512-MMrAoGgvhYULEqMB/r6AlcVz1D3Cyml/9cMB2NIqZsIsEJ+XEXPMqH0gjba8dVs9AnQUYvPReAS+OIYx4ip+Ug==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "estree-util-value-to-estree": "^3.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "property-information": "^7.0.0", + "style-to-js": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, "node_modules/hast-util-to-estree": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", @@ -13796,6 +13814,12 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "license": "MIT" + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -14557,6 +14581,19 @@ "node": ">=8" } }, + "node_modules/rehype-mdx-import-media": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/rehype-mdx-import-media/-/rehype-mdx-import-media-1.4.0.tgz", + "integrity": "sha512-O2Q/yFj8lj0IXK7iwfHFowYrn0c5wA4/RCoS8J30HVioMmMzr+fF82PbxV3YPf6IpmbaRrVPvRfae1LhM8hXmQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-properties-to-mdx-jsx-attributes": "^1.0.0", + "parse-srcset": "^1.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + } + }, "node_modules/rehype-recma": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", diff --git a/website/package.json b/website/package.json index 2784b00a7..d1597819a 100644 --- a/website/package.json +++ b/website/package.json @@ -34,6 +34,7 @@ "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.8", "react-markdown": "^10.1.0", + "rehype-mdx-import-media": "^1.4.0", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", diff --git a/website/quantum/hardware/quantum_hardware_102.mdx b/website/quantum/hardware/quantum_hardware_102.mdx index 43b989e57..6d188c112 100644 --- a/website/quantum/hardware/quantum_hardware_102.mdx +++ b/website/quantum/hardware/quantum_hardware_102.mdx @@ -144,7 +144,7 @@ ax2.set_ylabel('velocity $v$') Text(0, 0.5, 'velocity $v$') -![png](qquantum_hardware_102_8_1.png) +![png](quantum_hardware_102_8_1.png) quite importantly we can only test one set of initial conditions for each trajectory. The particle cannot be at several positions at the same time. With quantum mechanics, we can now look into a system where this restriction gets lifted. diff --git a/website/test/MdxCodeBlock.test.tsx b/website/test/MdxCodeBlock.test.tsx index 9de0895d4..f6869bf2d 100644 --- a/website/test/MdxCodeBlock.test.tsx +++ b/website/test/MdxCodeBlock.test.tsx @@ -12,6 +12,7 @@ import React from "react"; import { describe, it, expect } from "vitest"; import { render } from "@testing-library/react"; import { MdxPre } from "../components/MdxCodeBlock"; +import Lecture5 from "../quantum/amo/lecture5.mdx"; const PY = `def add(a, b):\n return a + b`; @@ -54,4 +55,29 @@ describe("MdxPre", () => { expect(container.querySelector("pre")).not.toBeNull(); expect(container.querySelector("[class^='hljs-']")).toBeNull(); }); + + it("leaves remark-math's language-math fence untouched (regression: useKaTeXRenderer depends on this class)", () => { + const latex = "\\hat{H} = \\frac{p^2}{2m}"; + const { container } = render( + + {latex} + , + ); + + const code = container.querySelector("code.language-math"); + expect(code).not.toBeNull(); + expect(code?.className).toBe("language-math math-display"); + expect(code?.textContent).toBe(latex); + expect(container.querySelector("[class^='hljs-']")).toBeNull(); + }); + + it("preserves code.language-math.math-display when a real lecture is rendered exactly as Post.tsx renders it (regression test)", () => { + const { container } = render(); + + // Inline math ($...$) renders as a bare that never + // goes through
/MdxPre at all, so it can't detect this regression. Display math ($$...$$)
+    // is the one that goes through 
 — assert on .math-display specifically, the same selector
+    // useKaTeXRenderer.ts relies on to find and replace block math. Lecture5 has both kinds.
+    expect(container.querySelectorAll("code.language-math.math-display").length).toBeGreaterThan(0);
+  });
 });
diff --git a/website/vite.config.ts b/website/vite.config.ts
index f8d9e0f14..df88eac19 100644
--- a/website/vite.config.ts
+++ b/website/vite.config.ts
@@ -6,6 +6,7 @@ import remarkFrontmatter from "remark-frontmatter";
 import remarkMdxFrontmatter from "remark-mdx-frontmatter";
 import remarkGfm from "remark-gfm";
 import remarkMath from "remark-math";
+import rehypeMdxImportMedia from "rehype-mdx-import-media";
 
 export default defineConfig({
   plugins: [
@@ -20,6 +21,10 @@ export default defineConfig({
         remarkGfm,
         remarkMath, // Protects $$...$$ from Markdown transformations
       ],
+      // Rewrites bare `` (from notebook-style `![alt](./x.png)` markdown)
+      // into an import, so Vite's asset pipeline hashes/copies the file instead of leaving a
+      // relative string that 404s once the page is served from a different URL.
+      rehypePlugins: [rehypeMdxImportMedia],
     }),
     react({}),
   ],

From 17b6ed254e1c298e209165a7283e98bdab55349b Mon Sep 17 00:00:00 2001
From: fretchen 
Date: Thu, 30 Jul 2026 22:16:10 +0200
Subject: [PATCH 4/4] Update Post.tsx

---
 website/components/Post.tsx | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/website/components/Post.tsx b/website/components/Post.tsx
index d99302c95..11a89b528 100644
--- a/website/components/Post.tsx
+++ b/website/components/Post.tsx
@@ -16,6 +16,10 @@ import { TableOfContents } from "./TableOfContents";
 import { Webmentions } from "./Webmentions";
 import { CommentsSection } from "./CommentsSection";
 
+// MDX components accept an extra `components` override prop (used to route `pre` through
+// MdxPre); plain TSX posts simply ignore it.
+type PostComponent = React.ComponentType<{ components?: Record }>;
+
 // Dynamic React component renderer
 const ReactPostRenderer: React.FC<{
   componentPath: string;
@@ -23,8 +27,7 @@ const ReactPostRenderer: React.FC<{
   contentRef: React.RefObject;
   onReady?: () => void;
 }> = ({ componentPath, tokenID, contentRef, onReady }) => {
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- MDX components accept an extra `components` override prop; plain TSX posts ignore it.
-  const [Component, setComponent] = React.useState | null>(null);
+  const [Component, setComponent] = React.useState(null);
   const [error, setError] = React.useState(null);
   const [loading, setLoading] = React.useState(true);