Skip to content

Commit f9e6458

Browse files
committed
feat(portable-text): support interactive SVG/JS htmlBlocks in Astro draft preview and Sanity Studio
1 parent eeaee07 commit f9e6458

2 files changed

Lines changed: 38 additions & 15 deletions

File tree

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,37 @@
1-
import React from "react";
2-
// TODO: Add stronger typing
3-
const HTMLPreview = (props: any) => {
1+
import React, { useEffect, useRef } from "react";
2+
3+
const STEGA_REGEX = /[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]|\uDB40[\uDC20-\uDC7F]/g;
4+
5+
interface HTMLPreviewProps {
6+
html?: string;
7+
}
8+
9+
const HTMLPreview = (props: HTMLPreviewProps) => {
410
const { html } = props;
5-
if (!html) {
11+
const containerRef = useRef<HTMLDivElement>(null);
12+
const cleanHtml = typeof html === "string" ? html.replace(STEGA_REGEX, "") : "";
13+
14+
useEffect(() => {
15+
const container = containerRef.current;
16+
if (!container || !cleanHtml) return;
17+
18+
// Re-execute any inline <script> tags so interactive SVG/HTML widgets work inside Sanity Studio preview
19+
const scripts = Array.from(container.querySelectorAll("script"));
20+
for (const oldScript of scripts) {
21+
const newScript = document.createElement("script");
22+
for (const attr of Array.from(oldScript.attributes)) {
23+
newScript.setAttribute(attr.name, attr.value);
24+
}
25+
newScript.text = oldScript.textContent || "";
26+
oldScript.parentNode?.replaceChild(newScript, oldScript);
27+
}
28+
}, [cleanHtml]);
29+
30+
if (!cleanHtml) {
631
return <div>Add some HTML</div>;
732
}
8-
return <div dangerouslySetInnerHTML={{ __html: html }} />;
33+
return <div ref={containerRef} dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
934
};
1035

11-
const preview = (props: any) => <HTMLPreview {...props} />;
12-
36+
const preview = (props: HTMLPreviewProps) => <HTMLPreview {...props} />;
1337
export default preview;

‎apps/site/src/components/portable-text/PtHtml.astro‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,16 @@ type HtmlNode = { _type: "htmlBlock"; html?: string };
66
type Props = PtProps<HtmlNode>;
77
88
const { node } = Astro.props;
9-
10-
/**
11-
* Raw HTML authored in the Studio, rendered unescaped by design — this block
12-
* exists so editors can embed markup the other block types do not cover.
9+
/*
10+
* Author-written HTML block from Sanity Studio.
1311
* It is trusted because only authenticated Studio users can write it.
1412
*
15-
* Not stegaClean'd: cleaning would strip characters from the author's markup.
16-
* Stega is only ever enabled on preview responses, which are `noindex` and
17-
* `no-store`, so encoded characters cannot reach a published page.
13+
* Strip zero-width Stega watermark characters (when previewing drafts) so inline
14+
* <script>, <style>, and <svg> blocks execute cleanly without syntax errors,
15+
* while preserving 100% of visible author markup.
1816
*/
19-
const html = node.html;
17+
const STEGA_REGEX = /[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]|\uDB40[\uDC20-\uDC7F]/g;
18+
const html = typeof node.html === "string" ? node.html.replace(STEGA_REGEX, "") : undefined;
2019
---
2120

2221
{html && <div class="not-prose my-6" set:html={html} />}

0 commit comments

Comments
 (0)