You will understand the XSS risk in code content, make sure HTML escaping works correctly, be wary of user-controlled meta strings, use rehype-sanitize, and compose a strict Content-Security- Policy for static pages.

Code that looks like plain text can actually be an entry point for attacks. If the contents of a code block or a meta string are rendered without protection, characters like <script> can slip through as HTML and execute JavaScript on visitors' pages.
Episode 14 covers security and output sanitization: how Shiki escapes code content, the danger of user-controlled meta strings, using rehype-sanitize, and composing a Content-Security-Policy for SSG result pages. Security isn't an extra feature; it's a production requirement.
An XSS attack happens when user content is rendered as HTML instead of text. A classic example: a comment containing code with an unescaped <script>. When Markdown is converted to HTML, that tag gets executed in visitors' browsers.
In the highlighting pipeline, the same risk appears if the contents of a fenced code block are inserted into HTML without an escaping step. Fortunately, Shiki and rehype-pretty-code handle this escaping automatically at the tokenization layer.
Shiki turns dangerous characters into HTML entities before wrapping tokens in spans. Characters like <, >, and & are rendered as <, >, and &. Because escaping happens per token, you can be sure code content always appears as text, no matter what the author writes.
This mechanism makes code containing HTML, such as component snippets, safe to render. Here's an example block containing HTML tags:
<div class="box">
<span>Halo dunia</span>
</div>Even though it contains HTML tags, all the lines above are rendered as colored text because they're escaped during tokenization.
The meta string on a fenced code block is translated into attributes on the resulting elements. If the content author is an untrusted party, the meta string can be abused to inject dangerous attributes, for example onmouseover that executes a script.
rehype-pretty-code filters meta into a predefined set of attributes, but always be wise to treat author input as not fully safe data. Don't pass raw meta strings into attributes without validation.
Separate two scenarios: trusted internal authors, and external contributors through a CMS. For the second scenario, limit the features allowed in the meta string. For example, whitelist languages and disable meta that adds arbitrary attributes:
const allowed = new Set(["ts", "js", "css", "html", "json"]);
const lang = meta.split(" ")[0].trim();
if (!allowed.has(lang)) {
lang = "plaintext";
}Normalizing the language before passing it to the plugin prevents arbitrary languages from users. The principle is simple: don't trust input, validate at the edge.
For extra security, install rehype-sanitize after the highlighting plugin in the pipeline. This sanitizer removes elements and attributes not in the allowed schema:
npm i rehype-sanitizeunified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypePrettyCode, {
theme: "github-dark-default",
})
.use(rehypeSanitize)
.use(rehypeStringify);The order matters: sanitize must run after the plugin that produces markup, but before rehypeStringify. That way, <figure>, <pre>, and data-* attributes are preserved as long as they fit the default schema.
The default rehype-sanitize schema is quite strict and can remove attributes you need. Extend the schema to preserve the code block attributes:
const schema = {
...defaultSchema,
attributes: {
...defaultSchema.attributes,
code: ["data-line-numbers", "data-theme"],
figure: ["data-rehype-pretty-code-figure"],
},
};Only add back the attributes actually used. The narrower the schema, the smaller the attack surface.
Because highlighting runs at build time, SSG result pages don't need inline scripts or dynamic code evaluation. This opens the door to a very strict CSP. The following header, with directives like script-src 'self', suits pages that only load their own assets:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
object-src 'none';
base-uri 'self';With script-src 'self', JavaScript may only come from your own domain, without 'unsafe-inline' and 'unsafe-eval'. style-src 'unsafe-inline' is still allowed because many frameworks inject styles in <style> tags.
Test the CSP before applying it fully. Enable the header in report mode first using Content-Security-Policy-Report-Only, then monitor violation reports in the console. After there are no violations, enable the real header. Remember: WASM isn't needed on the client with the SSG approach, so wasm-unsafe-eval isn't required.
Key takeaways:
rehype-sanitize removes elements and attributes outside the schema.Content-Security-Policy-Report-Only is useful for safe testing.In episode 15 you'll learn multiple dark and light themes: using a theme object like github-dark and github-light, leveraging the CSS custom properties --shiki-dark and --shiki-light, and following prefers-color-scheme to automatically adapt the theme.