I think we broke it - try visiting https://tools.simonwillison.net/markdown-svg-renderer#url=https%3A%2F%2Fgist.github.com%2Fsimonw%2Ffea4f7546626d627862dc241a4e3a86a in your browser
8 previous messages
I’ll open the live URL in the in-app browser and see exactly what state it lands in. If the break is from the hash no longer auto-loading, I’ll patch the local file so shared
#url=links can still produce the clean viewer experience without re-opening the XSS hole.MCP tool call
node_repl.js
{ "title": "Open live markdown renderer", "timeout_ms": 60000, "code": "if (!globalThis.agent) {\n const { setupBrowserRuntime } = await import(\"/Users/simon/.codex/plugins/cache/openai-bundled/browser/26.519.81530/scripts/browser-client.mjs\");\n await setupBrowserRuntime({ globals: globalThis });\n}\nif (!globalThis.browser) {\n globalThis.browser = await agent.browsers.get(\"iab\");\n}\nawait browser.nameSession(\"🔎 Live markdown renderer\");\nvar liveRendererTab = await browser.tabs.new();\nawait liveRendererTab.goto(\"https://tools.simonwillison.net/markdown-svg-renderer#url=https%3A%2F%2Fgist.github.com%2Fsimonw%2Ffea4f7546626d627862dc241a4e3a86a\");\nawait liveRendererTab.playwright.waitForLoadState({ state: \"load\", timeoutMs: 30000 });\nawait liveRendererTab.playwright.waitForTimeout(2000);\nconsole.log(await liveRendererTab.url());\nconsole.log((await liveRendererTab.playwright.domSnapshot()).slice(0, 3000));" }https://tools.simonwillison.net/markdown-svg-renderer#url=https%3A%2F%2Fgist.github.com%2Fsimonw%2Ffea4f7546626d627862dc241a4e3a86a - button "Paste" - button "URL" - textbox - generic: Preview - text: "`; } class SvgBlock extends HTMLElement { connectedCallback() { if (this.shadowRoot) return; const code = this.getAttribute(\"data-svg\") || \"\"; const shadow = this.attachShadow({ mode: \"open\" }); shadow.innerHTML = `" - button "Rendered" - button "Code" - iframe - text: "`; shadow.querySelector(\".svg-wrap\").style.aspectRatio = svgAspectRatio(code); shadow.querySelector(\"iframe\").srcdoc = svgPreviewDocument(code); shadow.querySelector(\"pre\").textContent = code; const buttons = shadow.querySelectorAll(\"button\"); const panels = shadow.querySelectorAll(\".panel\"); buttons.forEach((btn) => { btn.addEventListener(\"click\", () => { const tab = btn.dataset.tab; buttons.forEach((b) => b.classList.toggle(\"active\", b === btn)); panels.forEach((p) => p.classList.toggle(\"active\", p.dataset.panel === tab) ); }); }); } } customElements.define(\"svg-block\", SvgBlock); // ---- Markdown rendering ---- const markdownSanitizeConfig = { USE_PROFILES: { html: true }, ADD_ATTR: [\"data-svg\"], FORBID_ATTR: [\"style\"], FORBID_TAGS: [\"style\"] }; const svgSanitizeConfig = { USE_PROFILES: { svg: true }, FORBID_ATTR: [\"href\", \"xlink:href\", \"src\", \"style\"], FORBID_TAGS: [ \"animate\", \"animateMotion\", \"animatemotion\", \"animateTransform\", \"animatetransform\", \"audio\", \"embed\", \"foreignObject\", \"foreignobject\", \"iframe\", \"image\", \"link\", \"object\", \"script\", \"set\", \"style\", \"use\", \"video\" ] }; const md = window.markdownit({ html: false, linkify: false, typographer: false }); function escapeAttr(s) { return s .replace(/&/g, \"&\") .replace(//g, \">\") .replace(/\"/g, \"\"\"); } const defaultFenceRenderer = md.renderer.rules.fence; md.renderer.rules.fence = (tokens, idx, options, env, self) => { const token = tokens[idx]; const lang = (token.info || \"\").trim().split(/\\s+/)[0].toLowerCase(); if (lang === \"svg\") { const svg = window.DOMPurify.sanitize(token.content, svgSanitizeConfig).trim(); if (svg) { return `" - text: "\\n`; } } return defaultFenceRenderer(tokens, idx, options, env, self); }; function hydrateSvgBlocks(root) { root.querySelectorAll(\".svg-block-placeholder[data-svg]\").forEach((placeholder) => { const block = document.createElement(\"svg-block\"); block.setAttribute(\"data-svg\", placeholder.getAttribute(\"data-svg\") || \"\"); placeholder.replaceWith(block); }); } function renderMarkdown(src) { const html = md.render(src); return window.DOMPurify.sanitize(html, markdownSanitizeConfig); } const app = document.querySelector(\".app\"); const input = document.getElementById(\"input\"); const output = document.getElementById(\"output\"); const tabPaste = document.getElementById(\"tab-paste\"); const tabUrl = document.getElementById(\"tab-url\"); const urlBar = document.getElementById(\"url-bar\"); const urlInput = document.getElementById(\"url-input\"); const urlLoad = document.getElementById(\"url-load\"); const urlStatus = documThe live page is definitely broken: it’s rendering a chunk of the JavaScript source into the preview, which usually means the browser’s HTML parser hit something inside the inline module that prematurely ended or corrupted the script. I’m going to inspect the exact script/source and console errors now, then patch the local file.
Explored 1 search, ran 1 command
Explored 1 search
- Searched for
</script|<script|scriptinmarkdown-svg-renderer.html
- Ran
curl -fsSL https://tools.simonwillison.net/markdown-svg-renderer | rg -n "</script|<script|script|svgPreviewDocument|FORBID_TAGS"Found it: the deployed page has a footer script injected right into the
svgPreviewDocument()template because the inline JS contained a literal</body>. That injected</script>then terminates the module early. I’ll remove that literal closing-body sequence from the source while still generating it at runtime.Edited 1 file, explored 1 search, ran 2 commands, called 1 tool
- Wrote
./markdown-svg-renderer.html(+2 -2)Explored 1 search
- Searched for
</body|</html|footer.js|svgPreviewDocumentinmarkdown-svg-renderer.html
Ran
git diff -- markdown-svg-renderer.htmlRan
git diff --check -- markdown-svg-renderer.htmlMCP tool call
node_repl.js
{ "title": "Verify local patched renderer", "timeout_ms": 60000, "code": "var patchedLocalTab = await browser.tabs.new();\nawait patchedLocalTab.goto(\"http://127.0.0.1:8765/markdown-svg-renderer.html?patched-body-close=1\");\nawait patchedLocalTab.playwright.waitForLoadState({ state: \"load\", timeoutMs: 20000 });\nawait patchedLocalTab.playwright.waitForTimeout(1000);\nvar patchedCheck = await patchedLocalTab.playwright.evaluate(() => ({\n heading: document.querySelector(\"#output h1\")?.textContent || null,\n renderedCodeLeak: /class SvgBlock|svgPreviewDocument|Markdown rendering/.test(document.querySelector(\"#output\")?.textContent || \"\"),\n svgBlocks: document.querySelectorAll(\"#output svg-block\").length,\n errorsText: document.querySelector(\"#url-status\")?.textContent || \"\"\n}));\nvar patchedLogs = await patchedLocalTab.dev.logs({ levels: [\"error\"], limit: 20 });\nconsole.log(JSON.stringify({ patchedCheck, patchedLogs }, null, 2));" }{ "patchedCheck": { "errorsText": "", "heading": "Markdown renderer", "renderedCodeLeak": false, "svgBlocks": 1 }, "patchedLogs": [] }There’s a second regression hiding behind that deployed-script break: shared
#url=links should still auto-load now that the renderer is sanitized. I’m restoring that behavior so a provided URL loads into the clean viewer mode instead of stopping at the URL bar.Edited 1 file, explored 1 search, ran 5 commands, called 1 tool
Wrote
./markdown-svg-renderer.html(+2 -2)Ran
git diff --check -- markdown-svg-renderer.htmlExplored 1 search
- Searched for
restoreUrlFromHash|urlFromHash|loadFromUrl\(startupUrl|</body|</htmlinmarkdown-svg-renderer.html
Ran
git diff -- markdown-svg-renderer.htmlWrote
./markdown-svg-renderer.html(+0 -7)MCP tool call
node_repl.js
{ "title": "Verify hash auto-load", "timeout_ms": 60000, "code": "var hashAutoloadTab = await browser.tabs.new();\nawait hashAutoloadTab.goto(\"http://127.0.0.1:8765/markdown-svg-renderer.html?autoload=1#url=https%3A%2F%2Fgist.github.com%2Fsimonw%2Ffea4f7546626d627862dc241a4e3a86a\");\nawait hashAutoloadTab.playwright.waitForLoadState({ state: \"load\", timeoutMs: 20000 });\nawait hashAutoloadTab.playwright.waitForTimeout(3500);\nvar hashAutoloadCheck = await hashAutoloadTab.playwright.evaluate(() => ({\n viewerMode: document.querySelector(\".app\").classList.contains(\"viewer-mode\"),\n hasUrlContent: document.querySelector(\".app\").classList.contains(\"has-url-content\"),\n editorDisplay: getComputedStyle(document.querySelector(\".editor-pane\")).display,\n toggleDisplay: getComputedStyle(document.querySelector(\"#source-toggle\")).display,\n status: document.querySelector(\"#url-status\").textContent,\n outputText: (document.querySelector(\"#output\")?.textContent || \"\").slice(0, 500),\n codeLeak: /class SvgBlock|svgPreviewDocument|Markdown rendering/.test(document.querySelector(\"#output\")?.textContent || \"\")\n}));\nvar hashAutoloadLogs = await hashAutoloadTab.dev.logs({ levels: [\"error\"], limit: 20 });\nconsole.log(JSON.stringify({ hashAutoloadCheck, hashAutoloadLogs }, null, 2));" }{ "hashAutoloadCheck": { "codeLeak": false, "editorDisplay": "none", "hasUrlContent": true, "outputText": "2026-05-28T19:23:29 conversation: 01ksr0nxcb2e6fdtte2dq42dfq id: 01ksr0ncsbqhqkbj6zaw20v3a3\nModel: anthropic/claude-opus-4-8\nPrompt\nGenerate an SVG of a pelican riding a bicycle\nOptions\n\nthinking: True\nthinking_effort: low\n\nResponse\nI'll create an SVG of a pelican riding a bicycle for you!\n\nI've created a cheerful pelican riding a bicycle! The illustration features:\n\n🚲 A bicycle with two wheels, frame, pedals, and handlebars\n🦤 A pelican with its characteristic large orange beak and pouch\n🦵", "status": "", "toggleDisplay": "flex", "viewerMode": true }, "hashAutoloadLogs": [] }
Ran
git diff --check -- markdown-svg-renderer.htmlRan
git status --short -- markdown-svg-renderer.htmlRan
git diff --stat -- markdown-svg-renderer.html && git diff -- markdown-svg-renderer.html
Yep, confirmed. The live URL is broken right now: the page is rendering part of its own JavaScript into the preview.
Root cause: the deploy step injected footer.js into a literal </body> inside svgPreviewDocument(), which introduced a </script> inside the inline module and cut the script in half.
I patched markdown-svg-renderer.html to generate the closing tags without a literal </body> in the source, and restored shared #url= links so they auto-load again and enter viewer mode.
Verified locally with the same gist URL: it loads, hides the source pane, shows the toggle, no code leak, no console errors. The live site will need redeploying to pick this up.