Enabling open working on Piccalilli part one: content collections
I said we’d work in the open and I mean it. This content is on my blog for now because I’m literally building the infrastructure on Piccalilli as we speak.
This little mini series on my blog covers me doing the Open Working project and the first part of that is setting up Astro to support the content that we will publish.
Let’s have a look at what’s happened prior to this point:
- We’ve done some internal planning and strategy
- I’ve populated our backlog with some stuff that needs doing to enable us to Post™
- We’ve published an article that serves both to let people know what we’re up to and also, form a priority guide of sorts for the Projects landing page on Piccalilli

This clip shows the ticket I’m working from. The aim is:
- Define some configuration of the content types
- Refactor the existing configuration to create a
sharedConfig
- Make adjustments to the existing layout we use to render articles
- Build out a shell for
/projects
and/projects/${slug}
to better inform Jason, our designer
Today’s task
I’ve been working through points 1
and 2
from the ticket.
First up, I’m creating a coreConfig
. This is what our Astro content collections config looked like before:
import { z, defineCollection } from 'astro:content';
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
pubDate: z.date(),
summary: z.string().optional(),
author: z.string().optional(),
image: z
.object({
url: z.string(),
alt: z.string(),
})
.optional(),
tags: z.array(z.string()),
link: z.string().optional(),
socialSummary: z.string().optional(),
socialImage: z.string().optional(),
series: z
.object({
name: z.string(),
order: z.number(),
})
.optional(),
surpressAuthorPromo: z.boolean().optional(),
}),
});
const linksCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
pubDate: z.date(),
summary: z.string().optional(),
author: z.string().optional(),
image: z
.object({
url: z.string(),
alt: z.string(),
})
.optional(),
link: z.string().optional(),
socialSummary: z.string().optional(),
socialImage: z.string().optional(),
}),
});
const theIndexCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
pubDate: z.date(),
summary: z.string().optional(),
author: z.string().optional(),
image: z
.object({
url: z.string(),
alt: z.string(),
})
.optional(),
socialSummary: z.string().optional(),
socialImage: z.string().optional(),
sponsorMessage: z.string().optional(),
sponsorCTAURL: z.string().optional(),
sponsorCTALabel: z.string().optional(),
}),
});
const authorCollection = defineCollection({
type: 'content',
schema: z.object({
name: z.string(),
pubDate: z.date(),
summary: z.string().optional(),
socialSummary: z.string().optional(),
company: z.string().optional(),
language: z.string().optional(),
links: z
.array(
z.object({
name: z.string(),
url: z.string(),
})
)
.optional(),
avatar: z
.object({
full: z.string(),
scaled: z.string(),
})
.optional(),
promo: z
.object({
heading: z.string(),
summary: z.string(),
buttonLabel: z.string(),
buttonURL: z.string(),
})
.optional(),
}),
});
const completeCssCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
pubDate: z.date(),
author: z.string().optional(),
course: z.string().optional(),
module: z.string().optional(),
lessonNumber: z.number().optional(),
}),
});
const emailsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
heading: z.string(),
meta: z.string().optional(),
pubDate: z.date().optional(),
headingSize: z.enum(['extra-large', 'large']).optional(),
promoBox: z
.object({
heading: z.string().optional(),
content: z.string(),
buttonLink: z.string().optional(),
buttonLabel: z.string().optional(),
hideAdvertiseLink: z.boolean().optional(),
})
.optional(),
showUnsubscribeLink: z.boolean().optional(),
closerContent: z.string().optional(),
stream: z.string().optional(),
}),
});
export const collections = {
blog: blogCollection,
links: linksCollection,
'the-index': theIndexCollection,
author: authorCollection,
'complete-css': completeCssCollection,
emails: emailsCollection,
};
I’ll not go into the detail of how this stuff works because Astro do a good job of that already, but in short, each object defines a data structure for each type of content on Piccalilli.
The last block — export const collections
— sets the directory that contains .md
and .mdx
files and its value (the right side) is an assignment to a content collection configuration.
There’s a lot of repetition going on! Before I add more, I thought it would be a good idea to identify a shared set of configuration options that are used, mainly for blogCollection
, linksCollection
and theIndexCollection
.
const coreConfig = {
title: z.string(),
pubDate: z.date(),
summary: z.string().optional(),
socialSummary: z.string().optional(),
socialImage: z.string().optional(),
author: z.string().optional(),
}
Now I can spread coreConfig
in blogCollection
, linksCollection
and theIndexCollection
. I can also spread it in the new collections.
This is a TypeScript project (don’t come at me), so a good idea is to build the site to make sure I haven’t broken anything.

Ah, as is tradition, I broke the build. It seems like we’ve got some loose querying going on, on the homepage.
const authorLink = post.data.link ? post.data.link : `/author/${generateSlug(authorName)}`;
return {
id: post.id,
url: `/${post.data.link ? 'links' : 'blog'}/${post.slug}`,
title: post.data.title,
summary: post.data.summary,
author: { name: authorName, link: authorLink },
date: post.data.pubDate,
tag: post.data.tags ? { label: post.data.tags[0], link: `/category/${generateSlug(post.data.tags[0])}` } : undefined,
directLink: post.data.link,
};
In that snippet, we’re generating props for a <PostList />
component. If it is a links post, we assign the canonical URL we are linking to as the author’s link because that author won’t exist in our system. For a normal post, we link to the authors page, like mine, for example.
Now, in these situations the temptation is to fix everything and let me tell ya, I felt the urge, but I also asked Leanne to give this little PR a review, so the last thing I want to be doing is adding more to that — especially introducing changes on something as important as the homepage!
It’s also a good idea to stay focused on the task at hand, so with that in mind, I made this decision:
- I’ll bring
link
into thesharedConfig
temporarily - I’ll raise a ticket in our refactoring backlog to clean up the querying, remove
link
fromsharedConfig
and put it back onlinksCollection
// Shared core configuration for article-like content
const coreConfig = {
title: z.string(),
pubDate: z.date(),
summary: z.string().optional(),
socialSummary: z.string().optional(),
socialImage: z.string().optional(),
author: z.string().optional(),
// This property is in the shared config because we use it to query between Blog and Links in areas such as the homepage popular feeds
link: z.string().optional(),
};
Folks, always leave comments if you can. They’re useful for not just explaining code, but also, leaving a decision trail to give other developers context. I talk about that at length in Complete CSS as part of the important core skills that will improve you as a developer.
Adding the new collections
With the coreConfig
set up and the project building again, we’re safe to proceed. Let’s start with the overarching Project structure first:
// The core pillar for each open working project which itself, has a page
const projectsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
startDate: z.date(),
endDate: z.date(),
summary: z.string(),
socialImage: z.string(), // For each post to inherit and itself when shared on social media
featureImage: z.string(), // For rendering on the front-end
currentSprint: z.string(), // E.G. Sprint 1: Discovery
ID: z.string(), // So project posts can anchor to a project
}),
});
There’s absolutely nothing optional here because this is — like my comment suggests — a content pillar. It’s a structure for posts to anchor themselves too. When a reader goes to /projects
or /projects/${slug}
this information will be surfaced. We can also surface it in project posts if we like.
Speaking of which, let’s define our project posts structure, utilising our fresh coreConfig
:
// Each of these belongs in a project ☝️
const projectPostsCollection = defineCollection({
type: 'content',
schema: z.object({
...coreConfig,
projectID: z.string(), // To reference a Project's ID field
discipline: z.enum(['Strategy', 'Design', 'Development', 'Quality Assurance', 'Client Management']),
videoURL: z.string().optional(),
sprint: z.string(),
}),
});
Project posts are almost identical to normal blog posts, but with a few extra fields like projectID
, discipline
, videoURL
and sprint
.
I was going to make a projectSprints
a structure too for both projectPostsCollection
and projectsCollection
but it felt like I was creating busy work for us by over-abstracting. It’ll be totally acceptable to group posts by sprint
, just like we do with module
on completeCssCollection
earlier in this post. As I see it, it’s a simpler, flatter approach, so it wins.
With these structures in place, all we have to do is add them to our collections
object:
export const collections = {
blog: blogCollection,
links: linksCollection,
'the-index': theIndexCollection,
author: authorCollection,
'complete-css': completeCssCollection,
emails: emailsCollection,
projects: projectsCollection,
'project-posts': projectPostsCollection,
};
All projects content will be in our projects
directory as mdx
files and project posts will be in project-posts
as mdx
files. I’m still slightly unsure about this organisation strategy because I want the URLS for project posts to be projects/${postSlug}
, but I’ll cross that bridge in the next part. I’ve not added any content yet, so it’ll be no biggie to edit the collections
object, accordingly.
For now, we’re done so I’ll see you in the next one.
Open working is only possible thanks to the generous people that support Piccalilli on Open Collective. Please consider supporting us!
👋 Hello, I’m Andy and this is my little home on the web.
I’m the founder of Set Studio, a creative agency that specialises in building stunning websites that work for everyone and Piccalilli, a publication that will level you up as a front-end developer.
I’ve also got a CSS course called Complete CSS to help you get to a level in development that you never thought would be possible.
Back to blog