Skip to content

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 (also portfolios).
  • A portfolio categories page, keyed portfolio-categories (also portfolio categories).
  • Categories with name, slug, parent_id, and sort_order, so they can nest into a tree.
  • Media items (projects) with title, alt_text, description, file_url, mime_type, and media_type, each linked to a category.

Scalar tokens

TokenMeaningExample
{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

TokenMeaning
{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

LoopWhat 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.

ConditionRuns 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.