Skip to content

Commit c77fc56

Browse files
Sanitize blog post HTML (#34)
1 parent 428ba16 commit c77fc56

4 files changed

Lines changed: 141 additions & 1 deletion

File tree

apps/web/app/blog/[slug]/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { notFound } from 'next/navigation';
55
import Image from 'next/image';
66
import Link from 'next/link';
77
import { getDb } from '@/lib/db';
8+
import { sanitizeBlogHtml } from '@/lib/sanitize-blog-html';
89

910
interface Post {
1011
id: number;
@@ -61,6 +62,7 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug:
6162
const { slug } = await params;
6263
const post = await getPost(slug);
6364
if (!post) notFound();
65+
const sanitizedContent = sanitizeBlogHtml(post.content);
6466

6567
return (
6668
<div className="max-w-2xl mx-auto flex flex-col gap-8">
@@ -93,7 +95,7 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug:
9395

9496
<div
9597
className="prose prose-gray max-w-none text-gray-700 leading-relaxed [&_h2]:text-xl [&_h2]:font-bold [&_h2]:text-gray-900 [&_h2]:mt-8 [&_h2]:mb-3 [&_h3]:text-base [&_h3]:font-bold [&_h3]:text-gray-800 [&_h3]:mt-6 [&_h3]:mb-2 [&_p]:mb-4 [&_ul]:list-disc [&_ul]:pl-5 [&_ul]:mb-4 [&_ol]:list-decimal [&_ol]:pl-5 [&_ol]:mb-4 [&_li]:mb-1 [&_a]:text-orange-500 [&_a]:underline [&_code]:bg-gray-100 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-sm [&_pre]:bg-gray-900 [&_pre]:text-gray-100 [&_pre]:p-4 [&_pre]:rounded-xl [&_pre]:overflow-x-auto [&_blockquote]:border-l-4 [&_blockquote]:border-orange-300 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-gray-500"
96-
dangerouslySetInnerHTML={{ __html: post.content }}
98+
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
9799
/>
98100
</article>
99101

apps/web/lib/sanitize-blog-html.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import sanitizeHtml from 'sanitize-html';
2+
3+
const allowedTags = [
4+
...sanitizeHtml.defaults.allowedTags,
5+
'figure',
6+
'figcaption',
7+
'img',
8+
];
9+
10+
export function sanitizeBlogHtml(content: string): string {
11+
return sanitizeHtml(content, {
12+
allowedTags,
13+
allowedAttributes: {
14+
...sanitizeHtml.defaults.allowedAttributes,
15+
a: ['href', 'name', 'target', 'title', 'rel'],
16+
img: ['src', 'alt', 'title', 'width', 'height', 'loading'],
17+
code: ['class'],
18+
pre: ['class'],
19+
},
20+
allowedSchemes: ['http', 'https', 'mailto', 'tel'],
21+
allowedSchemesAppliedToAttributes: ['href', 'src'],
22+
transformTags: {
23+
a: sanitizeHtml.simpleTransform('a', {
24+
rel: 'noopener noreferrer',
25+
}, true),
26+
},
27+
});
28+
}

apps/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
"posthog-js": "^1.381.0",
2222
"react": "19.2.4",
2323
"react-dom": "19.2.4",
24+
"sanitize-html": "^2.17.5",
2425
"serwist": "^9.5.0"
2526
},
2627
"devDependencies": {
2728
"@tailwindcss/postcss": "^4",
2829
"@types/node": "^22",
2930
"@types/react": "^19",
3031
"@types/react-dom": "^19",
32+
"@types/sanitize-html": "^2.16.1",
3133
"eslint": "^9",
3234
"eslint-config-next": "16.2.7",
3335
"tailwindcss": "^4",

pnpm-lock.yaml

Lines changed: 108 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)