URL: /rules/content/unrendered-markup

---
title: "Unrendered Markup"
description: "Detects literal markdown or escaped HTML leaking into rendered copy"
---

Detects literal markdown or escaped HTML leaking into rendered copy

| | |
|---|---|
| **Rule ID** | `content/unrendered-markup` |
| **Category** | [Content](/rules/content) |
| **Scope** | Per-page |
| **Severity** | warning |
| **Weight** | 5/10 |

## Solution

Something rendered a value as plain text that was authored as markup. Find the field, not the page: a CMS that stores markdown and a template that prints it without a markdown-to-HTML pass produces literal `**bold**` and `[text](url)` everywhere that field appears, and fixing one page leaves the rest broken. Visible `<p>` or `<a href` means the opposite mistake: HTML was escaped twice, usually by escaping a value that a templating engine (Jinja, Twig, Blade, JSX) had already escaped, so remove the manual escape rather than marking the value safe. Visible `&nbsp;` or `&amp;` means the entity itself was encoded a second time on the way in, which is normally an import or a rich-text editor round-trip, so re-import the affected content. If the markup is meant to be on display, put it in `<code>` or `<pre>`, which this rule skips.

## What it checks

Seven kinds, in three families, in visible text only:

| Kind | Example on screen | Meaning |
|---|---|---|
| `markdown-emphasis` | `**unlimited audits**` | a markdown field printed as plain text |
| `markdown-link` | `[our docs](https://example.com/d)` | the same, for links and images |
| `markdown-heading` | a line starting `## ` | the same, for headings |
| `markdown-code-fence` | a line of three backticks | the same, for fenced code |
| `markdown-inline-code` | `` `squirrel audit` `` | corroborating only, see below |
| `escaped-html-tag` | `<p>Prices exclude tax.</p>` | HTML escaped twice |
| `double-encoded-entity` | `&nbsp;`, `&amp;` | an entity encoded a second time |

The check reads the text a visitor sees, decoded, never the raw HTML. That matters for the HTML families: the page source says `&lt;p&gt;`, and the browser paints `<p>`, so the rule looks for the painted form.

**Severity depends on certainty.** Visible tags and entities fail, because nothing outside a code block renders them on purpose. Literal markdown warns at one or two occurrences and fails at three or more, where it stops looking like a typo and starts looking like a template that never ran its markdown pass.

## What is excluded

Displaying markup is not the same as leaking it, and this rule is mostly a list of ways to tell the two apart.

- `<script>`, `<style>`, `<noscript>` and `<template>`, because a visitor never reads them.
- `<code>`, `<pre>`, `<samp>`, `<kbd>` and `<textarea>`, because a visitor reads them as a quoted literal. A tutorial showing `**bold**` in a code span is doing its job, and an editor pre-filled with a post's markdown source is not a defect.
- Any element a syntax highlighter marks as its own: the whole class tokens `highlight`, `hljs`, `shiki`, `chroma`, `codehilite`, `code-block`, `preformatted` and their siblings, any `language-*` or `lang-*` class, and the `data-language` family of attributes. Whole tokens, so a `highlight-box` callout is still judged as prose.

Five more limits keep the rule quiet on pages that talk about markup rather than mangle it.

- **Underscore emphasis must contain a space.** `__init__` and `__main__` are flanked exactly like bold and appear in ordinary prose, so `__bold__` on its own is not reported. `**bold**` still is.
- **A markdown link needs a URL-shaped target.** `array[i](arg)` and `[see note] (below)` are not links.
- **A single `#` needs a capitalised word after it.** `#` is also the number sign, so `# of seats` in a table header is not a heading, and neither is the URL fragment MDN prints under a specification link. `##` and deeper have no second reading and are taken as written, so `## my-package` still counts.
- **Escaped tags must be lowercase.** `<Article>` and `<Header>` are component names a page is showing on purpose. Markup that leaked from a template came from HTML a server emitted, which is lowercase.
- **At least one escaped tag must carry a true closing form or a recognised attribute with a value.** A page that writes `<pre>` or `<meta name>` in a sentence is documenting an element. A leak brings `</p>` or `class="row"` with it. A self-closing `<br/>` is not a closing form, `</path/to/file>` is a path rather than a closing tag, and `c=1` is not an attribute, so `if a<b then c=1>0` stays clean.

## False positives

Two classes are known and deliberate.

Prose full of identifiers stays clean: `CLOUDFLARE_API_TOKEN`, `some_file_name.ts`, `max_pages`, `width * height` and `2 * 3` are none of them emphasis, because both emphasis forms require CommonMark-style flanking.

Backticks alone never trip the rule. They are the one markdown character people put on a page on purpose and often, in a prompt written to be copied into an agent, a chat transcript, or a changelog. Inline code spans are reported in the detail once another family has fired, and never before.

Text is read block by block, so a match is never assembled out of two elements that each hold half of it. A heading ending in `*` followed by a link starting with `*` is two stray asterisks, not emphasis.

The residual case is a page whose subject is HTML source, displayed in a container the rule cannot recognise. A tutorial that shows `<p>This is a paragraph.</p>` inside a plain `<div>` with a bespoke class is indistinguishable from a page that leaked it. Wrap such examples in `<pre>` or `<code>`, which is also what makes them selectable and copyable for readers.

## Related rules

[content/mojibake](/rules/content/mojibake) covers the neighbouring problem: bytes decoded with the wrong encoding. The two sit one encoding level apart on entities. Mojibake reports a visible `&amp;nbsp;`, meaning the source was encoded three times; this rule reports a visible `&nbsp;`, meaning twice.

They do overlap on triple-encoded text, and deliberately so. A page showing `&amp;nbsp;` trips mojibake on the whole sequence and this rule on the `&amp;` inside it, and both findings are true: the entity was encoded once too many, and a reader is looking at raw markup. Expect one finding from each rule on such a page.

## Enable / disable

### Disable this rule

```toml squirrel.toml
[rules]
disable = ["content/unrendered-markup"]
```

### Disable all Content rules

```toml squirrel.toml
[rules]
disable = ["content/*"]
```

### Enable only this rule

```toml squirrel.toml
[rules]
enable = ["content/unrendered-markup"]
disable = ["*"]
```
