squirrelscan
GitHub

Placeholder Text

Detects template leftovers and filler copy that shipped to production

Detects template leftovers and filler copy that shipped to production

Rule IDcontent/placeholder-text
CategoryContent
ScopePer-page
Severitywarning
Weight6/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:

KindExampleMeaning
lorem-ipsumLorem ipsum dolor sit ametlatin filler was never replaced
boilerplate-copyYour Company Name, Insert text heretheme 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-artifactRating: NaN, a table cell reading undefineda missing value was rendered anyway
todo-markerTODO: 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 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:

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

[rules]
disable = ["content/placeholder-text"]

Disable all Content rules

[rules]
disable = ["content/*"]

Enable only this rule

[rules]
enable = ["content/placeholder-text"]
disable = ["*"]

Type to search…

↑↓ navigate↵ openesc close