Five helpers, doing two jobs. Four of them build an address: this page’s, the site’s, the preview host’s, or one of those with a query string on the end. The fifth writes the site’s whole generated _redirects file.
Three of them need no context at all. Only url needs to know what page it is on, and only leedRedirects reaches into the build’s collections. All five assume the mechanics on How Helpers Work — particularly the trailing options object, which has sharp consequences on two of the five.
url — this page’s address
url takes one optional parameter and gives you the current page’s address, absolute by default:
{{ url }} → https://example.com/blog/my-post/
{{ url "relative" }} → /blog/my-post/The address itself comes from the page context — page.url, falling back to page.filePathStem. Three things then happen to it:
- An
/indexsegment truncates the path. The path is cut at the first occurrence of/index, keeping the slash, which is what turns/blog/my-post/index.htmlinto/blog/my-post/. - A value already starting with
httppasses through unchanged, so an absolute URL is never prefixed twice. - Anything else gets
https://<your domain>prepended when absolute was requested.
When no URL can be extracted at all — url called somewhere with no page on the context — the helper returns undefined and logs Could not extract a url from the provided context! under the urls logger. In a template that renders as nothing, so watch the build console rather than the page.
The canonical call sites are the feed templates, which need both forms in the same file: the Atom feed uses {{ url }} for <id> and <link rel="alternate"> and {{ url "relative" }} where a path is wanted. Feeds, Sitemaps and robots.txt covers what those templates emit.
siteUrl and pagesUrl — the two hostnames
Neither takes a parameter and neither needs a page context. siteUrl returns your site’s origin and pagesUrl returns the Workers preview host:
{{ siteUrl }} → https://example.com
{{ pagesUrl }} → https://example-com-preview.workers.devNeither has a trailing slash, which is why the shipped robots.hbs writes the separator itself:
sitemap: {{ siteUrl }}/sitemap.xmlBoth are read from the build environment, so they differ between a preview build and a production one. Leed’s _headers template turns that into a real behavior: on a preview build it marks both hosts noindex, and on a production build only the preview host.
{{#if @root/leedSiteEnv.env.preview}}
{{ siteUrl }}/*
X-Robots-Tag: noindex
{{#unlessEq (siteUrl) (pagesUrl) }}
{{ pagesUrl }}/*
X-Robots-Tag: noindex
{{/unlessEq}}
{{else}}
{{ pagesUrl }}/*
X-Robots-Tag: noindex
{{/if}}Two things in that snippet are worth stealing. The parentheses around (siteUrl) and (pagesUrl) are required — as subexpressions they are evaluated and their results compared, where the bare names would be treated as missing context variables. And which hostname siteUrl resolves to is decided by the branch the build ran from; Preview Site vs Live Site explains which branch produces which.
pagesUrl is marked Templates: Leed in its source docblock, and leed-headers.hbs is the only shipped template that calls it. It is documented here so that reading that template makes sense, not because a customer site normally needs the preview hostname — if you find yourself linking to it from page content, you almost certainly want siteUrl.
A build-config failure, not a template failure
The URL plugin validates its configuration when it is installed, not when a template renders:
[urls] domain or pagesDomain not set
Error: domain or pagesDomain not setbuildUrl — appending a query string
Two parameters, both required, and it chooses the separator for you:
{{ buildUrl "/pricing/" "utm_source=footer" }}
→ /pricing/?utm_source=footer
{{ buildUrl "/pricing/?plan=growth" "utm_source=footer" }}
→ /pricing/?plan=growth&utm_source=footerThe rule is exactly “does the first argument contain a ? anywhere” — not “does it end with a query string” — so a URL with a ? in a fragment or a path also gets &.
The shipped use is the popup-video component, which appends an autoplay flag to a video URL only when the modal opens:
:src="showModal ? '{{ buildUrl videoSrc "autoplay=true" }}' : ''"Omitting the second argument is not safe either: the options object lands in params and you get ?[object Object] appended. Omitting both is worse — url.indexOf is called on the options object, which does not have that method, and the build stops with a TypeError.
leedRedirects — the generated _redirects file
leedRedirects returns the complete body of the site’s _redirects file, built in one pass. It takes no arguments: it reads favicon and collections off the render context and the page-type list from the build. Leed’s own leed-redirects.hbs is nothing but the one call, in triple braces:
{{{ leedRedirects }}}The file holds four families of rule, written in three blocks separated by a blank line: the favicon rule, every page alias, then everything scoped to a page type — every page type’s root redirect first, then the splat rules for every page type’s own aliases. Each rule is one tab-separated line:
/favicon.ico https://example.com/favicon.png 302
/old-post/ /blog/my-post/ 301
/blog/ /blog/the-latest-post/ 302
/blog/index.html /blog/the-latest-post/ 302
/articles/* /blog/:splat 301Those tabs are the field separator Cloudflare’s _redirects format expects. A page type’s root rule comes from its own documentation startingPage when that names a page in the set that has an output location — a single 302 to that page’s URL, with no index.html companion — and otherwise from its redirectIndex setting, which writes the pair shown above. When startingPage resolves but a page already renders at the root, no root rule is written at all, not even the redirectIndex pair. The redirectIndex fallback itself makes no such check.
redirectIndex value | Resolves to | Notes |
|---|---|---|
first | The URL Eleventy computed for the first page in the set | The :unpaged collection is ordered by ascending publishedAt, tie-broken by URL, so first is the oldest page, not the newest. Because it is the page’s real URL, a docs page in a nested menu folder resolves correctly |
last | The URL Eleventy computed for the last page in the set | Same ordering, so last is the most recently published. A page of a noRender page type resolves to the paginated page that renders it |
A path starting with / | The path itself | A trailing / is appended when missing. The value is matched lower-cased but emitted with its original casing |
A URL containing :// | The URL, normalized through new URL() | Use this to send an index off-site. A value that does not parse logs an error and writes no rule |
The match is case-insensitive, so First and FIRST both work.
Three ways a page type gets no root rule. A page type with no redirectIndex (and no usable startingPage) simply has none — the normal case for most page types. A first or last page with no output location anywhere is skipped silently. And first or last against an empty set, or a value that matches none of the four shapes, logs an error naming the value and the collection size and writes nothing:
leed:redirect-index:error redirectIndex: 'newest' has an issue; 12 items in collectionredirectIndex is a page-type setting, chosen in the CMS at Configuring a Page Type; this helper only renders the choice. The page aliases in the file’s middle block are the redirects Leed writes when a published page’s URL changes, covered in Aliases and Redirects.
Reference
| Helper | Form | Parameters | Requires on context | Returns | Braces |
|---|---|---|---|---|---|
url | inline | type — string, optional. Only the literal "relative" is recognized; everything else, including the options object, means absolute | A page context carrying page.url or page.filePathStem | A string, or undefined with a log.warn | {{ }} |
siteUrl | inline | none | nothing | https://<domain>, no trailing slash | {{ }} |
pagesUrl | inline | none | nothing | https://<pagesDomain>, no trailing slash | {{ }} |
buildUrl | inline | url — string, required. params — string, required. No encoding is applied to either | nothing | The two joined by ? or & | {{ }} |
leedRedirects | inline | none | collections and, optionally, favicon on the render context | The complete _redirects file body as a plain string | {{{ }}} |
None of the five returns a SafeString, and leedRedirects is the one where that matters: nothing in its output is HTML. Leed’s own leed-redirects.hbs calls it with {{{ }}}, and an override of that template must too: through {{ }}, Handlebars rewrites = to = and & to &, and a destination carrying a query string then redirects somewhere wrong. The full picture of which helpers escape and which do not is on Helper Gotchas and Failures.
url, siteUrl, pagesUrl and buildUrl can also be called from CMS page content, not just from a template file, because process expands partials against the page context before the content is written out — see Date and Content Helpers.