URL: /rules/content/placeholder-text

---
title: "Placeholder Text"
description: "Detects template leftovers and filler copy that shipped to production"
---

Detects template leftovers and filler copy that shipped to production

| | |
|---|---|
| **Rule ID** | `content/placeholder-text` |
| **Category** | [Content](/rules/content) |
| **Scope** | Per-page |
| **Severity** | warning |
| **Weight** | 6/10 |

## Solution

Each family points at a different break in the publishing pipeline. Lorem ipsum and theme boilerplate mean a page was published before its copy was written: replace the text, and add the affected fields to whatever check gates publishing. Unrendered template syntax means the templating engine never ran over that string, usually because the value was interpolated into an already-escaped fragment or the template was served as static HTML: render it server-side, or delete the stale copy. A visible `undefined`, `NaN`, `null` or `[object Object]` means the value was missing and the code concatenated it into the copy anyway: guard the field at the point of render rather than in CSS. `TODO` and `FIXME` markers mean a draft shipped: finish or remove the note, since search engines and readers see it exactly as written.

## What it checks

Five families, in visible text only:

| Kind | Example | Meaning |
|---|---|---|
| `lorem-ipsum` | `Lorem ipsum dolor sit amet` | latin filler was never replaced |
| `boilerplate-copy` | `Your Company Name`, `Insert text here` | theme or page-builder filler |
| `unrendered-template` | `{{ user.name }}`, `{% for x in y %}`, `<%= title %>`, `${total}`, `[[slug]]` | the template engine never ran |
| `stringified-object` | `[object Object]` | an object was concatenated into a string |
| `js-artifact` | `Rating: NaN`, a table cell reading `undefined` | a missing value was rendered anyway |
| `todo-marker` | `TODO: write this`, `FIXME(alice)` | a draft note shipped |

**Severity depends on certainty.** Lorem ipsum, unrendered template syntax and `[object Object]` are machine-generated and nothing legitimate produces them, so they fail. The rest warn, including the bare words `undefined`, `null` and `NaN`: an API reference whose defaults column reads `null` is ordinary, and telling it apart from a value that failed to render is not possible from the text alone.

## What is not visible text

The check reads rendered prose, never raw HTML, so a framework attribute such as `v-bind:title="{{ x }}"` is not reported. Four kinds of markup are excluded:

- `<script>`, `<style>` and `<noscript>`, because a visitor never reads them.
- `<code>`, `<pre>`, `<samp>` and `<kbd>`, because a visitor reads them as a quoted literal. This page is the proof: every example above is inside a code span, and this page passes its own rule.
- `<template>`, because its content is inert markup for JavaScript to clone. Without this exclusion every Vue, Alpine, Handlebars and htmx page would report its own component templates.
- Containers belonging to an in-page code editor or syntax highlighter, matched by class: `hljs`, `shiki`, `prism`, `torchlight`, `codeblock`, `code-block`, and anything starting `language-`, `cm-` or `monaco-`. CodeMirror and Monaco build their view from plain divs and spans with no `<pre>` or `<code>` anywhere, so a tag-only test reads a Liquid tutorial as a page full of unrendered Liquid.

Text is read with a line break at every block boundary, so a value alone in a table cell or list item is seen as a value alone on its line. A page with no body is skipped rather than passed, and a [soft 404](/rules/crawl/soft-404) is skipped so one broken error template cannot spray warnings across a crawl.

## False positives

Each family is deliberately narrower than its name suggests, because every one of them is also a legitimate thing to write.

**Lorem ipsum** needs corroboration: three distinct marker words, or the opening phrase `dolor sit amet`. The `Lorem ipsum` bigram alone is never enough at any position, so a page that mentions the filler, the way Jinja's documentation describes its `lipsum()` helper, stays clean. Words that are real in other languages, including `dolor`, `sit`, `amet`, `elit` and `sed`, are not markers at all, so Spanish and Latin copy stays clean.

**Boilerplate copy** matches whole-word phrases, never single words and never substrings. `Request a sample of our placeholder API`, `we resample textures` and the headings `Placeholder Text` and `Example text` are all ordinary and stay clean.

**JavaScript artifacts** match only the JavaScript spellings, case-sensitively, as standalone words. `NULL` in SQL prose, a sentence beginning `Undefined`, and identifiers such as `nullable` or `non-null` never match. Beyond that, the word has to sit where a **value** sits, not where a noun sits. It is reported only when it is alone on its line, which is what `<h1>{title}</h1>` renders as when the title is missing, or when a label such as `:` or `>` introduces it. A comma does not count as a label, and neither does a pipe: both read as prose far more often than as a list. All of these stay clean:

```text
This triggers undefined behaviour in C++.
The function returns undefined when the key is missing.
We reject the null hypothesis.
Comparison with NaN
If an operand is a quiet NaN, there is no exception.
Accepts a string, null, or undefined.
The parameter accepts string | null | undefined
TypeError: null/undefined has no properties
Reject the [null] hypothesis.
```

The cost of that strictness is deliberate: `Posted by undefined` at the end of a line is the same shape as `Comparison with NaN`, so the ambiguous shape is left alone.

**Comment markers** match `TODO` and `FIXME` in annotation form: followed by `:` or `(`, preceded by a comment sigil such as `//`, or introducing a lowercase sentence on the same line. A bare standalone `TODO` is left alone, because it is the name of a column on every task board ever shipped. `XXX` is narrower still and needs the comment sigil, because `Super Bowl XXX: the box score` and `Rated XXX (explicit)` fit annotation form exactly. Redacted digits such as `555-XXX-XXXX` and `$XXX,XXX` are not markers either.

## Known limit

A page whose SUBJECT is one of these values is reported, as a warning. A JavaScript language reference that lists `null`, `undefined` and `NaN` as navigation entries, an API reference whose defaults column reads `null`, or an article whose visible heading is the bare word all look exactly like a page whose values failed to render, and no amount of reading the text can tell them apart. That is why this family warns rather than fails. Disable the rule for those pages.

## Enable / disable

### Disable this rule

```toml squirrel.toml
[rules]
disable = ["content/placeholder-text"]
```

### Disable all Content rules

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

### Enable only this rule

```toml squirrel.toml
[rules]
enable = ["content/placeholder-text"]
disable = ["*"]
```
