A template that renders nothing is not the same problem as a build that stops, and Leed’s helpers do both. Roughly a dozen helpers reach into an object without checking it first, so a wrong uuid takes the whole build down with a TypeError. Twice that many swallow the same mistake and return an empty string. This page sorts every helper by how it fails, so you can start from the symptom rather than from the helper name.
The mechanics behind all of this — how helpers are registered, why the last argument is always the Handlebars options object, and what SafeString means — are established once in How Helpers Work. This page assumes them.
Failures that stop the build
These helpers dereference something they never checked. When the value is missing you get a TypeError, Eleventy aborts, and leed site build exits non-zero — the page never renders and neither does the rest of the site.
| Helper | Trigger | Error | Guard that prevents it |
|---|---|---|---|
pageType, user, label | The uuid is not in the backing map and you passed a field name | TypeError reading the field of undefined | {{#if (lookup pageTypeList this.pageTypeId)}} before the call, or omit the field name — with no field the helper returns undefined instead of throwing |
Series, MultipartSeries, AnnouncementSeries | The label id is not in labelList | TypeError reading series of undefined | Wrap in {{#IfMultipartSeries …}} / {{#IfAnnouncementSeries …}}, which guard the lookup for you |
Series, MultipartSeries, AnnouncementSeries | The label is a series of the requested type, but the collection for this page type is absent | TypeError reading length of undefined | Call the helper only from a page whose page type actually owns that series |
searchIndexHash, searchDocumentHash | The page has no documentationConfiguration | TypeError reading searchIndexId of undefined | {{#if documentationConfiguration.searchIndexId}} — this is exactly what Leed’s own head.hbs does |
stripTags | The argument is null or undefined | TypeError: .replace of null | {{ stripTags (default item.name "") }} |
FeatureVideo | featureVideo exists but carries no videoOptions object | TypeError reading allowFullscreen of undefined | {{#if featureVideo.videoOptions}} |
wrapSubstring | Any of the three arguments is not a string, including omitting one | TypeError carrying the usage string | Pass all three: {{ wrapSubstring title "vs." "no-bold" }} |
date | The format argument is omitted or is not a string | TypeError carrying the usage string | Always pass the format first: {{#date "iso"}}…{{/date}} |
latestDate | Either argument is not a string | TypeError carrying the usage string | Pass both: {{ latestDate "publishedAt" "all" }} |
turnstileKey | Called without @root | TypeError reading env of undefined | {{ turnstileKey @root }} — the context is never read from this |
pageTypeSlug | A page-type id that is not in pageTypeList | TypeError reading slug of undefined | Pass the page-type object rather than the id where you have it |
filterObject | The map argument is null or undefined | TypeError: Cannot convert undefined or null to object | {{#if pageTypeList}}, or pass a literal {} |
resolveRedirectIndex | The page type has a redirectIndex but its :unpaged collection is missing | TypeError reading length of undefined | Only ever call this from the redirects template, where the collection is guaranteed |
| Any unregistered name | Calling a handlebars-helpers string helper, or any typo, with an argument or as a block | Missing helper: "<name>" | Check the name against the Helper Index |
The three helpers that validate their arguments deliberately throw a TypeError whose message is the usage line, which makes them the easiest of the set to diagnose:
usage: {{#date 'iso'|'rfc3339'|'rfc822'|'default' [context]}}{{ e.g. publishedAt }}{{/date}}
usage: {{ latestDate collectionId ['publishedAt','modifiedAt'] }}
field name must be a stringIf you see one of those strings in your build output, the fix is always in the call, never in the data.
Two failures that happen before any template renders
Two of Leed’s plugins validate their configuration when Eleventy loads them, not when a template calls them. If the build environment has no domain, the plugin throws while the config is still being assembled:
domain or pagesDomain not set
domain not setThe first comes from the URL helper plugin, the second from the media helper plugin. Tell them apart from a template error by what is missing from the output: no file name, no line number, and no partially-built site, because nothing has rendered yet. This is a build-environment problem — the deployment was started without the site’s domain configuration — and no change to a template will fix it.
Failures that render nothing
This is where the time goes. A helper that returns "" leaves a hole in the page that looks identical to a template that was never written, and most of these say nothing at all unless you ask the build to talk.
| Helper | Condition | Returns | Log level |
|---|---|---|---|
collection | The collection name does not exist | undefined, not [] | none |
SmartPaginationLinks | Called outside a pagination context | "" | none |
ImageTag | The argument is not an object | "" | none |
firstTruthy | No argument is truthy | "" | none |
Series family | The label exists but is not a series of the requested type | "" | none |
fullName | this is not a user record | the literal undefined undefined | none |
CTA | The page carries disableCta: true | "" | none |
JSON-LD | The page type’s slug is not blog | an HTML comment, not structured data | none |
toc | No heading in the content carries an id | undefined | none |
FeatureImage, FeatureVideo | No featureImage / featureVideo on the context | "" | debug |
previousPageNav, nextPageNav | The current page is not in the left menu | undefined | debug |
staticImage, Image | The url is empty or whitespace | "" | warn |
sorted | The first argument is not an array | [] | warn |
breadcrumbsNav | The left menu id resolves to no menu | "" | warn |
hasTier, hasFeature | Unrecognized tier or unknown feature name | false — fail-closed | warn |
docsLayoutName, docsColorTheme, docsLayoutTemplate, docsMenuName | No documentationConfiguration on the page | undefined | info |
leedConfiguredCSS | No documentationConfiguration on the page | "" | info |
process | The page’s tier does not include auto-linking | content renders, unlinked | info, once per build |
readingTime | No wordCount on the context | "" | error |
PaginationLinks, PaginationRange | Called outside a pagination context | "" | error |
staticImage, Image, svgDiagram | A required argument is missing, or the diagram name fails validation | "" | error |
imageUrl, absoluteImageUrl | The url argument is missing or falsy | "" | error |
imageUrl, absoluteImageUrl | The variant argument is omitted | a url containing [object Object] | none |
collection deserves its own sentence, because its failure is invisible even at the highest log level. A misspelled name returns undefined, and {{#each undefined}} renders nothing without complaint — indistinguishable from a collection that legitimately has no members. If a list is empty and you cannot tell which, print the name you are asking for and compare it against the collection table on Collection, Lookup and Series Helpers.
Run leed site build with those two faults in a layout and the output carries one loud line and one silence:
$ leed site build
leed:media:error Too few parameters. {url, 'variant name'} are required.
leed:build:info rendered 128 pages in 4.2sThe {{ Image src }} call logs leed:media:error and names exactly what it wanted. The {{#each (collection "typo") }} block logs nothing at all — the page simply renders without that section, which is why a missing list is worth grepping the build output for rather than staring at the template.
Every line Leed logs is namespaced leed:<plugin>:<level>, and the plugin half is not always the name you would guess — the documentation-navigation helpers log under menu:navigation, and a dozen small utilities log under uncategorized. Use this table to point --noise at the right channel:
| Helpers | Logger namespace |
|---|---|
url, siteUrl, pagesUrl, buildUrl | leed:urls:* |
| Every image and video helper | leed:media:* |
collection, sorted, filterObject, pageType, user, label, Authors, Labels, the series helpers, the menu-safety helpers | leed:collections:* |
PaginationLinks, SmartPaginationLinks, PaginationRange | leed:paging:* |
pageTypeSlug, readingTime, fullName, CTA, Recommendations, fieldId, turnstileKey, resolveRedirectIndex | leed:custom-fields:* |
date, latestDate | leed:dates:* |
docsLayoutName, docsColorTheme, docsLayoutTemplate, docsMenuName, previousPageNav, nextPageNav, breadcrumbsNav | leed:menu:navigation:* |
hasTier, hasFeature | leed:entitlements:* |
process | leed:process:* |
JSON-LD | leed:jsonld:* |
stripTags, wrapSubstring, firstTruthy, concat, JSONsafe, templateExists, useCustomTemplate, searchIndexHash, searchDocumentHash, leedConfiguredCSS, formatSecurityName, logger | leed:uncategorized:* |
Output that appears as escaped HTML
The symptom is unmistakable: you view source on the built page and find <a href= where a link should be, or a visible <span class="pagination-active"> sitting in the middle of your text. Handlebars escaped the helper’s return value because the helper returned a plain string rather than a SafeString.
Seven helpers return raw markup that Handlebars will escape. Call them with triple braces:
| Helper | Returns | Correct braces |
|---|---|---|
SmartPaginationLinks | raw HTML | {{{ SmartPaginationLinks true }}} |
previousPageNav | raw HTML | {{{ previousPageNav "Previous" }}} |
nextPageNav | raw HTML | {{{ nextPageNav "Next" }}} |
breadcrumbsNav | raw HTML | {{{ breadcrumbsNav '<i class="home"></i>' }}} |
JSON-LD | a raw <script> block | {{{ JSON-LD }}} |
process | rendered page content | {{{ process content }}} |
resolveRedirectIndex | plain text, but unescaped | {{{ resolveRedirectIndex this ../this }}} |
Ten more return a SafeString, so double braces are correct and triple braces buy you nothing:
| Helper | Returns | Correct braces |
|---|---|---|
staticImage | SafeString <img> | {{#staticImage "/x.svg"}}…{{/staticImage}} |
svgDiagram | SafeString wrapper <div> | {{ svgDiagram "svg/chart-1" }} |
Image | SafeString <img> | {{#Image image "medium"}}…{{/Image}} |
FeatureImage | SafeString <img> | {{#FeatureImage}}…{{/FeatureImage}} |
ImageTag | SafeString <img> | {{ ImageTag item.image }} |
FeatureVideo | SafeString iframe wrapper | {{ FeatureVideo }} |
CTA | SafeString hidden marker | {{ CTA "article-cta" }} |
Recommendations | SafeString hidden marker | {{ Recommendations "recommendations" }} |
JSONsafe | SafeString JSON string literal | {{ JSONsafe data.summary }} |
wrapSubstring | SafeString <span> | {{ wrapSubstring title "vs." "no-bold" }} |
That last point generalizes. Escaping is correct inside an HTML attribute — = decodes back to = when the browser parses the tag, which is why Leed’s own {{ buildUrl videoSrc "autoplay=true" }} inside a src attribute is fine with double braces. It is only wrong when the output is not HTML at all.
Failures caused by the wrong context
Most helpers read something off this or off the Handlebars options object. Move the same call into a partial, a paginated list item or a feed template and the thing it reads may no longer be there.
| Helper(s) | What the context must carry | Where that context exists |
|---|---|---|
fullName | this is a user record with firstName / lastName | Inside {{#Authors this}}, inside {{#user id}} used as a block, or an author list item |
fieldId | this is a form field | Inside the field iteration of a form template |
turnstileKey | the argument carries leedSiteEnv and deployment | Pass @root; it is never read from this |
readingTime | wordCount (and optionally readingWpm) | Any page or list item built from CMS content |
FeatureImage, FeatureVideo | featureImage / featureVideo | A page or list item whose record has one set |
Authors, Labels | authors / labels on the context you pass | Any page or collection item — pass this explicitly |
Series, MultipartSeries, AnnouncementSeries | pageTypeId, labelList and collections | A real page context; not a bare partial |
PaginationLinks, SmartPaginationLinks, PaginationRange | both pagination and list on the root context | A paginated list template only |
docsLayoutName, docsColorTheme, docsMenuName, previousPageNav, nextPageNav, breadcrumbsNav, leedConfiguredCSS | documentationConfiguration | A page bound to a documentation page type |
searchIndexHash, searchDocumentHash | documentationConfiguration.searchIndexId | A documentation page with a search index configured |
collection, latestDate, sorted (when fed (collection …)) | collections | Any page context |
hasTier, hasFeature | entitlements.tier | Every page — supplied by the build environment |
Around eight helpers never read this directly. They call an internal unwrapper that walks four levels and takes the first one that looks like page data:
options.data.root.list.data— a paginated list itemoptions.data.root— the root page contextoptions.data— the Handlebars data frame- the value itself
This is the reason the same helper works on a single page and inside a paginated list item without you passing anything. It is also the reason a helper can work in one place and fail in the other: on a paginated list page step 1 wins, so a helper reads the item’s data, not the list page’s. FeatureImage, FeatureVideo and readingTime compensate by trying both the options object and this before giving up.
date does not compensate, which is why it takes an optional context argument. On a single page, this is the page and no second argument is needed:
{{#date "MMMM D, YYYY"}}{{ publishedAt }}{{/date}}On a list page, pass the item explicitly so timezone and dateFormat resolve against the item rather than the list:
{{#date "MMMM D, YYYY" this}}{{ publishedAt }}{{/date}}Arguments in the wrong slot
Handlebars appends the options object as the last argument of every helper call, always. A helper declared with three parameters and called with two therefore receives the options object in its second parameter — and does not receive an options object at all.
Only eight helpers detect this and shift the arguments back into place. On those, omitting the optional middle argument is supported and safe:
pageType · user · label · Authors · Labels · filterObject · sorted · date
Everywhere else, the options object simply becomes your argument. Three shapes of damage follow, in increasing order of nastiness:
The helper treats it as a truthy value. {{ url }} with no argument puts the options object in type; the helper tests type === "relative", which is false, and you get an absolute URL. This one is documented behavior and is why {{ url }} and {{ url "relative" }} are the two supported calls.
The helper loses its options object and cannot function. {{#PaginationLinks}} with no argument does not behave like {{#PaginationLinks true}}. The options object lands in reverse, the real options parameter is undefined, and the helper — which reads the pagination context off that object — logs Pagination links only works inside a pagination context. First parameter must be {reverse:boolean}. and renders nothing at all.
The helper interpolates it into your output. imageUrl guards with if (!variant), and the options object is perfectly truthy, so the guard never fires. The helper then runs url.replace("original", options), JavaScript stringifies the object, and the variant segment of the emitted URL becomes the literal text [object Object]. The image 404s and nothing is logged.
{{ imageUrl image }}That is the broken call. This is the correct one:
{{ imageUrl image "profile" }}previousPageNav and nextPageNav fail the same way: with no label argument the options object lands in the label slot and is interpolated straight into the markup, so the sub-label of the link reads [object Object]. Leed’s own partial routes an unset label through default rather than leaving the argument off, and Documentation Navigation Helpers shows the pattern.
If a built page carries [object Object] anywhere, an omitted argument is almost always the cause. The full account of why the options object is always last lives in How Helpers Work.
Name collisions and missing helpers
Leed’s 64 helpers are registered onto the Handlebars instance after the handlebars-helpers library, so where the two libraries share a name, Leed’s wins. There is exactly one such collision, and it matters:
The library’s whole string category is deliberately not installed — its reverse helper collides with the array category’s. Thirty-six helper names you might reasonably expect are therefore unavailable, and calling one is a build-stopping error, not a quiet blank:
Missing helper: "truncate"Handlebars raises that whenever an unregistered name is called with an argument ({{ truncate title 40 }}) or as a block ({{#truncate …}}). The one shape that fails quietly is a bare {{ truncate }} with no arguments, which Handlebars treats as a property lookup on the context and resolves to an empty string.
The string helpers most often reached for, and what to use instead
truncate · ellipsis · lowercase · uppercase · capitalize · titleize · replace · trim · append · prepend · split · startsWith
Leed ships four minimal stand-ins that cover the cases the shipped templates need — stripTags, wrapSubstring, concat and firstTruthy — documented on Utility Helpers. For anything else, do the transformation in the CMS content or in a partial rather than in the template. The complete list of all 36 absent names is on How Helpers Work.
One further name to strike from older notes: addAnchorReason does not exist. It was removed and replaced by a build-time data-reason convention on recommendation links; there is nothing to call.
A symptom index
Start here when you know what you are seeing but not what caused it.
| What you see | Likely cause | Where it is explained |
|---|---|---|
The build stops with TypeError: Cannot read properties of undefined | An unguarded lookup — a uuid, a label id or a missing documentationConfiguration | Failures that stop the build |
The build stops with domain not set before any page renders | The build environment has no site domain | Two failures that happen before any template renders |
The build stops with a message beginning usage: | date, latestDate or wrapSubstring was called with a non-string argument | Failures that stop the build |
<a href= or visible tag text in the page source | A raw-HTML helper called with {{ }} | Output that appears as escaped HTML |
[object Object] in a URL or in the page text | An omitted argument on a helper that does not shift | Arguments in the wrong slot |
| A list renders nothing, no error anywhere | collection was given a name that does not exist and returned undefined | Collection, Lookup and Series Helpers |
undefined undefined on the page | fullName called outside an author record | Failures caused by the wrong context |
| Pagination controls are absent from a list page | The template is not a paginated list, so pagination and list are missing | Pagination Helpers |
| Prev/next links are missing on one docs page | That page is not an item in the left menu | Documentation Navigation Helpers |
| A menu item vanished from the built nav | Its pageid: target did not render, so menuItemShouldRender returned false | Menu-Safety Helpers |
| A feature image is missing on list pages but present on the page itself | The list item’s data has no featureImage; the helper logs at debug only | Image and Media Helpers |
| Internal terms stopped auto-linking | Auto-linking is Starter and up, and the gate is silent apart from one info line | When a Feature Is Gated |
| A “Powered by Leed” badge you cannot remove | The badge gate is a tier check, not a template override | Tier Gating in Templates |
| A custom docs header or footer is ignored | The slot requires both the template file and the tier | Tier Gating in Templates |
| No structured data on a non-blog page, only an HTML comment | JSON-LD emits structured data for the blog page-type slug only | Date and Content Helpers |
| A table of contents is empty | None of the headings carry an id, so toc returned undefined | Documentation Navigation Helpers |
A redirect destination contains = | resolveRedirectIndex called with {{ }} | Output that appears as escaped HTML |
The build stops with Missing helper: "truncate" (or lowercase, titleize, replace) | The handlebars-helpers string category is not installed | Name collisions and missing helpers |
A date example from the handlebars-helpers docs does nothing | date is Leed’s block helper; the library’s is moment | Name collisions and missing helpers |
When the message you are chasing came from the CLI or the CMS rather than from a helper, Common Error Messages is the wider list, and every helper’s own parameters and defaults are on its group page — reachable by name from the Helper Index.