Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions apps/sanity/components/HTMLPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
import React from "react";
// TODO: Add stronger typing
const HTMLPreview = (props: any) => {
import React, { useEffect, useRef } from "react";

const STEGA_REGEX = /[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]|\uDB40[\uDC20-\uDC7F]/g;

interface HTMLPreviewProps {
html?: string;
}

const HTMLPreview = (props: HTMLPreviewProps) => {
const { html } = props;
if (!html) {
const containerRef = useRef<HTMLDivElement>(null);
const cleanHtml = typeof html === "string" ? html.replace(STEGA_REGEX, "") : "";

useEffect(() => {
const container = containerRef.current;
if (!container || !cleanHtml) return;

// Re-execute any inline <script> tags so interactive SVG/HTML widgets work inside Sanity Studio preview
const scripts = Array.from(container.querySelectorAll("script"));
for (const oldScript of scripts) {
const newScript = document.createElement("script");
for (const attr of Array.from(oldScript.attributes)) {
newScript.setAttribute(attr.name, attr.value);
}
newScript.text = oldScript.textContent || "";
oldScript.parentNode?.replaceChild(newScript, oldScript);
}
}, [cleanHtml]);

if (!cleanHtml) {
return <div>Add some HTML</div>;
}
return <div dangerouslySetInnerHTML={{ __html: html }} />;
return <div ref={containerRef} dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
};

const preview = (props: any) => <HTMLPreview {...props} />;

const preview = (props: HTMLPreviewProps) => <HTMLPreview {...props} />;
export default preview;
15 changes: 7 additions & 8 deletions apps/site/src/components/portable-text/PtHtml.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,16 @@ type HtmlNode = { _type: "htmlBlock"; html?: string };
type Props = PtProps<HtmlNode>;

const { node } = Astro.props;

/**
* Raw HTML authored in the Studio, rendered unescaped by design — this block
* exists so editors can embed markup the other block types do not cover.
/*
* Author-written HTML block from Sanity Studio.
* It is trusted because only authenticated Studio users can write it.
*
* Not stegaClean'd: cleaning would strip characters from the author's markup.
* Stega is only ever enabled on preview responses, which are `noindex` and
* `no-store`, so encoded characters cannot reach a published page.
* Strip zero-width Stega watermark characters (when previewing drafts) so inline
* <script>, <style>, and <svg> blocks execute cleanly without syntax errors,
* while preserving 100% of visible author markup.
*/
const html = node.html;
const STEGA_REGEX = /[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]|\uDB40[\uDC20-\uDC7F]/g;
const html = typeof node.html === "string" ? node.html.replace(STEGA_REGEX, "") : undefined;
---

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