Skip to content

Commit ead81ed

Browse files
Remove isomorphic-dompurify and add sanitize-html with types (#1316)
* chore: remove isomorphic-dompurify and add sanitize-html with types * fix: replace TiptapExtensions with RenderExtensions for server-safe HTML generation
1 parent 775b5c8 commit ead81ed

File tree

5 files changed

+190
-572
lines changed

5 files changed

+190
-572
lines changed

app/(app)/articles/[slug]/page.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import { type Metadata } from "next";
1515
import { getPost } from "@/server/lib/posts";
1616
import { getCamelCaseFromLower } from "@/utils/utils";
1717
import { generateHTML } from "@tiptap/core";
18-
import { TiptapExtensions } from "@/components/editor/editor/extensions";
19-
import DOMPurify from "isomorphic-dompurify";
18+
import { RenderExtensions } from "@/components/editor/editor/extensions/render-extensions";
19+
import sanitizeHtml from "sanitize-html";
2020
import type { JSONContent } from "@tiptap/core";
2121
import NotFound from "@/components/NotFound/NotFound";
2222

@@ -73,9 +73,27 @@ const parseJSON = (str: string): JSONContent | null => {
7373
};
7474

7575
const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
76-
const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);
77-
// Sanitize the HTML
78-
return DOMPurify.sanitize(rawHtml);
76+
const rawHtml = generateHTML(jsonContent, [...RenderExtensions]);
77+
// Sanitize the HTML using sanitize-html (server-safe, no jsdom dependency)
78+
return sanitizeHtml(rawHtml, {
79+
allowedTags: sanitizeHtml.defaults.allowedTags.concat([
80+
"img",
81+
"iframe",
82+
"h1",
83+
"h2",
84+
]),
85+
allowedAttributes: {
86+
...sanitizeHtml.defaults.allowedAttributes,
87+
img: ["src", "alt", "title", "width", "height", "class"],
88+
iframe: ["src", "width", "height", "frameborder", "allowfullscreen"],
89+
"*": ["class", "id", "style"],
90+
},
91+
allowedIframeHostnames: [
92+
"www.youtube.com",
93+
"youtube.com",
94+
"www.youtube-nocookie.com",
95+
],
96+
});
7997
};
8098

8199
const ArticlePage = async (props: Props) => {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Server-safe extensions for generateHTML() - no React/DOM dependencies
3+
* Use this for server-side rendering of Tiptap content (e.g., article pages)
4+
* For the editor, use TiptapExtensions from index.tsx instead
5+
*/
6+
import StarterKit from "@tiptap/starter-kit";
7+
import HorizontalRule from "@tiptap/extension-horizontal-rule";
8+
import TiptapLink from "@tiptap/extension-link";
9+
import Link from "@tiptap/extension-link";
10+
import TextStyle from "@tiptap/extension-text-style";
11+
import { Markdown } from "tiptap-markdown";
12+
import { InputRule } from "@tiptap/core";
13+
import UpdatedImage from "./updated-image";
14+
import Document from "@tiptap/extension-document";
15+
import Paragraph from "@tiptap/extension-paragraph";
16+
import Text from "@tiptap/extension-text";
17+
import Youtube from "@tiptap/extension-youtube";
18+
19+
const CustomDocument = Document.extend({
20+
content: "heading block*",
21+
});
22+
23+
export const RenderExtensions = [
24+
CustomDocument,
25+
Paragraph,
26+
Text,
27+
StarterKit.configure({
28+
document: false,
29+
bulletList: {
30+
HTMLAttributes: {
31+
class: "list-disc list-outside leading-3 -mt-2",
32+
},
33+
},
34+
orderedList: {
35+
HTMLAttributes: {
36+
class: "list-decimal list-outside leading-3 -mt-2",
37+
},
38+
},
39+
listItem: {
40+
HTMLAttributes: {
41+
class: "leading-normal -mb-2",
42+
},
43+
},
44+
blockquote: {
45+
HTMLAttributes: {
46+
class: "border-l-4 border-stone-700",
47+
},
48+
},
49+
codeBlock: {
50+
HTMLAttributes: {
51+
class:
52+
"rounded-sm bg-stone-100 p-5 font-mono font-medium text-stone-800",
53+
},
54+
},
55+
code: {
56+
HTMLAttributes: {
57+
class:
58+
"rounded-md bg-stone-200 px-1.5 py-1 font-mono font-medium text-stone-900",
59+
spellcheck: "false",
60+
},
61+
},
62+
horizontalRule: false,
63+
dropcursor: {
64+
color: "#DBEAFE",
65+
width: 4,
66+
},
67+
gapcursor: false,
68+
}),
69+
HorizontalRule.extend({
70+
addInputRules() {
71+
return [
72+
new InputRule({
73+
find: /^(?:---|-|___\s|\*\*\*\s)$/,
74+
handler: ({ state, range }) => {
75+
const { tr } = state;
76+
const start = range.from;
77+
const end = range.to;
78+
79+
tr.insert(start - 1, this.type.create({})).delete(
80+
tr.mapping.map(start),
81+
tr.mapping.map(end),
82+
);
83+
},
84+
}),
85+
];
86+
},
87+
}).configure({
88+
HTMLAttributes: {
89+
class: "mt-4 mb-6 border-t border-stone-300",
90+
},
91+
}),
92+
TiptapLink.configure({
93+
HTMLAttributes: {
94+
class:
95+
"text-stone-400 underline underline-offset-[3px] hover:text-stone-600 transition-colors cursor-pointer",
96+
},
97+
}),
98+
UpdatedImage.configure({
99+
HTMLAttributes: {
100+
class: "rounded-lg border border-stone-200",
101+
},
102+
}),
103+
TextStyle,
104+
Link.configure({
105+
HTMLAttributes: {
106+
class:
107+
"text-stone-400 underline underline-offset-[3px] hover:text-stone-600 transition-colors cursor-pointer",
108+
},
109+
}),
110+
Markdown.configure({
111+
html: false,
112+
transformCopiedText: true,
113+
}),
114+
Youtube.configure({
115+
width: 480,
116+
height: 320,
117+
allowFullscreen: true,
118+
}),
119+
];

next.config.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ const REMOTE_PATTERNS = [
2020
}));
2121

2222
const config = {
23-
// Exclude jsdom and isomorphic-dompurify from bundling to fix ESM/CJS compatibility
24-
serverExternalPackages: ["jsdom", "isomorphic-dompurify"],
2523
// Turbopack configuration for SVGR (replaces webpack config)
2624
turbopack: {
2725
rules: {

0 commit comments

Comments
 (0)