Operators

Pipeline, optional chaining, and null coalescing operators

3 min read 677 words

Kida adds modern operators for data transformation and null-safe access.

Pipeline Operator (|>)

The pipeline operator chains filters left-to-right:

{% let recent_posts = site.pages
  |> where('type', 'blog')
  |> where('draft', false)
  |> sort_by('date', reverse=true)
  |> take(10) %}

Pipeline vs Pipe

Both|and|>compile to identical code:

{# These are functionally identical #}
{{ items |> where('published', true) |> sort_by('date') |> take(5) }}
{{ items | where('published', true) | sort_by('date') | take(5) }}
Syntax Convention
| Inline expressions, Jinja2 familiarity
|> Multiline pipelines, functional style

Note

You cannot mix|and|>in the same expression. Pick one per chain.

Multiline Pipelines

Add inline comments to explain each step:

{% let recent_posts = site.pages
  |> where('type', 'blog')        {# Only blog posts #}
  |> where('draft', false)        {# Exclude drafts #}
  |> sort_by('date', reverse=true) {# Newest first #}
  |> take(10) %}                   {# Limit to 10 #}

Filter Name Mapping

Common Jinja2 patterns and their Kida equivalents:

Jinja2 Filter Kida Filter Description
selectattr('key') where('key', true) Boolean filter
selectattr('key', 'eq', val) where('key', val) Equality filter
rejectattr('key') where_not('key', true) Inverse boolean
sort(attribute='key') sort_by('key') Sort by attribute
batch(n) | first take(n) Get first n items (both return a list of n items;take()avoids grouping overhead)
groupby('key') group_by('key') Group by attribute

Note

Aboutbatch(n) | firstvstake(n): While both return the first n items,batch(n)groups all items into batches first, thenfirstextracts the first batch. Thetake(n)filter directly takes the first n items without grouping, making it more efficient for this use case.

Optional Chaining (?.)

Safe navigation through potentially null values:

{{ user?.profile?.name ?? 'Anonymous' }}
{{ page?.metadata?.author?.avatar }}
{{ config?.social?.twitter?.handle }}

Compare to Jinja2's defensive coding:

{{ user.profile.name if user and user.profile and user.profile.name else 'Anonymous' }}

Null Coalescing (??)

Concise fallback for null/undefined values:

{{ page.subtitle ?? page.title }}
{{ user.nickname ?? user.name ?? 'Guest' }}
{{ config.theme ?? 'default' }}

Precedence Gotcha

The??operator has lower precedence than|, so filters bind to the fallback:

{# ❌ Parses as: items ?? ([] | length) — returns list, not length! #}
{{ items ?? [] | length }}

{# ✅ Correct: use | default() for filter chains #}
{{ items | default([]) | length }}

Rule: Use??for simple direct output. Use| default()when applying filters after the fallback.

Combined Usage

{{ page?.metadata?.image ?? site?.config?.default_image ?? '/images/placeholder.png' }}

Best Practices

Filter Early, Limit Early

{# ✅ Good: Filter before sorting #}
{% let posts = site.pages
  |> where('type', 'blog')
  |> where('draft', false)
  |> sort_by('date')
  |> take(10) %}

{# ❌ Less efficient: Sort everything first #}
{% let posts = site.pages
  |> sort_by('date')
  |> where('type', 'blog')
  |> take(10) %}

Use Multiline for Complex Chains

{# 3+ filters: use multiline #}
{% let posts = site.pages
  |> where('type', 'blog')
  |> sort_by('date', reverse=true)
  |> take(5) %}

{# Simple chains: inline is fine #}
{{ items |> take(3) }}

Complete Example

{% extends "baseof.html" %}

{% let post = page %}
{% let author = post?.metadata?.author ?? site?.config?.default_author ?? 'Anonymous' %}
{% let post_image = post?.metadata?.image ?? post?.metadata?.cover %}
{% let related_posts = site.pages
  |> where('type', 'blog')
  |> where('tags', post.tags | first)
  |> where_not('_path', post._path)
  |> sort_by('date', reverse=true)
  |> take(3) %}

{% block content %}
<article class="blog-post">
  {% if post_image %}
    <img src="{{ post_image }}" alt="{{ post.title }}">
  {% end %}

  <header>
    <h1>{{ post.title }}</h1>
    <span>By {{ author }}</span>
  </header>

  {{ post.content | safe }}

  {% if related_posts | length > 0 %}
    <aside class="related">
      <h2>Related Posts</h2>
      {% for item in related_posts %}
        <a href="{{ item.url }}">{{ item.title }}</a>
      {% end %}
    </aside>
  {% end %}
</article>
{% endblock %}