---
title: Distinguishing Collection Sets from Category Collections
description: A single boolean flag in the frontmatter separates navigational collection nodes from publishable photo sets — keeping the archive clean without adding structural complexity.
pubDate: '2026-03-21T12:09:00+01:00'
category: en/development
tags:
  - astro
  - photography
seriesParent: building-the-photo-stream
seriesOrder: 3
---

When I started building the archives page, two kinds of nodes in the photo tree suddenly had to be told apart — and in the filesystem they looked identical. Without an explicit signal, the archive would end up listing navigation aids as if they were finished works.

This post documents the choice I made: a single boolean flag in the frontmatter, why I didn't reuse `publishDate` as a proxy, and how the filter ends up reading.

## The problem

The collection tree has two kinds of nodes that look identical in the filesystem but serve completely different purposes.

A node like `travels/asia/` is purely organisational. It groups cities under a region but isn't something I'd point someone to as a finished work. A node like `travels/asia/chiang-mai/` is a real photo set — curated, dated, and ready to stand on its own.

Both are `index.md` files. Both live in the same hierarchy. Without an explicit signal, there's no clean way to tell them apart.

The archives page aggregates all published content into a chronological timeline — articles, notes, links, and now photo collections. The intent is to show things worth revisiting, not every internal navigation node. Showing `Travels` or `Asia` in the archive alongside `Chiang Mai` would be noise. Those nodes don't have standalone value; they're structure.

## Implementation

**Problem:** The schema for `collections_photos` didn't distinguish navigation nodes from real sets.

**Implementation:** A single boolean field in the schema:

```ts
const collections_photos = defineCollection({
  schema: z.object({
    title: z.string(),
    description: z.string(),
    location: z.string().optional(),
    publishDate: z.coerce.date().optional(),
    draft: z.boolean().default(false),
    coverImage: z.string().optional(),
    order: z.number().int().default(0),
    set: z.boolean().default(false),
  }),
});
```

It defaults to `false`, so existing category nodes need no changes. Only the actual photo sets carry `set: true`:

```yaml
---
title: Chiang Mai
description: Street scenes and quiet moments from Chiang Mai.
location: Chiang Mai, Thailand
publishDate: 2025-10-06T11:04:00+01:00
set: true
---
```

`travels/index.md` and `asia/index.md` stay untouched. No `set` field, no `publishDate` — they remain what they are: navigation aids.

**Solution:** The filter in the archives page is unambiguous:

```ts
const photoSets = (
  await getCollection(
    "collections_photos",
    ({ data }) => data.set && !data.draft && !!data.publishDate,
  )
).map((c) => ({
  title: c.data.title,
  date: c.data.publishDate!,
  url: `/photos/collections/${collectionSlug(c)}`,
  type: "photos" as const,
}));
```

Both conditions must hold: the node must be flagged as a set, and it must have a publish date. Either alone isn't enough.

## Why not infer from `publishDate`?

The `publishDate` field was already present on leaf collections and absent on category nodes, which makes it a tempting proxy. Filter on `!!publishDate` and you'd get the same result today.

The problem is semantic drift. `publishDate` means "when this was published". Using its presence as a proxy for "this is a real set" gives the field two meanings. It also creates a subtle trap — adding a date to a category node for some future purpose would silently promote it to the archive.

An explicit `set: true` says what it means. The two flags also serve different authors in different moments: you might create a collection node and start adding photos over weeks before it's ready to publish. `set` declares intent; `publishDate` declares readiness. Keeping them separate makes the draft workflow cleaner.

## What changed

Photo sets appear in the archive alongside articles and notes, grouped by year, with a `Photo Collection` type badge. Category collections never appear. New collections added in the future are opt-in — the default is invisible until explicitly marked as a set.

## What to take away

- **One boolean beats overloading an existing field.** `set: true` means exactly one thing; repurposing `publishDate` would mean two.
- **Default to the safer value.** With `set` defaulting to `false`, a forgotten flag keeps a node out of the archive rather than accidentally promoting it.
- **Intent and readiness are different axes.** Declaring that something is a set is not the same as declaring it's publishable — `set` and `publishDate` compose cleanly.
- **The filter reads like the requirement.** `data.set && !data.draft && !!data.publishDate` is the whole rule, in one line, and matches how I'd describe it in prose.
