Skip to main content

Overview

Astro Portfolio v3 uses Astro’s Content Collections API to manage all content with type safety and validation. Content is organized into collections, each with its own schema defined using Zod.
All content collections are defined in ~/workspace/source/src/content.config.ts with their schemas and validation rules.

Content Collections Types

The project uses two types of content collections:
  1. Content-based collections - Store Markdown/MDX files with frontmatter
  2. Data-based collections - Load JSON files using Astro’s file loader

Available Collections

Blog Collection

The blog collection stores all blog posts as Markdown files.Schema Definition:
// src/content.config.ts:74-87
const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    author: z.string(),
    tags: z.array(z.string()),
    description: z.string(),
    dateCreated: z.coerce.date(),
    cover_image: z.string().optional(),
    series: z.string().optional(),
    sponsors: z.array(z.string()).optional(),
    canonical_url: z.string().url().optional(),
  }),
});
Example Blog Post:
---
title: AI Is Not Replacing You. It's Reshaping How You Think.
author: Lewis Kori
tags: ["artificial intelligence", "software engineering", "ai agents"]
series: Human + AI
description: A software engineer reflects on how AI agents are reshaping engineering work
dateCreated: 2026-02-17
sponsors: ["Scraper API"]
canonical_url: https://example.com/original-post
cover_image: /images/ai-article.jpg
---

When AI started writing decent code, I did not feel excitement...
Location: src/content/blog/Usage:
---
import { getCollection } from 'astro:content';

const blogPosts = await getCollection('blog');
const sortedPosts = blogPosts.sort((a, b) => 
  b.data.dateCreated.valueOf() - a.data.dateCreated.valueOf()
);
---

{sortedPosts.map(post => (
  <article>
    <h2>{post.data.title}</h2>
    <p>{post.data.description}</p>
  </article>
))}

Working with Content Collections

Fetching All Entries

---
import { getCollection } from 'astro:content';

// Get all entries from a collection
const blogPosts = await getCollection('blog');
const projects = await getCollection('projects');
---

Fetching a Single Entry

---
import { getEntry } from 'astro:content';

// Get a specific entry by ID
const aboutContent = await getEntry('about', 'about-content');

// Render markdown content
const { Content } = await aboutContent.render();
---

<Content />

Filtering Collections

---
import { getCollection } from 'astro:content';

// Filter by a condition
const featuredProjects = await getCollection('projects', (project) => {
  return project.data.featured === true;
});

// Filter by tag
const aiPosts = await getCollection('blog', (post) => {
  return post.data.tags.includes('artificial intelligence');
});
---

Sorting Collections

---
import { getCollection } from 'astro:content';

const blogPosts = await getCollection('blog');

// Sort by date (newest first)
const sortedPosts = blogPosts.sort((a, b) => 
  b.data.dateCreated.valueOf() - a.data.dateCreated.valueOf()
);

// Sort by order field
const experiences = await getCollection('experience');
const orderedExperiences = experiences.sort((a, b) => 
  a.data.order - b.data.order
);
---

Adding New Content

Adding a Blog Post

  1. Create a new Markdown file in src/content/blog/:
---
title: My New Blog Post
author: Your Name
tags: ["web development", "astro"]
description: A brief description of the post
dateCreated: 2026-03-05
---

Your blog post content here...
  1. The post will automatically be available through the blog collection

Adding a Project

  1. Create a new Markdown file in src/content/projects/:
---
title: My Awesome Project
tech: ["React", "Node.js", "MongoDB"]
github_link: https://github.com/username/project
featured: true
year: 2026
---

Project description and details...

Adding Data to JSON Collections

Edit the corresponding JSON file in src/data/:
{
  "id": "unique-id",
  "name": "Item Name",
  "description": "Description",
  "link": "https://example.com"
}

Type Safety

Content Collections provide full TypeScript support:
import { getCollection, type CollectionEntry } from 'astro:content';

// Type-safe blog post
type BlogPost = CollectionEntry<'blog'>;

const posts = await getCollection('blog');
// posts is typed as BlogPost[]

posts.forEach((post: BlogPost) => {
  console.log(post.data.title); // ✓ Type-safe
  console.log(post.data.invalidField); // ✗ TypeScript error
});

Generating Dynamic Routes

Use content collections to generate pages:
---
// src/pages/blog/[slug]/index.astro
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const blogPosts = await getCollection('blog');
  
  return blogPosts.map(post => ({
    params: { slug: post.id },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await post.render();
---

<article>
  <h1>{post.data.title}</h1>
  <p>By {post.data.author} on {post.data.dateCreated}</p>
  <Content />
</article>

Schema Validation

Zod schemas ensure data integrity:
// If you try to add invalid data:
{
  title: "Post Title",
  dateCreated: "invalid date", // ✗ Will fail validation
  tags: "not an array",        // ✗ Will fail validation
}

// Valid data:
{
  title: "Post Title",         // ✓ String
  dateCreated: "2026-03-05",   // ✓ Coerced to Date
  tags: ["tag1", "tag2"],      // ✓ Array of strings
}

Advanced Techniques

Querying by Series

---
import { getCollection } from 'astro:content';

const seriesName = "Human + AI";
const seriesPosts = await getCollection('blog', (post) => {
  return post.data.series === seriesName;
});
---

Using Content in Components

---
// src/components/LatestPosts.astro
import { getCollection } from 'astro:content';

const allPosts = await getCollection('blog');
const latestPosts = allPosts
  .sort((a, b) => b.data.dateCreated.valueOf() - a.data.dateCreated.valueOf())
  .slice(0, 3);
---

<section>
  <h2>Latest Posts</h2>
  {latestPosts.map(post => (
    <article>
      <h3>{post.data.title}</h3>
      <p>{post.data.description}</p>
      <a href={`/blog/${post.id}`}>Read more</a>
    </article>
  ))}
</section>

Rendering MDX Components

If using MDX, you can pass components to the renderer:
---
import { getEntry } from 'astro:content';
import CustomComponent from '@/components/CustomComponent.astro';

const entry = await getEntry('blog', 'my-mdx-post');
const { Content } = await entry.render();
---

<Content components={{ CustomComponent }} />

Best Practices

  1. Use strict schemas - Define all required fields explicitly
  2. Leverage Zod coercion - Use z.coerce.date() for date fields
  3. Keep collections focused - Don’t mix unrelated content types
  4. Use optional fields wisely - Mark fields as optional only when necessary
  5. Validate URLs - Use z.string().url() for URL fields
  6. Sort consistently - Always sort collections before rendering
  7. Cache collection queries - Queries are cached during build
  8. Use TypeScript types - Leverage CollectionEntry for type safety

Project Structure

See where content files are located

Components

Learn how components use content collections

i18n

Understand internationalized content

Build docs developers (and LLMs) love