Channels
Work with custom content types using Channels, scope, sort, and powerful filtering
Channels are Nimbu's custom content types. Use them to manage any structured content: team members, testimonials, case studies, events, locations, FAQs—anything beyond the built-in blogs and products.
What Are Channels?
Channels are:
- Custom content types you define in the Nimbu admin
- Flexible with custom fields (text, rich text, files, references, dates, etc.)
- Queryable with powerful filtering using
{% scope %}and{% sort %} - Enumerable with helper methods like
first,last,random,count
Creating Channels
In the Nimbu admin:
- Go to Channels
- Create a new channel (e.g., "team", "testimonials", "events")
- Define custom fields (name, bio, photo, role, etc.)
- Add entries through the admin interface
Accessing Channel Data
Access channels through the channels drop:
{{ channels.team }} {# Access 'team' channel #}
{{ channels.testimonials }} {# Access 'testimonials' channel #}Channel Properties
| Property | Description |
|---|---|
slug | Channel slug/identifier |
name | Channel display name |
description | Channel description |
attributes | Array of field definitions |
select_options | Options for select fields |
new_entry | Create new entry form |
updated_at | Last update timestamp |
| All custom fields | Access by field name |
Enumeration Helpers
| Method | Description |
|---|---|
all | Entries visible in the current render context |
first | First visible entry |
last | Last visible entry |
latest | Most recently created |
random | Random visible entries |
count, size | Number of visible entries |
empty? | Boolean: no entries |
any? | Boolean: has entries |
Basic Channel Usage
Listing All Entries
<div class="team-members">
{% for member in channels.team.all %}
<div class="team-member">
<img src="{{ member.photo.url | filter, width: '300px' }}" alt="{{ member.name }}">
<h3>{{ member.name }}</h3>
<p class="role">{{ member.role }}</p>
<p class="bio">{{ member.bio }}</p>
</div>
{% endfor %}
</div>ACL-Aware Lists
By default, theme channel loops use the normal channel query. Add use_acl when the list should
respect the logged-in customer and the channel's row-level ACLs:
{% scope use_acl %}
{% for document in channels.documents.all %}
<a href="{{ document.url }}">{{ document.title }}</a>
{% endfor %}
{% endscope %}The ACL check uses the current customer. Row-level grants and denies live on each entry's _acl;
private channel access compares the entry _owner with the customer id. See
Channel Access Control for the API shape.
Publishable Channels
When a channel is marked publishable in the admin, Nimbu adds two managed fields to every entry:
| Field | Description |
|---|---|
_status | Publication state: draft, published, or scheduled |
_publish_at | Datetime used for scheduled publication |
Public theme rendering is publication-aware. Normal channel loops automatically show:
- published entries
- scheduled entries where
_publish_at <= now - legacy entries created before publication fields existed
Drafts and future scheduled entries are hidden from all, first, last, latest, random,
count, size, select_options, pagination, search, RSS, sitemap, and navigation. You do not
need to add a publication scope for normal public lists:
{% sort _publish_at desc %}
{% for article in channels.news.all %}
<article>
<h2><a href="{{ article.url }}">{{ article.title }}</a></h2>
{% if article._publish_at %}
<time datetime="{{ article._publish_at | date: '%Y-%m-%d' }}">
{{ article._publish_at | localized_date: 'long' }}
</time>
{% endif %}
</article>
{% endfor %}
{% endsort %}Use _status scopes only when you intentionally want a management or preview view. Any explicit
scope on _status or _publish_at overrides the automatic public filter, so this can expose drafts
if the template is reachable by visitors:
{% scope _status == 'draft' %}
{% for draft in channels.news.all %}
<p>{{ draft.title }}</p>
{% endfor %}
{% endscope %}For an editorial schedule, query scheduled entries directly:
{% scope _status == 'scheduled' %}
{% sort _publish_at asc %}
{% for article in channels.news.all %}
<p>{{ article.title }} publishes {{ article._publish_at | localized_date: 'long' }}</p>
{% endfor %}
{% endsort %}
{% endscope %}With Limit
<!-- Show first 3 entries -->
{% for testimonial in channels.testimonials.all limit: 3 %}
<blockquote>
<p>{{ testimonial.quote }}</p>
<cite>{{ testimonial.customer_name }}</cite>
</blockquote>
{% endfor %}
<!-- Show random 4 entries -->
{% for item in channels.portfolio.random limit: 4 %}
{% include 'portfolio-card', item: item %}
{% endfor %}Filtering with {% scope %}
The {% scope %} tag filters channel entries using powerful query expressions:
Basic Filtering
<!-- Filter by field value -->
{% scope role == 'Developer' %}
{% for member in channels.team.all %}
<p>{{ member.name }} - {{ member.role }}</p>
{% endfor %}
{% endscope %}
<!-- Filter by boolean -->
{% scope featured == true %}
{% for project in channels.portfolio.all %}
{% include 'project-card', project: project %}
{% endfor %}
{% endscope %}Comparison Operators
| Operator | Comparison Type | Usage Example |
|---|---|---|
== | Equals | role == 'Developer' |
!= | Not equals | workflow_state != 'archived' |
< | Less than | price < 100 |
> | Greater than | views > 1000 |
<= | Less than or equal | stock <= 10 |
>= | Greater than or equal | rating >= 4 |
{% scope price > 5000 %}
<!-- Products over €50 -->
{% endscope %}
{% scope publication_date >= '2024-01-01' %}
<!-- Published in 2024 or later -->
{% endscope %}String Operators
| Operator | String Operation | Usage Example |
|---|---|---|
contains | Contains substring | title contains 'Guide' |
start | Starts with | name start 'Dr.' |
end | Ends with | email end '@example.com' |
regex | Matches regex | sku regex '^PROD-' |
{% scope title contains 'Tutorial' %}
<!-- Articles with 'Tutorial' in title -->
{% endscope %}
{% scope email end '@company.com' %}
<!-- Company email addresses -->
{% endscope %}Array Operators
| Operator | Array Operation | Usage Example |
|---|---|---|
in | Value in array | category_slug in params.categories |
nin | Value not in array | workflow_state nin ['archived', 'cancelled'] |
{% scope workflow_state in ['active', 'featured'] %}
<!-- Active or featured items -->
{% endscope %}
{% scope category_slug in params.selected_categories %}
<!-- User-selected categories -->
{% endscope %}Existence Operators
| Operator | Existence Check | Usage Example |
|---|---|---|
exists | Field exists and is not null | featured_image exists |
{% scope featured_image exists %}
<!-- Only items with images -->
{% endscope %}
{% scope end_date exists %}
<!-- Events with end dates -->
{% endscope %}Logical Operators
Combine conditions with and / or:
{% scope workflow_state == 'active' and featured == true %}
<!-- Active AND featured -->
{% endscope %}
{% scope category == 'news' or category == 'blog' %}
<!-- News OR blog category -->
{% endscope %}
{% scope price > 1000 and price < 5000 and in_stock == true %}
<!-- Mid-range products in stock -->
{% endscope %}Nested Scopes
{% scope category == 'events' %}
<h2>Upcoming Events</h2>
{% scope start_date >= 'now' %}
{% for event in channels.calendar.all %}
<p>{{ event.title }} - {{ event.start_date | localized_date }}</p>
{% endfor %}
{% endscope %}
{% endscope %}Reference Filtering
Filter by referenced content:
<!-- Filter by referenced item ID -->
{% scope author_id == current_author.id %}
<!-- Articles by this author -->
{% endscope %}
<!-- Filter by multiple references -->
{% scope category_ids contains selected_category.id %}
<!-- Items in this category -->
{% endscope %}Real-World Filtering Examples
Event Calendar
<section class="upcoming-events">
<h2>Upcoming Events</h2>
{% scope start_date >= 'now' %}
{% sort start_date asc %}
{% for event in channels.events.all limit: 5 %}
<div class="event">
<h3>{{ event.title }}</h3>
<p class="date">{{ event.start_date | localized_date: 'long' }}</p>
<p class="location">{{ event.location }}</p>
</div>
{% endfor %}
{% endsort %}
{% endscope %}
</section>Team Directory with Filters
<div class="team-directory">
<!-- Department filter from URL params -->
{% if params.department %}
{% scope department == params.department %}
{% for member in channels.team.all %}
{% include 'team-card', member: member %}
{% endfor %}
{% endscope %}
{% else %}
{% for member in channels.team.all %}
{% include 'team-card', member: member %}
{% endfor %}
{% endif %}
</div>Portfolio with Category Filter
{% if params.category %}
{% scope category_slug == params.category %}
{% for project in channels.portfolio.all %}
{% include 'project-card', project: project %}
{% endfor %}
{% endscope %}
{% else %}
{% scope featured == true %}
{% for project in channels.portfolio.random limit: 6 %}
{% include 'project-card', project: project %}
{% endfor %}
{% endscope %}
{% endif %}Sorting with {% sort %}
The {% sort %} tag orders channel entries:
Basic Sorting
{% sort name asc %}
{% for member in channels.team.all %}
<p>{{ member.name }}</p>
{% endfor %}
{% endsort %}
{% sort created_at desc %}
{% for article in channels.blog.all %}
<p>{{ article.title }}</p>
{% endfor %}
{% endsort %}Multi-Field Sorting
{% sort priority desc, name asc %}
{% for task in channels.tasks.all %}
<p>{{ task.priority }} - {{ task.name }}</p>
{% endfor %}
{% endsort %}Combining Scope and Sort
{% scope featured == true %}
{% sort publication_date desc %}
{% for article in channels.blog.all %}
<article>
<h2>{{ article.title }}</h2>
<p class="date">{{ article.publication_date | localized_date }}</p>
<p>{{ article.excerpt }}</p>
</article>
{% endfor %}
{% endsort %}
{% endscope %}Channel Entry Properties
Every channel entry has these standard properties:
| Property | Description |
|---|---|
id | Unique entry ID |
created_at | Creation timestamp |
updated_at | Last update timestamp |
title | Entry title |
url | Entry detail page URL (if routable) |
_permalink | Custom permalink |
_slug | URL slug |
_seo_title | SEO title override |
_seo_description | SEO description |
_status | Publication state on publishable channels |
_publish_at | Publication datetime on publishable channels |
| All custom fields | By field name |
Plus all your custom fields are accessible directly:
{{ entry.name }}
{{ entry.bio }}
{{ entry.photo.url }}
{{ entry.start_date }}
{{ entry.featured }}Working with Reference Fields
Reference fields link to other content:
References to publishable channels are filtered in public rendering. If a related entry is a draft or a future scheduled entry, Nimbu hides it from nested drops and title expansion. Explicit publication scopes can still include unpublished related entries, so use them only on protected preview or management pages.
<!-- Author reference -->
{% if article.author %}
<p class="byline">
By {{ article.author.name }}
{% if article.author.photo %}
<img src="{{ article.author.photo.url | filter, width: '50px' }}" alt="{{ article.author.name }}">
{% endif %}
</p>
{% endif %}
<!-- Multiple references -->
{% if project.team_members %}
<div class="project-team">
{% for member in project.team_members %}
<div class="team-member">
<img src="{{ member.photo.url | filter, width: '100px' }}" alt="{{ member.name }}">
<p>{{ member.name }}</p>
</div>
{% endfor %}
</div>
{% endif %}Pagination
Use {% paginate %} for large channel listings:
{% paginate channels.blog.all by 10 %}
{% for article in paginate.collection %}
<article>
<h2><a href="{{ article.url }}">{{ article.title }}</a></h2>
<p>{{ article.excerpt }}</p>
</article>
{% endfor %}
{{ paginate | default_pagination }}
{% endpaginate %}Channel Detail Pages
Create detail pages by routing to templates:
Channel Entry Template
templates/team_member.liquid:
<div class="team-member-page">
<div class="container">
<article class="member-details">
<header>
{% if entry.photo %}
<img src="{{ entry.photo.url | filter, width: '400px' }}" alt="{{ entry.name }}">
{% endif %}
<h1>{{ entry.name }}</h1>
<p class="role">{{ entry.role }}</p>
</header>
<div class="member-bio">
{{ entry.bio }}
</div>
{% if entry.email %}
<p><a href="mailto:{{ entry.email }}">{{ entry.email }}</a></p>
{% endif %}
{% if entry.social_links %}
<ul class="social-links">
{% for link in entry.social_links %}
<li><a href="{{ link.url }}">{{ link.platform }}</a></li>
{% endfor %}
</ul>
{% endif %}
</article>
<!-- Related team members -->
<aside class="related-members">
<h2>More Team Members</h2>
{% scope role == entry.role %}
{% for member in channels.team.random limit: 3 %}
{% unless member.id == entry.id %}
{% include 'team-card', member: member %}
{% endunless %}
{% endfor %}
{% endscope %}
</aside>
</div>
</div>Forms for Channels
Create submission forms for channels:
{% form channels.contact, class: 'contact-form' %}
{% error_messages_for form_model %}
{% input 'name', label: 'Name', required: true %}
{% input 'email', as: 'email', label: 'Email', required: true %}
{% input 'phone', label: 'Phone' %}
{% text_area 'message', label: 'Message', rows: 5, required: true %}
{% submit_tag 'Send Message', class: 'btn btn-primary' %}
{% endform %}See Forms for complete form documentation.
Advanced Patterns
Dynamic Filtering
<form action="{{ url.current_path }}" method="get" class="filter-form">
<select name="category">
<option value="">All Categories</option>
{% for cat in channels.projects.select_options.category %}
<option value="{{ cat }}" {% if params.category == cat %}selected{% endif %}>
{{ cat }}
</option>
{% endfor %}
</select>
<select name="status">
<option value="">All Statuses</option>
<option value="active" {% if params.status == 'active' %}selected{% endif %}>Active</option>
<option value="completed" {% if params.status == 'completed' %}selected{% endif %}>Completed</option>
</select>
<button type="submit">Filter</button>
</form>
<!-- Apply filters -->
{% assign has_filters = false %}
{% if params.category or params.status %}
{% assign has_filters = true %}
{% endif %}
{% if has_filters %}
{% capture scope_expr %}
{% if params.category %}category == '{{ params.category }}'{% endif %}
{% if params.category and params.status %} and {% endif %}
{% if params.status %}status == '{{ params.status }}'{% endif %}
{% endcapture %}
{% scope {{ scope_expr }} %}
{% for project in channels.projects.all %}
{% include 'project-card', project: project %}
{% endfor %}
{% endscope %}
{% else %}
{% for project in channels.projects.all %}
{% include 'project-card', project: project %}
{% endfor %}
{% endif %}Counting & Grouping
<!-- Count by category -->
{% assign categories = channels.blog.all | group_by: 'category' %}
{% for category_group in categories %}
<p>{{ category_group.name }}: {{ category_group.items.size }} articles</p>
{% endfor %}
<!-- Group by year -->
{% assign by_year = channels.events.all | group_by_exp: 'event', 'event.start_date | date: "%Y"' %}
{% for year_group in by_year %}
<h3>{{ year_group.name }}</h3>
{% for event in year_group.items %}
<p>{{ event.title }}</p>
{% endfor %}
{% endfor %}Best Practices
1. Use Scope for Filtering
Always use {% scope %} instead of {% if %} inside loops for better performance:
<!-- ❌ Bad: Filter in loop -->
{% for item in channels.news.all %}
{% if item.featured %}
{{ item.title }}
{% endif %}
{% endfor %}
<!-- ✅ Good: Filter with scope -->
{% scope featured == true %}
{% for item in channels.news.all %}
{{ item.title }}
{% endfor %}
{% endscope %}2. Cache Channel Queries
{% cache 'featured-team', expires_in: 3600 %}
{% scope featured == true %}
{% for member in channels.team.all %}
{% include 'team-card', member: member %}
{% endfor %}
{% endscope %}
{% endcache %}3. Use Pagination for Large Sets
{% paginate channels.blog.all by 20 %}
{% for article in paginate.collection %}
<!-- Article content -->
{% endfor %}
{{ paginate | default_pagination }}
{% endpaginate %}4. Check for Existence
{% if channels.team.any? %}
<!-- Show team -->
{% else %}
<p>No team members yet.</p>
{% endif %}5. Use References
Instead of storing names/IDs manually, use reference fields to link content and maintain data integrity.
Troubleshooting
Scope Not Working
- Check field names match exactly (case-sensitive)
- Ensure operators are correct (
==not=) - Verify data types (strings need quotes, booleans don't)
Empty Results
- Check if entries exist:
{{ channels.channel_name.count }} - Verify scope conditions are correct
- Test without scope to see all entries
Performance Issues
- Use
limitto restrict results - Add caching with
{% cache %} - Use pagination for large sets
Next Steps
- Pages - Use channels in editable references
- Forms - Create channel submission forms
- Filters - Arrays - Advanced filtering with
where,group_by - Global Variables - Complete channels drop reference
Build powerful custom content experiences with Channels!