Helper Gotchas and Failures

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.

HelperTriggerErrorGuard that prevents it
pageType, user, labelThe uuid is not in the backing map and you passed a field nameTypeError 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, AnnouncementSeriesThe label id is not in labelListTypeError reading series of undefinedWrap in {{#IfMultipartSeries …}} / {{#IfAnnouncementSeries …}}, which guard the lookup for you
Series, MultipartSeries, AnnouncementSeriesThe label is a series of the requested type, but the collection for this page type is absentTypeError reading length of undefinedCall the helper only from a page whose page type actually owns that series
searchIndexHash, searchDocumentHashThe page has no documentationConfigurationTypeError reading searchIndexId of undefined{{#if documentationConfiguration.searchIndexId}} — this is exactly what Leed’s own head.hbs does
stripTagsThe argument is null or undefinedTypeError: .replace of null{{ stripTags (default item.name "") }}
FeatureVideofeatureVideo exists but carries no videoOptions objectTypeError reading allowFullscreen of undefined{{#if featureVideo.videoOptions}}
wrapSubstringAny of the three arguments is not a string, including omitting oneTypeError carrying the usage stringPass all three: {{ wrapSubstring title "vs." "no-bold" }}
dateThe format argument is omitted or is not a stringTypeError carrying the usage stringAlways pass the format first: {{#date "iso"}}…{{/date}}
latestDateEither argument is not a stringTypeError carrying the usage stringPass both: {{ latestDate "publishedAt" "all" }}
turnstileKeyCalled without @rootTypeError reading env of undefined{{ turnstileKey @root }} — the context is never read from this
pageTypeSlugA page-type id that is not in pageTypeListTypeError reading slug of undefinedPass the page-type object rather than the id where you have it
filterObjectThe map argument is null or undefinedTypeError: Cannot convert undefined or null to object{{#if pageTypeList}}, or pass a literal {}
resolveRedirectIndexThe page type has a redirectIndex but its :unpaged collection is missingTypeError reading length of undefinedOnly ever call this from the redirects template, where the collection is guaranteed
Any unregistered nameCalling a handlebars-helpers string helper, or any typo, with an argument or as a blockMissing 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 string

If 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 set

The 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.

HelperConditionReturnsLog level
collectionThe collection name does not existundefined, not []none
SmartPaginationLinksCalled outside a pagination context""none
ImageTagThe argument is not an object""none
firstTruthyNo argument is truthy""none
Series familyThe label exists but is not a series of the requested type""none
fullNamethis is not a user recordthe literal undefined undefinednone
CTAThe page carries disableCta: true""none
JSON-LDThe page type’s slug is not blogan HTML comment, not structured datanone
tocNo heading in the content carries an idundefinednone
FeatureImage, FeatureVideoNo featureImage / featureVideo on the context""debug
previousPageNav, nextPageNavThe current page is not in the left menuundefineddebug
staticImage, ImageThe url is empty or whitespace""warn
sortedThe first argument is not an array[]warn
breadcrumbsNavThe left menu id resolves to no menu""warn
hasTier, hasFeatureUnrecognized tier or unknown feature namefalse — fail-closedwarn
docsLayoutName, docsColorTheme, docsLayoutTemplate, docsMenuNameNo documentationConfiguration on the pageundefinedinfo
leedConfiguredCSSNo documentationConfiguration on the page""info
processThe page’s tier does not include auto-linkingcontent renders, unlinkedinfo, once per build
readingTimeNo wordCount on the context""error
PaginationLinks, PaginationRangeCalled outside a pagination context""error
staticImage, Image, svgDiagramA required argument is missing, or the diagram name fails validation""error
imageUrl, absoluteImageUrlThe url argument is missing or falsy""error
imageUrl, absoluteImageUrlThe variant argument is omitteda 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.2s

The {{ 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:

HelpersLogger namespace
url, siteUrl, pagesUrl, buildUrlleed:urls:*
Every image and video helperleed:media:*
collection, sorted, filterObject, pageType, user, label, Authors, Labels, the series helpers, the menu-safety helpersleed:collections:*
PaginationLinks, SmartPaginationLinks, PaginationRangeleed:paging:*
pageTypeSlug, readingTime, fullName, CTA, Recommendations, fieldId, turnstileKey, resolveRedirectIndexleed:custom-fields:*
date, latestDateleed:dates:*
docsLayoutName, docsColorTheme, docsLayoutTemplate, docsMenuName, previousPageNav, nextPageNav, breadcrumbsNavleed:menu:navigation:*
hasTier, hasFeatureleed:entitlements:*
processleed:process:*
JSON-LDleed:jsonld:*
stripTags, wrapSubstring, firstTruthy, concat, JSONsafe, templateExists, useCustomTemplate, searchIndexHash, searchDocumentHash, leedConfiguredCSS, formatSecurityName, loggerleed:uncategorized:*

Output that appears as escaped HTML

The symptom is unmistakable: you view source on the built page and find &lt;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:

HelperReturnsCorrect braces
SmartPaginationLinksraw HTML{{{ SmartPaginationLinks true }}}
previousPageNavraw HTML{{{ previousPageNav "Previous" }}}
nextPageNavraw HTML{{{ nextPageNav "Next" }}}
breadcrumbsNavraw HTML{{{ breadcrumbsNav '<i class="home"></i>' }}}
JSON-LDa raw <script> block{{{ JSON-LD }}}
processrendered page content{{{ process content }}}
resolveRedirectIndexplain text, but unescaped{{{ resolveRedirectIndex this ../this }}}

Ten more return a SafeString, so double braces are correct and triple braces buy you nothing:

HelperReturnsCorrect braces
staticImageSafeString <img>{{#staticImage "/x.svg"}}…{{/staticImage}}
svgDiagramSafeString wrapper <div>{{ svgDiagram "svg/chart-1" }}
ImageSafeString <img>{{#Image image "medium"}}…{{/Image}}
FeatureImageSafeString <img>{{#FeatureImage}}…{{/FeatureImage}}
ImageTagSafeString <img>{{ ImageTag item.image }}
FeatureVideoSafeString iframe wrapper{{ FeatureVideo }}
CTASafeString hidden marker{{ CTA "article-cta" }}
RecommendationsSafeString hidden marker{{ Recommendations "recommendations" }}
JSONsafeSafeString JSON string literal{{ JSONsafe data.summary }}
wrapSubstringSafeString <span>{{ wrapSubstring title "vs." "no-bold" }}

That last point generalizes. Escaping is correct inside an HTML attribute — &#x3D; 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 carryWhere that context exists
fullNamethis is a user record with firstName / lastNameInside {{#Authors this}}, inside {{#user id}} used as a block, or an author list item
fieldIdthis is a form fieldInside the field iteration of a form template
turnstileKeythe argument carries leedSiteEnv and deploymentPass @root; it is never read from this
readingTimewordCount (and optionally readingWpm)Any page or list item built from CMS content
FeatureImage, FeatureVideofeatureImage / featureVideoA page or list item whose record has one set
Authors, Labelsauthors / labels on the context you passAny page or collection item — pass this explicitly
Series, MultipartSeries, AnnouncementSeriespageTypeId, labelList and collectionsA real page context; not a bare partial
PaginationLinks, SmartPaginationLinks, PaginationRangeboth pagination and list on the root contextA paginated list template only
docsLayoutName, docsColorTheme, docsMenuName, previousPageNav, nextPageNav, breadcrumbsNav, leedConfiguredCSSdocumentationConfigurationA page bound to a documentation page type
searchIndexHash, searchDocumentHashdocumentationConfiguration.searchIndexIdA documentation page with a search index configured
collection, latestDate, sorted (when fed (collection …))collectionsAny page context
hasTier, hasFeatureentitlements.tierEvery 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:

  1. options.data.root.list.data — a paginated list item
  2. options.data.root — the root page context
  3. options.data — the Handlebars data frame
  4. 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 seeLikely causeWhere it is explained
The build stops with TypeError: Cannot read properties of undefinedAn unguarded lookup — a uuid, a label id or a missing documentationConfigurationFailures that stop the build
The build stops with domain not set before any page rendersThe build environment has no site domainTwo 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 argumentFailures that stop the build
&lt;a href= or visible tag text in the page sourceA raw-HTML helper called with {{ }}Output that appears as escaped HTML
[object Object] in a URL or in the page textAn omitted argument on a helper that does not shiftArguments in the wrong slot
A list renders nothing, no error anywherecollection was given a name that does not exist and returned undefinedCollection, Lookup and Series Helpers
undefined undefined on the pagefullName called outside an author recordFailures caused by the wrong context
Pagination controls are absent from a list pageThe template is not a paginated list, so pagination and list are missingPagination Helpers
Prev/next links are missing on one docs pageThat page is not an item in the left menuDocumentation Navigation Helpers
A menu item vanished from the built navIts pageid: target did not render, so menuItemShouldRender returned falseMenu-Safety Helpers
A feature image is missing on list pages but present on the page itselfThe list item’s data has no featureImage; the helper logs at debug onlyImage and Media Helpers
Internal terms stopped auto-linkingAuto-linking is Starter and up, and the gate is silent apart from one info lineWhen a Feature Is Gated
A “Powered by Leed” badge you cannot removeThe badge gate is a tier check, not a template overrideTier Gating in Templates
A custom docs header or footer is ignoredThe slot requires both the template file and the tierTier Gating in Templates
No structured data on a non-blog page, only an HTML commentJSON-LD emits structured data for the blog page-type slug onlyDate and Content Helpers
A table of contents is emptyNone of the headings carry an id, so toc returned undefinedDocumentation Navigation Helpers
A redirect destination contains &#x3D;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 installedName collisions and missing helpers
A date example from the handlebars-helpers docs does nothingdate is Leed’s block helper; the library’s is momentName 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.

ESC