import { defaultSchema, type Schema } from 'hast-util-sanitize';
export const markdownSchema: Schema = {
...defaultSchema,
attributes: {
...defaultSchema.attributes,
code: [...(defaultSchema.attributes?.code ?? []), ['className', /^language-/]],
span: [...(defaultSchema.attributes?.span ?? []), 'className'],
a: [...(defaultSchema.attributes?.a ?? []), 'rel', 'target'],
h1: ['id'],
h2: ['id'],
h3: ['id'],
},
protocols: {
...defaultSchema.protocols,
href: ['http', 'https', 'mailto'],
src: ['http', 'https'],
},
tagNames: [
...(defaultSchema.tagNames ?? []),
'del',
'input', // GFM task-list checkboxes
],
clobberPrefix: 'user-content-',
};
import { useMemo } from 'react';
import { renderMarkdown } from './markdown-pipeline';
interface MarkdownProps {
source: string;
className?: string;
}
export function Markdown({ source, className }: MarkdownProps) {
const html = useMemo(() => renderMarkdown(source), [source]);
return (
<div
className={className}
// Safe: the pipeline sanitized against an allowlist before serialization.
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}
import { unified, type Processor } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import rehypeStringify from 'rehype-stringify';
import { markdownSchema } from './sanitize-schema';
let cached: Processor | null = null;
function buildProcessor(): Processor {
return unified()
.use(remarkParse)
.use(remarkGfm)
// allowDangerousHtml keeps raw HTML in the tree so rehypeRaw can
// parse it and rehypeSanitize can then vet every node.
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeSanitize, markdownSchema)
.use(rehypeStringify)
.freeze() as Processor;
}
export function renderMarkdown(source: string): string {
if (!cached) {
cached = buildProcessor();
}
const file = cached.processSync(source);
return String(file);
}
Rendering user-authored markdown safely means never trusting the HTML that markdown can produce. The naive approach of piping marked output straight into dangerouslySetInnerHTML opens the door to script injection, javascript: URLs, and event-handler attributes. This snippet builds a hardened pipeline on top of the unified ecosystem, where remark parses markdown to an mdast tree and rehype transforms it into an hast HTML tree that is sanitized against an explicit allowlist before it is ever serialized.
The markdown pipeline tab assembles the processor once and caches it. The order of plugins matters: remarkParse reads the source, remarkGfm adds tables and strikethrough, remarkRehype bridges to HTML with allowDangerousHtml: false so raw inline HTML is dropped rather than passed through, rehypeRaw re-parses any surviving HTML into real nodes, and only then does rehypeSanitize run. Sanitizing last is critical — it must see the final tree, because plugins that run afterward could reintroduce unsafe nodes. The processor is built with .freeze() so it can be reused across calls without re-registering plugins.
The sanitize schema tab extends defaultSchema from hast-util-sanitize rather than replacing it, so the safe baseline is preserved. It permits className on code and span to keep syntax-highlighting classes, adds id on headings for anchor links, and restricts a to http, https, and mailto protocols. Anything not on the allowlist — onclick, style, <script>, unknown protocols — is stripped silently. This allowlist model is safer than a denylist because new attack vectors are excluded by default instead of requiring a patch.
The Markdown component tab exposes the result to React. Because the pipeline guarantees the HTML is already clean, dangerouslySetInnerHTML is acceptable here — the danger has been removed upstream. useMemo avoids re-parsing on unrelated re-renders. The main trade-off is that sanitization runs synchronously and can be costly for very large documents, so the memo key and the frozen processor together keep repeated renders cheap. Reach for this pattern whenever markdown originates from users, comments, or third-party content.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
Share this code
Here's the card — post it anywhere.