Skip to main content
Site Architecture

Content Collections

Astro content collection schemas, content backend abstraction, admin flags, and rendering pipeline

February 23, 2026

Content Collections

Arcturus-Prime uses six Astro content collections with Zod schema validation. Each collection defines a strict frontmatter schema, supports admin review flags, and renders through Astro’s built-in content pipeline. In production, content can be served from either the local filesystem or the Gitea API via a content backend abstraction layer.

Collection Overview

All collections are defined in src/content/config.ts:

CollectionDirectoryContent TypeKey Fields
postssrc/content/posts/Blog articlescategory, complexity, technologies, featured
journalsrc/content/journal/Engineering logsmood, distro, system
docssrc/content/docs/Technical docs (this hub)section, order, toc
learnsrc/content/learn/Educational trackstrack, difficulty, duration, prerequisites
configurationssrc/content/configurations/Config tutorialsversion, github
projectssrc/content/projects/Project docsstatus, 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 single category string and a categories array 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., "Capella-Outpost", "Altair-Link").

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 to true.

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.Arcturus-Prime.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.Arcturus-Prime.com/api/v1/repos/KeyArgo/Arcturus-Prime/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:

FieldTypeDefaultPurpose
reviewedbooleanfalseHas the content been reviewed by an admin
reviewedDateDateundefinedWhen the review happened
needsWorkbooleanfalseFlagged for modification or improvement

These fields are not rendered publicly. They power the admin content review workflow:

  1. New content is created with reviewed: false
  2. Content appears in the admin review queue at /admin/content/review
  3. Admin reviews and marks reviewed: true with current date
  4. If issues are found, needsWork: true flags it for follow-up
  5. After fixes, needsWork is cleared and reviewedDate updated

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.

contentcollectionsastromarkdownmdxzod