Skip to content

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

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

ParameterMeaning
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=trueRender only root categories. Accepts true, yes, on, or 1.

Category navigation

html
<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

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

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

ParameterMeaning
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

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

html
{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.

html
{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.

html
{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}
TokenResolves 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.
html
<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.

html
<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}.