Overview
The Portfolio extension adds gallery pages and template tokens to a project. When the extension is enabled, the project gains a portfolio page and a portfolio categories page, and any template or block rendered on those pages can read portfolio tokens or loop over portfolio categories and media. On a project where the extension is not enabled, portfolio markup is left untouched.
What the extension adds
- A portfolio page, keyed
portfolio(alsoportfolios). - A portfolio categories page, keyed
portfolio-categories(alsoportfolio categories). - Categories with
name,slug,parent_id, andsort_order, so they can nest into a tree. - Media items (projects) with
title,alt_text,description,file_url,mime_type, andmedia_type, each linked to a category.
Scalar tokens
| Token | Meaning | Example |
|---|---|---|
{portfolio.page_url} | Path to the portfolio page. Falls back to /portfolio when no page is configured. | <a href="{portfolio.page_url}">All work</a> |
{portfolio.categories_page_url} | Path used for the categories page. Resolves to the same path as {portfolio.page_url}. | <a href="{portfolio.categories_page_url}">Categories</a> |
{portfolio.media_count} | Number of active media items across all categories. | {portfolio.media_count} projects |
{portfolio.categories_json} | JSON array of every category. | <script type="application/json">{portfolio.categories_json}</script> |
{portfolio.categories_tree_json} | JSON array of root categories, each with a children array. | <script type="application/json">{portfolio.categories_tree_json}</script> |
{portfolio.media_json} | JSON array of active media items. | <script type="application/json">{portfolio.media_json}</script> |
{portfolio.current_category_media_json} | JSON array of media items in the current category. | <script type="application/json">{portfolio.current_category_media_json}</script> |
{portfolio.category_media_json} | Same value as {portfolio.current_category_media_json}. | <script type="application/json">{portfolio.category_media_json}</script> |
Current category tokens
| Token | Meaning |
|---|---|
{portfolio.current_category.id} | Category id. |
{portfolio.current_category.project_id} | Project id that owns the category. |
{portfolio.current_category.name} | Category name. |
{portfolio.current_category.slug} | Category slug. |
{portfolio.current_category.url} | Path to the category view. |
{portfolio.current_category.link} | Alias of {portfolio.current_category.url}. |
{portfolio.current_category.parent_id} | Parent category id, empty for a root category. |
{portfolio.current_category.sort_order} | Sort position. |
Outside a category view, each current category token resolves to an empty string.
Loops
| Loop | What it yields |
|---|---|
{each portfolio_categories as=category} | Categories that contain at least one active media item. |
{each portfolio_media as=media} | Active media items, optionally filtered by category. |
{each portfolio_category_media as=media} | Media items in the current category. |
Add limit=<n> to cap a loop, and close it with {/each} or the labelled form {/each portfolio_categories}.
Example
html
<a href="{portfolio.page_url}">All work ({portfolio.media_count})</a>
<nav>
{each portfolio_categories as=category limit=6}
<a href="{category.url}" class="nav-link{if category.is_current} is-current{/if}">{category.name}</a>
{/each portfolio_categories}
</nav>
<div class="grid grid-cols-3 gap-4">
{each portfolio_media as=media limit=9 order=newest}
<img src="{media.image}" alt="{media.alt_text}">
{/each portfolio_media}
</div>
Conditionals
Portfolio conditionals are scoped to the portfolio name, and loop conditionals are scoped to the loop alias.
| Condition | Runs when |
|---|---|
{if portfolio.current_category} | The page is a category view. |
{if portfolio.media_count} | At least one active media item exists. |
{if portfolio.current_category.name} | The current category has a name. |
{if category.is_current} | The category row is the current one. |
{if media.title} | The media row has a title. |
Next: Categories and projects.

