Categories and projects
Portfolio content is split into categories and media items (projects). This page covers the category and media loops, the fields each row exposes, the parameters a loop accepts, and the page URLs the tokens resolve to.
Looping over categories
{each portfolio_categories as=category} walks the category list in sort order. Only categories that contain at least one active media item are rendered, so an empty category does not leave a blank tile behind.
Category fields
| Field | Meaning |
|---|---|
{category.id} | Category id. |
{category.project_id} | Project id that owns the category. |
{category.name} | Category name. |
{category.slug} | Category slug. |
{category.url} | Path to the category view. |
{category.link} | Alias of {category.url}. |
{category.sort_order} | Sort position. |
{category.parent_id} | Parent category id, empty for a root category. |
{category.index} | Position in the loop, starting at 1. |
{category.is_current} | 1 when the row is the current category, otherwise empty. |
{category.selected} | Same value as {category.is_current}. |
Category loop parameters
| Parameter | Meaning |
|---|---|
limit=<n> | Render at most n categories. |
slug=<value> or category_slug=<value> | Render only the category with that slug. |
parent_id=<id> | Render only the children of that category id. |
root_only=true | Render only root categories. Accepts true, yes, on, or 1. |
Category navigation
<nav class="portfolio-nav">
{each portfolio_categories as=category limit=8}
<a href="{category.url}" class="nav-link{if category.is_current} is-current{/if}">
{category.name}
</a>
{/each portfolio_categories}
</nav>
Root categories only
{each portfolio_categories as=category root_only=true}
<h2>{category.name}</h2>
{/each portfolio_categories}
Looping over projects (media)
Use {each portfolio_media as=media} for a full gallery, and {each portfolio_category_media as=media} for the media of the category currently being viewed. Both loops expose the same fields.
Media fields
| Field | Meaning |
|---|---|
{media.id} | Media id. |
{media.project_id} | Project id that owns the media. |
{media.category_id} | Id of the linked category. |
{media.category_name} | Name of the linked category. |
{media.category_slug} | Slug of the linked category. |
{media.title} | Project title. |
{media.alt_text} | Alt text. |
{media.description} | Description. |
{media.url} | File URL. |
{media.link} | Alias of {media.url}. |
{media.src} | Alias of {media.url}. |
{media.image} | Alias of {media.url}. |
{media.file_url} | File URL. |
{media.mime_type} | MIME type of the file. |
{media.media_type} | Media kind, for example image or video. |
{media.status} | Record status. |
{media.sort_order} | Sort position. |
{media.index} | Position in the loop, starting at 1. |
Media loop parameters
| Parameter | Meaning |
|---|---|
limit=<n> | Render at most n media items. |
order=<value> or sort=<value> | recent, latest, or newest orders by newest first. Any other value uses the configured sort order. |
category_slug=<value> or category=<value> | Render only media in that category. |
Project grid
<div class="grid grid-cols-2 md:grid-cols-3 gap-6">
{each portfolio_media as=media limit=12 order=newest}
<figure class="project-card">
<img src="{media.image}" alt="{media.alt_text}">
<figcaption>
<span class="project-title">{media.title}</span>
<span class="project-category">{media.category_name}</span>
</figcaption>
</figure>
{/each portfolio_media}
</div>
Media for the current category
On a category view, portfolio_category_media is already scoped to the category being shown.
{each portfolio_category_media as=media limit=8}
<img src="{media.image}" alt="{media.alt_text}">
{/each portfolio_category_media}
Projects inside a category loop
Inside a portfolio_categories loop you can nest a media loop for each category. Pass the row slug through category or category_slug using {category.slug}, and it resolves to the row being rendered.
{each portfolio_categories as=category}
<section class="category-section">
<h2><a href="{category.url}">{category.name}</a></h2>
<div class="grid grid-cols-3 gap-4">
{each portfolio_media category={category.slug} as=media limit=3}
<img src="{media.image}" alt="{media.alt_text}">
{/each portfolio_media}
</div>
</section>
{/each portfolio_categories}
portfolio_category_media nested inside the same loop scopes to the row automatically.
{each portfolio_categories as=category limit=4}
<h2>{category.name}</h2>
{each portfolio_category_media as=media limit=3}
<img src="{media.image}" alt="{media.alt_text}">
{/each portfolio_category_media}
{/each portfolio_categories}
Page URLs and links
| Token | Resolves to |
|---|---|
{portfolio.page_url} | Path to the portfolio page, for example /portfolio. |
{portfolio.categories_page_url} | Path for the categories page. |
{category.url} | Path to a category view, built from the category page path and the row slug. |
{category.link} | Alias of {category.url}. |
{portfolio.current_category.url} | Path to the current category view. |
{portfolio.current_category.link} | Alias of {portfolio.current_category.url}. |
{portfolio.current_category.slug} | Current category slug, useful as a media filter value. |
<nav class="breadcrumbs">
<a href="{portfolio.page_url}">Portfolio</a>
{if portfolio.current_category.name}
<span>/</span>
<a href="{portfolio.current_category.url}">{portfolio.current_category.name}</a>
{/if}
</nav>
JSON payloads
The JSON tokens carry the same data the loops render, ready for client-side use.
<script type="application/json" id="portfolio-categories">
{portfolio.categories_json}
</script>
<script type="application/json" id="portfolio-projects">
{portfolio.media_json}
</script>
For the media of one category, use {portfolio.category_media_json} or {portfolio.current_category_media_json}.

