How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

Add a filterable tag facet to a listing

Tags are flat, shared across types and already filterable on every listing route: append /tags/<slug> to the listing path. Build the facet from the tags in use, mark the active one from the route's tag variable, and combine with + or comma.

Context

Context. Every listing route already filters by tag: /blogs/news/tags/summer, /faq/questions/tags/returns, /stores/tags/open-sunday. Two tags joined with + require both, joined with a comma either. On a tag route the template receives tags and, for one tag, tag. A facet is therefore a list of links; the platform does the filtering.

Steps

  1. Tag the records in the admin (turn tags on for the definition or module first if the field is missing).
  2. In the listing template, list the tags to offer: the site's tags from _query, or a curated set from site data.
  3. Link each to the listing's tag route; a blog gives you getTagPostsUrl(), a custom entity listing is /<path>/tags/<slug>.
  4. Mark the active tag from the route's tag variable and offer a "clear" link back to the plain listing.

Template code

Template code
{* a blog listing *}
{$facet = $_query->findBy('Common\Entities\Tag', [], ['name' => 'ASC'], 30)}
<nav class="facet" aria-label="Filter by tag">
  <a href="{$blog->getFullPath()}" class="{if !isset($tag)}active{/if}">All</a>
  {foreach $facet as $t}
    <a href="{$blog->getTagPostsUrl($t)}" class="{if isset($tag) && $tag->getId() == $t->getId()}active{/if}">{$t->getName()|escape}</a>
  {/foreach}
</nav>

{* a custom entity listing at /stores *}
{foreach $facet as $t}<a href="/stores/tags/{$t->getSlug()}">{$t->getName()|escape}</a>{/foreach}
{% set facet = _query.findBy('Common\\Entities\\Tag', {}, {'name': 'ASC'}, 30) %}
<nav class="facet" aria-label="Filter by tag">
  <a href="{{ blog.getFullPath() }}" class="{% if tag is not defined or not tag %}active{% endif %}">All</a>
  {% for t in facet %}
    <a href="{{ blog.getTagPostsUrl(t) }}" class="{% if tag is defined and tag and tag.getId() == t.getId() %}active{% endif %}">{{ t.getName()|e }}</a>
  {% endfor %}
</nav>

{% for t in facet %}<a href="/stores/tags/{{ t.getSlug() }}">{{ t.getName()|e }}</a>{% endfor %}

Why this and not that

Not a query parameter and a template filter: the route form is cached per URL, paginates, and 404s cleanly when nothing matches. Not categories for a facet: a category is structural, one tree per site, and answers "where does this belong"; a tag answers "what is this about" and can be several. Listing every tag on the site offers tags no record in this listing uses; keep the facet to a curated set once the tag list grows.

Go deeper

Was this page helpful?

Your answer goes to the docs team.