Content Collections
Astro content collection schemas, content backend abstraction, admin flags, and rendering pipeline
Content Collections
ArgoBox uses five content collections served via a dynamic content system. Content lives at content/ (repo root), NOT src/content/. The src/content/config.ts is intentionally empty (export const collections = {};) to avoid bundling a ~14MB data layer into the Cloudflare Pages SSR worker. Metadata is generated at build time by build-content-index.mjs, and content bodies are served via content-backend.ts (Gitea API in production, local filesystem in dev).
Collection Overview
Collections are served dynamically via content-backend.ts:
| Collection | Directory | Content Type | Key Fields |
|---|---|---|---|
posts |
content/posts/ |
Blog articles | category, complexity, technologies, featured |
journal |
content/journal/ |
Engineering logs | mood, distro, system |
docs |
content/docs/ |
Technical docs (this hub) | section, order, toc |
configurations |
content/configurations/ |
Config tutorials | version, github |
projects |
content/projects/ |
Project docs | status, github, website |
Collection Schemas
posts
Blog articles, tutorials, and technical deep dives.
const postsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string().optional(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
// Categorization
category: z.string().optional(),
categories: z.array(z.string()).optional(),
tags: z.array(z.string()).default([]),
// Author and metadata
author: z.string().optional(),
readTime: z.string().optional(),
draft: z.boolean().optional().default(false),
related_posts: z.array(z.string()).optional(),
// Content metadata
featured: z.boolean().optional().default(false),
technologies: z.array(z.string()).optional(),
complexity: z.enum(['beginner', 'intermediate', 'advanced']).optional(),
// Admin flags
reviewed: z.boolean().optional().default(false),
reviewedDate: z.coerce.date().optional(),
needsWork: z.boolean().optional().default(false),
}),
});
Notable fields:
featured-- Controls whether the post appears in the featured section on the homepage and blog index.complexity-- Three-level difficulty rating shown as a badge on post cards.categories-- Supports both a singlecategorystring and acategoriesarray for backward compatibility.related_posts-- Array of slugs linking to related content.
journal
Day-to-day engineering logs, debugging sessions, and infrastructure change records.
const journalCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string().optional(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
tags: z.array(z.string()).default([]),
// Journal-specific
mood: z.string().optional(),
draft: z.boolean().optional().default(false),
// System context
distro: z.string().optional(),
system: z.string().optional(),
// Admin flags
reviewed: z.boolean().optional().default(false),
reviewedDate: z.coerce.date().optional(),
needsWork: z.boolean().optional().default(false),
}),
});
Notable fields:
mood-- Captures the emotional context of the entry. Values like"DEBUGGING","TRIUMPHANT","LOG","DISCOVERY","FRUSTRATED". Displayed as a badge in the journal index.distro-- The Linux distribution being used during the entry (e.g.,"ArgoOS (Gentoo)","openSUSE","CachyOS").system-- The machine being worked on (e.g.,"callisto-galileo","argobox-lite").
docs
Technical documentation organized into sections with explicit ordering. This is the collection powering the docs hub you are reading right now.
const docsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string().optional(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
category: z.string().optional(),
tags: z.array(z.string()).default([]),
// Documentation-specific
section: z.string().optional(),
order: z.number().optional(),
toc: z.boolean().optional().default(true),
draft: z.boolean().optional().default(false),
// Admin flags
reviewed: z.boolean().optional().default(false),
reviewedDate: z.coerce.date().optional(),
needsWork: z.boolean().optional().default(false),
}),
});
Notable fields:
section-- Groups docs into navigation sections ("overview","cloudflare","site","build-swarm","infrastructure","admin","ai","projects","reference"). Each section gets its own nav group in the docs sidebar.order-- Numeric sort order within a section. Lower numbers appear first.toc-- Controls whether a table of contents is auto-generated from headings. Defaults totrue.
learn
Educational content organized into learning tracks with prerequisites and difficulty levels.
const learnCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string().optional(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
category: z.string().optional(),
tags: z.array(z.string()).default([]),
// Learning-specific
track: z.string().optional(),
difficulty: z.enum(['beginner', 'intermediate', 'advanced']).optional(),
duration: z.string().optional(),
prerequisites: z.array(z.string()).optional(),
order: z.number().optional(),
draft: z.boolean().optional().default(false),
// Admin flags
reviewed: z.boolean().optional().default(false),
reviewedDate: z.coerce.date().optional(),
needsWork: z.boolean().optional().default(false),
}),
});
Notable fields:
track-- Groups content into learning paths (e.g.,"linux-fundamentals","docker-basics","gentoo-advanced").difficulty-- Three-level difficulty rating displayed as a colored badge.duration-- Estimated completion time as a string (e.g.,"30 minutes","2 hours").prerequisites-- Array of slugs pointing to other learn entries that should be completed first.
configurations
Configuration file references and tutorials.
const configurationsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string().optional(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
category: z.string().optional(),
categories: z.array(z.string()).optional(),
tags: z.array(z.string()).default([]),
technologies: z.array(z.string()).optional(),
complexity: z.enum(['beginner', 'intermediate', 'advanced']).optional(),
draft: z.boolean().optional().default(false),
version: z.string().optional(),
github: z.string().optional(),
// Admin flags
reviewed: z.boolean().optional().default(false),
reviewedDate: z.coerce.date().optional(),
needsWork: z.boolean().optional().default(false),
}),
});
Notable fields:
version-- The version of the software the configuration applies to.github-- Link to the configuration file in a GitHub/Gitea repository.
projects
Project documentation with status tracking and links.
const projectsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string().optional(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
category: z.string().optional(),
categories: z.array(z.string()).optional(),
tags: z.array(z.string()).default([]),
technologies: z.array(z.string()).optional(),
github: z.string().optional(),
website: z.string().optional(),
status: z.enum(['concept', 'in-progress', 'completed', 'maintained']).optional(),
draft: z.boolean().optional().default(false),
// Admin flags
reviewed: z.boolean().optional().default(false),
reviewedDate: z.coerce.date().optional(),
needsWork: z.boolean().optional().default(false),
}),
});
Notable fields:
status-- Project lifecycle stage, displayed as a colored badge on project cards.github-- Repository URL.website-- Live project URL if applicable.
Content Backend Abstraction
Content can be loaded from two backends depending on the environment, abstracted through src/lib/content-backend.ts:
Node.js Filesystem (Development)
In local development, content is read directly from the filesystem using node:fs:
// Development mode
import { readFile, readdir } from 'node:fs/promises';
async function getContentFiles(collection: string): Promise<ContentFile[]> {
const dir = `src/content/${collection}`;
const files = await readdir(dir, { recursive: true });
// Parse frontmatter, return content
}
This is the default behavior when running npm run dev. Files are read from disk, and Astro's HMR updates the browser when files change.
Gitea API (Production)
In production, content can be fetched from the Gitea API at git.argobox.com. This enables the admin panel to create, edit, and delete content files through the API without a rebuild:
// Production mode (API-backed content management)
async function getContentFromGitea(
collection: string,
slug: string
): Promise<ContentFile> {
const response = await fetch(
`https://git.argobox.com/api/v1/repos/KeyArgo/argobox/contents/src/content/${collection}/${slug}.md`,
{ headers: { Authorization: `token ${GITEA_TOKEN}` } }
);
const data = await response.json();
return parseContent(atob(data.content));
}
The abstraction layer selects the appropriate backend based on the runtime environment. This lets the admin create a new post in the browser, which writes to Gitea, which triggers a GitHub mirror, which triggers a Cloudflare Pages rebuild with the new content.
Admin Flags
All six collections share three admin-only fields:
| Field | Type | Default | Purpose |
|---|---|---|---|
reviewed |
boolean |
false |
Has the content been reviewed by an admin |
reviewedDate |
Date |
undefined | When the review happened |
needsWork |
boolean |
false |
Flagged for modification or improvement |
These fields are not rendered publicly. They power the admin content review workflow:
- New content is created with
reviewed: false - Content appears in the admin review queue at
/admin/content/review - Admin reviews and marks
reviewed: truewith current date - If issues are found,
needsWork: trueflags it for follow-up - After fixes,
needsWorkis cleared andreviewedDateupdated
The review queue API endpoint is POST /api/admin/mark-reviewed.
Rendering Pipeline
Content goes through this pipeline from file to HTML:
Markdown/MDX file
│
▼
Astro Content Collections (Zod validation)
│
├── Frontmatter parsed and validated
│ (type errors caught at build time)
│
▼
getCollection('posts') or getEntry('posts', slug)
│
▼
entry.render()
│
├── Markdown → HTML (remark/rehype pipeline)
├── MDX → JSX → HTML (with component resolution)
├── Code blocks → Shiki syntax highlighting
│ (one-dark-pro theme, inline styles)
├── Headings → ID generation (for TOC links)
│
▼
{ Content, headings, remarkPluginFrontmatter }
│
▼
<Content /> component renders in page template
Usage in Pages
---
import { getCollection } from 'astro:content';
// Get all published posts, sorted by date
const posts = (await getCollection('posts'))
.filter(post => !post.data.draft)
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---
{posts.map(post => (
<PostCard
title={post.data.title}
slug={post.slug}
pubDate={post.data.pubDate}
tags={post.data.tags}
/>
))}
Rendering a Single Entry
---
import { getEntry } from 'astro:content';
const post = await getEntry('posts', Astro.params.slug);
if (!post) return Astro.redirect('/404');
const { Content, headings } = await post.render();
---
<BlogPost frontmatter={post.data}>
<TableOfContents headings={headings} slot="toc" />
<Content />
</BlogPost>
The headings array returned by render() contains all headings with their depth, text, and generated slug -- used to build the table of contents sidebar.