← Home

Adding Notes, Links, and Archives

The site now has three additional sections alongside Articles and Photos: Notes, Links, and Archives. Each maps to a distinct content shape and a different reading pattern.

Notes

Notes are short, informal Markdown entries — observations, quick references, things that do not warrant a full article. The collection schema is minimal:

notes: defineCollection({
  schema: z.object({
    title: z.string(),
    publishDate: z.coerce.date(),
    draft: z.boolean().default(false),
  }),
}),

The index at /notes renders all note content inline rather than linking to summaries. Every note is passed through render() at build time and the resulting Content component is embedded directly in the list:

const renderedNotes = await Promise.all(
  notes.map(async (note) => ({
    note,
    Content: (await render(note)).Content,
  })),
);

This means the page is a continuous scroll through all notes — no click required to read them. Individual pages at /notes/[slug] still exist for direct linking and sharing.

Links are a curated log of external URLs worth keeping. The schema captures everything useful about a bookmark without being a database:

links: defineCollection({
  type: "data",
  schema: z.object({
    title: z.string(),
    url: z.string().url(),
    date: z.coerce.date(),
    description: z.string().optional(),
    via: z.string().optional(),
    tags: z.array(z.reference("tags")).default([]),
  }),
}),

Using type: "data" means entries live as .json or .yaml files in src/content/links/ — no Markdown body needed. The via field records the source of a discovery. Tags reference the shared tags collection, which keeps labels consistent across Articles and Links.

The index strips the www. prefix from each URL for a cleaner display:

function getDomain(url: string) {
  try {
    return new URL(url).hostname.replace(/^www\./, "");
  } catch {
    return url;
  }
}

Tag filtering is handled by a static route at /links/tag/[slug], generated from all referenced tags at build time.

Archives

Archives is the one page that does not belong to a single collection. It pulls from Articles, Notes, photo collections, and Links, normalises them into a common shape, and sorts everything newest-first:

const all = [...posts, ...notes, ...photoSets, ...links].sort(
  (a, b) => b.date.valueOf() - a.date.valueOf(),
);

The common shape is just four fields: title, date, url, and type. type drives the colour-coded label on each row — Article, Note, Photo Collection, Link — so the origin of an entry is immediately visible without opening it.

Entries are then grouped by year before rendering:

const byYear = new Map<number, typeof all>();
for (const entry of all) {
  const year = entry.date.getFullYear();
  if (!byYear.has(year)) byYear.set(year, []);
  byYear.get(year)!.push(entry);
}
const years = [...byYear.keys()].sort((a, b) => b - a);

Each year becomes its own <section> with a bold separator. The date column shows only day and month — the year heading makes the year redundant.

What stays the same

All three pages share the same layout skeleton as Articles: BaseLayout for meta and OG tags, Nav and Footer at top and bottom, a centred single-column main capped at 680px. No new layout component was introduced.

Content lives entirely in src/content/. The build reads it, validates it against the schema, and generates static HTML. No runtime queries, no API routes.

← Home