Menu-Safety Helpers

A Leed menu is built in the CMS, and it can point at a page that is still a draft. Nothing stops you from adding the item before the page exists — that is the point, because the nav then lights up on its own the moment the page publishes. But if your template renders the item anyway, a visitor gets a link to nothing. These three helpers are how the shipped menu partials avoid that, and how yours should too.

All three return a boolean, so all three are used in subexpression position — inside an {{#if}}, never on their own. If you have not read How Helpers Work, the subexpression form is explained there; the Helper Index (A–Z) is the way back out if you arrived here by name.

Where the answer comes from

The build keeps a map of every page it actually rendered. It is populated as a side effect of the allPages collection: each item carrying a non-empty page id is recorded with the path it was written to. All three helpers ask the same question of that map — is this page id in it, and does its entry have a path?

The test is rendered, not “exists” and not “published”. That distinction is what makes the check work at all: an unpublished page is simply not part of the build, so there is nothing to find. It also means the answer is only as complete as the build that produced it. A page excluded from a preview build for any other reason answers the same way as a draft.

Because the map is filled while collections are computed, these helpers are safe to call from any template — by the time a template renders, the collection has run.

Takes a whole menu item object and answers “should this <li> exist at all?”.

{{#if (menuItemShouldRender item)}}
<li role="menuitem" data-id="{{ item.menuItemId }}">
  {{> leed/menu/link item=item menuName=menuName }}
</li>
{{/if}}

It returns true in three cases and false in one:

flowchart TD
    A["menuItemShouldRender item"] --> B{"Does the item<br/>have a non-empty submenu?"}
    B -- yes --> R1["render — the subtree<br/>may still be navigable"]
    B -- no --> C{"Does it have an href?"}
    C -- no --> R2["render — a label-only item"]
    C -- yes --> D{"Does the href start<br/>with pageid: ?"}
    D -- no --> R3["render — external URL,<br/>document path, anchor"]
    D -- yes --> E{"Did that page render<br/>in this build?"}
    E -- yes --> R4["render — a real link"]
    E -- no --> R5["omit the item entirely"]

The parent-with-children rule is the part worth internalizing. A folder whose own target is missing still keeps its children reachable, so it survives; only a leaf pointing at a page that did not render is dropped. That is why an unpublished page disappears from the nav without taking its siblings’ folder with it.

hrefIsRenderable — the href-level check

Takes an href string and answers “can this be a working link?”.

{{#if (hrefIsRenderable item.href)}}
<a href="{{ item.href }}" class="leed-menu__item--wrapper leed-menu__item--link">
{{else}}
<div class="leed-menu__item--wrapper leed-menu__item--no-link">
{{/if}}

  <span class="leed-menu__item--text">{{{ item.name }}}</span>

{{#if (hrefIsRenderable item.href)}}
</a>
{{else}}
</div>
{{/if}}

That is the shipped link partial’s shape, and it is worth copying exactly: the helper is called twice, once to open the tag and once to close it, because the two decisions must agree. An item that opens an <a> and closes a </div> produces markup no browser will lay out the way you meant.

The rules are narrower than menuItemShouldRender’s, because there is no item to look at:

  • An empty or missing href returns false. The partial treats that as a folder marker and wraps the label in a <div> instead of a link.
  • A pageid: href returns true only when that page rendered.
  • Anything else — an external URL, a path to a document, a bare path — returns true without a lookup.

The page id is pulled out of the href with the URL parser rather than string surgery, so a pageid: link carrying a query string still resolves: pageid:a02d6d8f?from=nav looks up a02d6d8f. The pageid: prefix is matched case-insensitively.

pageIdExists — the raw check

Takes a bare page id — no pageid: prefix, no query string — and returns whether that page rendered and has a path.

{{#if (pageIdExists relatedPageId)}}
  <a href="pageid:{{ relatedPageId }}">Read the companion piece</a>
{{/if}}

No shipped Leed template uses it; the menu partials reach for the two above. It is here for the case where you hold a page id from your own front matter, a data file or a computed value, rather than from a menu item’s href — which is exactly the situation the example shows, because a pageid: link in body content that has no target degrades to unlinked text rather than disappearing.

What a reader actually sees

Item shapeResult in the published nav
A folder — children, empty hrefRendered. The label is wrapped in a non-link <div>, and its children are rendered under it.
A leaf pointing at a published pageRendered as a real <a>, with the click-tracking attribute the menu partial adds.
A leaf pointing at an unpublished or deleted pageThe whole <li> is omitted. Nothing marks the gap.
Any item with disabled: trueOmitted, whatever its href.

The last row is not these helpers’ doing — the shipped item partial wraps everything in a disabled check before it ever asks menuItemShouldRender. Both filters are silent, so a nav that is missing an entry you know you added is almost always one of those two, and the fix is to publish the target rather than to touch the template. Where items are created, ordered and disabled is covered at Building and Editing a Menu, and a documentation left nav has extra rules of its own at Left Navigation Menu.

If you are rendering menus with {{> leed/menu/builder }} rather than writing your own, all of this is already in place — Menu Partials shows exactly which partial makes which call.

The one place this does not apply

Documentation previous/next links are not filtered at template time. previousPageNav and nextPageNav read the left menu, take the adjacent item and emit its href verbatim — which for a docs page is a pageid: href — with no renderability check of any kind.

What resolves it is the same DOM pass that rewrites every other pageid: link once the HTML is finished. When the target rendered, the href becomes a real path. When it did not, the anchor is replaced by a <span class="page-removed"> carrying the same inner markup, so the reader sees the previous/next label as plain unlinked text in the position where a link belongs.

The same <span class="page-removed"> is what a pageid: link in ordinary body content becomes when its target is missing. What that looks like to a reader, and how to avoid producing one, is at Linking Between Docs Pages.

Reference

HelperFormParameterReturns true whenReturns false whenUsed by Leed’s own templates?
menuItemShouldRendersubexpressionitem — a menu item object, requiredthe item has a non-empty submenu; or it has no href; or its href is not a pageid:; or its pageid: target renderedit is a leaf whose pageid: target did not renderyes — leed/menu/item guards every <li> with it
hrefIsRenderablesubexpressionhref — string or undefined, requiredthe href is not a pageid:; or its pageid: target renderedthe href is empty or missing; or its pageid: target did not renderyes — leed/menu/link calls it twice, to open and to close the tag
pageIdExistssubexpressionpageId — a bare page id string, requiredthat page rendered in this build and has a pathit did notno — provided for ids you hold yourself
ESC