From efd3db03ce2bf065d6b89ee38e765cfe7985862d Mon Sep 17 00:00:00 2001
From: fretchen
`.
+ * 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{children};
+ }
+
+ return 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"
-}`}
-
+}`}
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": "..." }`}
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..." }`}
Returns supported networks, payment schemes, and fee configuration.
-
- {`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);`}
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$')
-
+
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 `` 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);