Avoiding duplicates on complicated homepages
Making #contentFor and block deduplicate a page with tag-based content sections
Many themes have a big hero section up top, followed by some later sections with posts that are selected based on their tags. (Examples include Ghost official theme Headline, plus nearly every theme in the "News" section of the theme directory.) Inevitably the client doesn't want duplicates between the top section and the lower sections, and wants lots of control over the top section. This is where things get complicated. Here's a better way to avoid duplicates than I've seen in many themes.
Keeping hero posts out of the tag sections further down a Ghost homepage
The obvious fix is to exclude the hero's post ids in each section's filter. The catch is how {{#get}} resolves the placeholders inside its filter string. It looks them up against the current Handlebars context only, with its own simple path lookup. Inside a {{#foreach}} over tags, the context is the tag, so {{slug}} works, but anything computed elsewhere on the page is out of reach. There is no ../ allowed in filters. And even if you solve the ../ with nested partials (more below), having to nest everything within the #get that produces the hero section can get messy and confusing fast.
The pieces
Three things make it work, and they have to nest in a particular way.
1. The hero records its post ids with contentFor. Ghost has the contentFor and block pair, which many themes use to get information "up" to default.hbs from within a template. But here, we turn things around a bit. The hero partial writes the ids into a named block, and it writes them as a complete filter clause.
{{!-- partials/hero.hbs --}}
{{!-- This part sets the "hero_posts_filter" block. It renders nothing here. --}}
{{#if posts}}{{#contentFor "hero_posts_filter"}}+id:-[{{#foreach posts limit="3"}}{{#unless @first}},{{/unless}}{{id}}{{/foreach}}]{{/contentFor}}{{/if}}
{{!-- This is the actual loop that handles the cards in the hero section --}}
<section class="hero">
{{#foreach posts limit="3"}}
{{> post-card}}
{{/foreach}}
</section>
My hero has three posts, retrieved with a {{#get}} request not shown in this snippet. (It might be filtered by tag, or featured status, or publication date, or some combination. Whatever we're using, we're inside that context.
If the hero renders nothing, the block resolves to an empty string and the filter is unchanged. That's important for part 2, below.
2. The template reads the block back as a partial parameter. block is a normal helper, so it works as a subexpression in a partial call. Hash parameters on a partial become properties of the partial's context, which is exactly where {{#get}} looks. The hero has to appear earlier in the template than the sections so the block is filled before it is read:
{{!-- home.hbs --}}
{{> hero}}
{{#get "tags" filter="slug:[{{@custom.section_tags}}]" limit="10"}}
{{#foreach tags}}
{{> tag-section extra_filter=(block "hero_posts_filter")}}
{{/foreach}}
{{/get}}
3. The tag-section partial reconstitutes the filter. Because the partial was called with the tag as context, {{slug}} and {{extra_filter}} are both plain properties of this, and the filter can use them side by side. Inside the get, the context switches to the posts, so the tag's own fields need ../:
{{!-- partials/tag-section.hbs --}}
{{#get "posts" filter="primary_tag:{{slug}}{{extra_filter}}" include="tags,authors" limit="4"}}
{{#if posts}}
<section class="tag-section">
<h2>{{../name}}</h2>
{{#foreach posts}}
{{> post-card}}
{{/foreach}}
<a href="{{../url}}">More {{../name}}</a>
</section>
{{/if}}
{{/get}}
Why the nesting is the whole trick, and the best trick
The nested partials are required. Don't simplify. The partial call turns the block value into a context property, so the #get can use it for filtering.
So what about duplicates between tag sections?
Easiest fix, and visible above: I'm filtering by primary tag, not tag. That's a guaranteed no duplicates between tag sections, since a post has exactly one primary tag.
A more complicated fix would require passing in the ids of the first tag section into the second tag section. It is possible to write this sort of recursion into Ghost, but that's more complexity than I needed here.
Hey, before you go... If your finances allow you to keep this tea-drinking ghost and the freelancer behind her supplied with our hot beverage of choice, we'd both appreciate it!