A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1import sanitizeHtml from 'sanitize-html';
2
3/**
4 * Sanitise rendered article HTML before injecting it into a reading page.
5 *
6 * Article content comes from arbitrary PDSes and is UNTRUSTED — a malicious record
7 * could embed `<script>`/`onerror`/etc. We allow only the tags + attributes the curated
8 * blocks (Decision 0002) and inline rich text produce; everything else is stripped.
9 * External links get `rel="noopener noreferrer nofollow ugc"` and open in a new tab.
10 */
11const OPTIONS: sanitizeHtml.IOptions = {
12 allowedTags: [
13 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
14 'ul', 'ol', 'li',
15 'blockquote', 'cite',
16 'pre', 'code', 'kbd',
17 'hr', 'figure', 'figcaption', 'img',
18 'strong', 'em', 'b', 'i', 's', 'del', 'ins', 'sub', 'sup', 'mark',
19 'a', 'br', 'span',
20 'button', // video-embed facade play button (no iframe — injected client-side, Task 7)
21 ],
22 allowedAttributes: {
23 a: [ 'href', 'title', 'rel', 'target' ],
24 img: [ 'src', 'alt', 'loading' ],
25 button: [ 'type', 'data-embed-provider', 'data-embed-id' ],
26 '*': [ 'class' ],
27 },
28 allowedSchemes: [ 'http', 'https', 'mailto' ],
29 allowedSchemesByTag: { img: [ 'http', 'https' ] },
30 transformTags: {
31 a: sanitizeHtml.simpleTransform( 'a', {
32 rel: 'noopener noreferrer nofollow ugc',
33 target: '_blank',
34 } ),
35 },
36};
37
38export function sanitizeArticleHtml( html: string ): string {
39 return sanitizeHtml( html, OPTIONS );
40}