Distinguishing Collection Sets from Category Collections
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 you’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 problem: archives need to filter
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.
The solution: a set flag
A single boolean field in the collections_photos schema:
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),
}),
});
Defaults to false, so existing category nodes need no changes. Only the actual photo sets carry set: true:
---
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.
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 filter in the archives page is unambiguous:
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.
The archive result
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.
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. The set flag declares intent; publishDate declares readiness. Keeping them separate makes the draft workflow cleaner.