~/nicholaslennox/blog

Part 3 of 3 in Foundations

How this blog is organised

Contents
  1. 1. Why a blog needs structure
  2. 2. Three ways to group posts
  3. 3. When two groupings do one job
  4. 4. Storing the structure in folders
  5. 5. Checking the content before it's published
  6. 6. From structure to pages
  7. 7. What this structure doesn't do
  8. 8. Applying this to your own projects
  9. 9. Summary
  10. 10. Sources

This post covers how the posts on this blog are organised into collections, series and tags, and how that structure is stored in folders and checked before anything is published. It also covers how the first version of the structure had two groupings doing the same job, and how that was fixed. The same approach works for documentation, course material, or any other set of related pages.

New terms get a short plain-English explanation in brackets.

Goals

  1. Tell collections, series and tags apart.
  2. Spot when two groupings in a content structure are doing the same job.
  3. Describe how a folder layout can hold a site's content structure.
  4. Explain why each fact about a post should be stored in one place only.
  5. Describe how a build step can check content and stop a broken post from being published.
  6. Use generateStaticParams to build a page for every collection, series, tag and post.

1. Why a blog needs structure

A blog with five posts can be one list, newest first. A blog with fifty posts, on subjects as different as Cloudflare configuration and teaching beginners to code, can't. Readers arrive looking for different things:

  • some want everything on one subject,
  • some want to work through a topic in order, one post after another,
  • and some want every post that mentions a particular tool.

One list can't serve all three. Each of those needs is a different way of grouping the same posts, and this blog has one grouping for each.

2. Three ways to group posts

2.1 Collections

A collection is a broad subject, such as Development or Building this site. Every post belongs to exactly one collection.

Collections are the top level of the blog. They're listed first in the sidebar, and there are only a few of them: Building this site, Development, AI & systems and Teaching. A reader can pick a subject without being shown everything else.

2.2 Series

A series is a numbered sequence of posts inside a collection, meant to be read in order. Each post in it is a part: part 1, part 2, part 3. A post can be in one series, or none.

This post is part 3 of Foundations, a series in the Building this site collection. Part 1 introduced the site, part 2 covered how it's built and deployed, and this part covers how the blog is organised. Each part builds on the ones before it, so the order matters more than the dates.

A series is listed under its collection in the sidebar, not beside it. That keeps the top level short. With every series listed at the top level, the sidebar would grow with every new series. With series nested under their collection, the top level only grows when a whole new subject is added.

2.3 Tags

A tag is a short label, such as nextjs or cloudflare. A post can have as many tags as it needs, and tags cross collections: a post about Next.js in Development and a post about this site's Next.js setup in Building this site can both be tagged nextjs.

Tags are for finding everything that mentions a tool or idea, whatever subject it was written under.

2.4 Comparing them

The three groupings differ on two questions: how many a post can have, and whether order matters.

Grouping How many per post Ordered? Answers
Collection Exactly one No, newest first "What subject is this?"
Series One or none Yes, by part number "What do I read next?"
Tag Any number No, newest first "What else mentions this?"

Those two questions are worth asking about any grouping you design. If two groupings give the same answers to both, they may be doing the same job.

3. When two groupings do one job

The blog didn't start with this structure. The first version had sections for broad subjects, and collections for sequences of posts. There was a section called AI & systems, and a collection called Ideas into systems, described as "a series on using AI to turn your ideas into systems".

Looking at the two side by side in the sidebar, I noticed that the Ideas into systems collection was really the same thing as the AI & systems section. Both were "posts about a subject". The two names described one idea, and it wasn't clear which one a new post should go in.

If the person writing the posts can't say clearly how two groupings differ, readers won't be able to either. So the two were merged:

  • sections became collections, the top level,
  • and the old idea of a collection became a series, nested inside a collection.

The two questions from the comparison table above show why the new version works. A collection is exactly one per post and unordered. A series is one or none per post and ordered by part. They give different answers to both questions, so each has a job the other can't do.

When two groupings keep getting confused, check whether they're doing the same job.

4. Storing the structure in folders

4.1 The folder layout

Each post is a Markdown file, and a script reads them all during the build, as covered in Building, deploying and CI/CD on Cloudflare Workers. Where each file sits decides how the post is organised:

content/blog/
  structure.json                        titles and descriptions
  building-this-site/                   a collection
    foundations/                        a series in that collection
      01-building-in-public.md          part 1
      02-idea-to-pipeline.md            part 2
      03-how-this-blog-is-organised.md  part 3
  development/                          another collection
    a-standalone-post.md                in a collection, not in a series
  • A post's collection is the first folder it's in.
  • If it's one level deeper, its series is the second folder.
  • Its part is the number at the start of the filename. Two digits, 01- rather than 1-, keep the files in reading order when they're listed, since 10- would otherwise sort before 2-.
  • Its URL is the filename without the number, so this post is at /blog/how-this-blog-is-organised. Leaving the number out of the URL means a post can be renumbered without breaking links to it.

The first version kept every post in one flat folder, with the collection, series and part written in each post's header. With three posts that was fine. With a hundred, nothing in the file list would show which posts belong together, and finding one would mean opening files to read their headers. With folders, the file list in the editor looks the same as the blog's structure.

4.2 The post header

Each Markdown file starts with a header between two --- lines.

That's called frontmatter [settings at the top of a Markdown file, written as name: value pairs].

The frontmatter holds what the folders can't: the title, a one-line summary for the post list, the date and the tags. This is the frontmatter for this post:

---
title: "How this blog is organised"
summary: "How the posts on this blog are grouped into collections, series and tags, ..."
date: "2026-11-09"
tags: [meta, nextjs, markdown]
---

There's no collection, series or part in it. The folders already say those.

4.3 structure.json

The folder names are slugs [short, lowercase, URL-friendly names, such as building-this-site]. A slug works in a URL, but it isn't what a reader should see. The titles and descriptions readers do see are in structure.json, next to the posts:

{
  "collections": [
    {
      "slug": "building-this-site",
      "title": "Building this site",
      "description": "How this site is built: the tools, the decisions and ..."
    }
  ],
  "series": [
    {
      "slug": "foundations",
      "title": "Foundations",
      "description": "The first version of the site: getting it live, ...",
      "collection": "building-this-site"
    }
  ]
}

Collections appear in the sidebar in the order they're listed here. Each series names the collection it belongs to, which the build checks against the folder it's actually in.

Tags aren't listed anywhere. Any tag used in a post appears in the sidebar automatically. Tags are informal and new ones come up all the time, so requiring each one to be declared first would get in the way. Collections and series are the blog's structure, so they get a folder and an entry in structure.json.

4.4 One place for each fact

Every fact about a post is now written in exactly one place:

Fact Where it's written
Collection The first folder
Series The second folder
Part The number at the start of the filename
URL The rest of the filename
Title, summary, date, tags The frontmatter
A collection's or series' title and description structure.json

In the first version, a post's collection was written in its frontmatter, and its series was written in the frontmatter as well as in structure.json. The same fact in two places can disagree: a post could say it was in one collection while its series said another. With one place for each fact, there's nothing to keep in sync.

Store each fact once, and everything that needs it reads it from there.

That's often called having a single source of truth [one place where a fact is recorded, so copies can't disagree].

5. Checking the content before it's published

Before every build, the content script checks every post against the folders and structure.json. It checks that:

  • every post is inside a collection folder, and that folder is a collection in structure.json,
  • a series folder is a series in structure.json, and it's inside the collection that series belongs to,
  • every post in a series starts with a part number, and no two posts in a series share one,
  • no two posts have the same filename anywhere in the blog, because the filename becomes the URL,
  • and every post's frontmatter has a title, summary and date.

If any check fails, the script stops the build and lists every problem, with the file each one is in. These are real messages from testing it:

Blog content has problems:
  - content/blog/building-this-site/foundations/no-number.md: posts in a series need
    a part number at the start of the filename, like 01-no-number.md
  - building-this-site/foundations/01-building-in-public.md and
    development/building-in-public.md would both be /blog/building-in-public

Cloudflare builds and deploys the site on every merge to main, and only deploys if the build succeeds. So a misplaced or misnamed post stops at the build, with a message saying exactly what to fix, and never reaches readers.

Checking content during the build stops a broken post before it can be deployed.

6. From structure to pages

6.1 One route for each grouping

Each grouping has its own page, and each kind of page is one route [a pattern of URLs that one page file in the project handles]:

URL Shows
/blog Every post, newest first
/blog/collection/building-this-site A collection: each of its series in order, then any posts not in a series
/blog/series/foundations A series, by part number
/blog/tag/nextjs Every post with that tag, newest first
/blog/how-this-blog-is-organised One post. In a series, it shows "Part 3 of 3" at the top and the previous and next parts at the bottom

In Next.js, a route with a variable part is a folder with the variable in square brackets. app/blog/series/[slug]/page.tsx handles /blog/series/foundations, and any other series, with foundations passed to the page as slug.

That's called a dynamic route [a route where part of the URL is a variable, such as which series to show].

6.2 Building every page ahead of time

A dynamic route could build its page each time someone visits. But every collection, series, tag and post is known when the site is built, so every page can be built then instead.

Next.js does that with a function called generateStaticParams. Its documentation says it "can be used in combination with dynamic route segments to statically generate routes at build time instead of on-demand at request time". It returns a list of the values the route should be built for. This is the one for series:

export const dynamicParams = false;

export function generateStaticParams() {
  return series.map((s) => ({ slug: s.slug }));
}

During the build, Next.js calls it, gets back [{ slug: "foundations" }], and builds one page for each value. The build log lists every page it made:

├   /blog/series/[slug]
│ └ ● /blog/series/foundations
├   /blog/tag/[tag]
│ ├ ● /blog/tag/meta
│ ├ ● /blog/tag/nextjs

dynamicParams = false covers every other value. The same page explains that with it, "only paths provided by generateStaticParams will be served, and unspecified routes will 404". So /blog/series/anything-else gets a "not found" page, instead of Next.js trying to build a page for a series that doesn't exist.

Adding a series to structure.json and merging it is enough for its page to appear. Nothing else has to be listed by hand.

6.3 Ordering

Each list is sorted for the job its grouping does:

  • Collections and tags are sorted by date, newest first, because a reader browsing a subject usually wants the most recent posts.
  • Series are sorted by part number, because they're meant to be read in order. Part 1 could be written last, and it would still come first.
  • Posts on the same day are sorted with the later part first, so a series published in one day shows up in the main list in the right order.

7. What this structure doesn't do

The structure covers how posts are grouped and found. It leaves some things out:

  • There's no search. Finding a post means browsing collections, series and tags.
  • Tags aren't checked. Because tags aren't listed anywhere, a typo creates a new tag. nextjs and next-js would show up as two separate tags, and the build wouldn't notice.
  • A post can only be in one series. A post that belongs in two sequences has to be written for one of them, and linked from the other.
  • Renaming a file breaks its URL. There's no redirect [an instruction that sends visitors from an old URL to a new one], so links to the old URL would get a "not found" page.
  • Inserting a part means renumbering. Adding a new part 2 to a series means renaming every file after it by hand.
  • Publishing needs a deploy. A post only goes live when a pull request is merged and the site is rebuilt. Publishing without a deploy is what the planned writing platform is for.

None of these are a problem at the blog's current size. Each one has a known fix if it becomes one.

8. Applying this to your own projects

The same approach works for any set of related content, such as documentation, course modules or a recipe collection:

  1. Start from how people look for things, and add one grouping for each way. Don't add a grouping until there's a real need for it.
  2. For each grouping, decide how many an item can have, exactly one, at most one, or any number, and whether order matters.
  3. Check for overlap. If two groupings give the same answers to both questions, they're probably one grouping with two names.
  4. Let the folders mirror the structure, so where a file sits says what it belongs to.
  5. Write each fact in one place, and have everything else read it from there.
  6. Check the content automatically before it's published, and make a failed check stop the build with a message that names the file.

9. Summary

Collections, series and tags each answer a different question. A collection says what subject a post is about, a series says what to read next, and tags say what else mentions the same thing. They differ in how many a post can have, and in whether order matters.

Two groupings that give the same answers are one grouping. The first version had sections and collections, and they both meant "posts about a subject". Merging them, and nesting series inside collections, gave each grouping one clear job.

Folders can hold the structure. A post's folders give its collection and series, and its filename gives its part and URL. The file list in an editor looks the same as the blog.

Each fact belongs in one place. When the same fact is written twice, the copies can disagree. When it's written once, there's nothing to keep in sync.

A check in the build protects the live site. Every post is checked before the site builds, and a problem stops the build with a message naming the file. A deploy only happens after a successful build, so a broken post can't go live.

Every page is built ahead of time. generateStaticParams lists every collection, series, tag and post during the build, Next.js builds a page for each, and dynamicParams = false turns any other URL into a "not found" page.

10. Sources

  1. Next.js Docs, generateStaticParams