typescript 75 lines · 3 tabs

Safe markdown rendering (remark + rehype)

Shared by codesnips Jan 2026
3 tabs
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-',
};
3 files · typescript Explain with highlit

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

ruby
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

jwt authentication api
by Kai Nakamura 2 tabs
typescript
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

typescript reliability retry
by codesnips 2 tabs
bash
#!/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

secrets-management vault environment-variables
by Kai Nakamura 1 tab
typescript
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

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
typescript
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)

security node jwt
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Safe markdown rendering (remark + rehype) — share card
Link copied