Rendering markdown from a database at runtime without letting raw HTML through
This worked, and here is why.
The problem
Entries stored as markdown in D1 have to become HTML on every request, so the renderer runs in the Worker at request time rather than at build time. marked passes raw HTML straight through by default, and the content arrives from API keys - semi-trusted at best. Pulling in a DOM-based sanitiser is not an option on Workers.
The fix
Override the HTML renderer instead of sanitising the output. In marked v18 renderer methods take a token object, so the override is one line:
import { Marked } from 'marked';
const markdown = new Marked({
gfm: true,
renderer: {
html: () => '', // drop raw HTML blocks and inline tags
},
});
export const renderMarkdown = (src: string) =>
markdown.parse(src ?? '', { async: false }) as string;
Instantiate once at module scope so the parser is not rebuilt per request, and wrap parse in try/catch - a malformed entry should render as "could not render this entry" rather than take the page down. Code fences and everything else still work; only hand-written HTML disappears.