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:
Content-based collections - Store Markdown/MDX files with frontmatter
Data-based collections - Load JSON files using Astro’s file loader
Available Collections
Blog
Projects
Experience
Advisory & Notes
Data 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 >
)) }
Projects Collection Showcases portfolio projects with metadata and links. Schema Definition: // src/content.config.ts:100-114
const projects = defineCollection ({
type: 'content' ,
schema: z . object ({
title: z . string (),
tech: z . array ( z . string ()),
external_link: z . string (). url (). optional (),
github_link: z . string (). url (). optional (),
app_store: z . string (). url (). optional (),
google_play: z . string (). url (). optional (),
cover_image: z . string (). optional (),
featured: z . boolean (). optional (),
year: z . number (),
made_at: z . string (). optional (),
}),
});
Example Project: ---
title : E-commerce Platform
tech : [ "Next.js" , "TypeScript" , "Stripe" , "Tailwind CSS" ]
external_link : https://example-shop.com
github_link : https://github.com/user/ecommerce
cover_image : /projects/ecommerce-cover.jpg
featured : true
year : 2026
made_at : Freelance
---
A full-featured e-commerce platform with payment processing...
Location: src/content/projects/Filtering Featured Projects: ---
import { getCollection } from 'astro:content' ;
const allProjects = await getCollection ( 'projects' );
const featuredProjects = allProjects . filter ( p => p . data . featured );
---
Experience Collection Work experience entries for the portfolio. Schema Definition: // src/content.config.ts:89-98
const experience = defineCollection ({
type: 'content' ,
schema: z . object ({
company: z . string (),
website: z . string (). url (),
role: z . string (),
period: z . string (),
order: z . number (),
}),
});
Example Experience Entry: ---
company : Acme Corporation
website : https://acme.com
role : Senior Software Engineer
period : "2024 - Present"
order : 1
---
Led the development of microservices architecture...
Location: src/content/experience/Ordering Entries: ---
import { getCollection } from 'astro:content' ;
const experiences = await getCollection ( 'experience' );
const sortedExperiences = experiences . sort (( a , b ) =>
a . data . order - b . data . order
);
---
Advisory Collection Advisory services and offerings. Schema Definition: // src/content.config.ts:63-72
const advisory = defineCollection ({
type: 'content' ,
schema: z . object ({
title: z . string (),
subtitle: z . string (),
description: z . string (),
lastUpdated: z . coerce . date (),
featuredImage: z . string (). optional (),
}),
});
Location: src/content/advisory/Operating Notes Collection Personal operating principles and notes. Schema Definition: // src/content.config.ts:53-61
const operatingNotes = defineCollection ({
type: 'content' ,
schema: z . object ({
title: z . string (),
subtitle: z . string (),
description: z . string (),
lastUpdated: z . coerce . date (),
}),
});
Location: src/content/operatingNotes/About Collection About page content. Schema Definition: // src/content.config.ts:49-51
const about = defineCollection ({
type: 'content' ,
});
Location: src/content/about/The about collection doesn’t have a schema, allowing for flexible content structure.
Information about sponsors and partners. Schema Definition: // src/content.config.ts:116-126
const sponsors = defineCollection ({
type: 'content' ,
schema: z . object ({
name: z . string (),
url: z . string (). url (),
twitter: z . string (). url (). optional (),
logo: z . string (). optional (),
excerpt: z . string (),
is_featured: z . boolean (),
}),
});
Location: src/content/sponsors/Socials Collection (Data Loader) Social media links loaded from JSON. Schema Definition: // src/content.config.ts:4-13
import { file } from 'astro/loaders' ;
const socials = defineCollection ({
loader: file ( 'src/data/socials.json' ),
schema: z . object ({
id: z . string (),
name: z . string (),
url: z . string (). url (),
icon: z . string (),
ariaLabel: z . string (),
}),
});
JSON Structure: [
{
"id" : "github" ,
"name" : "github" ,
"url" : "https://github.com/lewis-kori" ,
"icon" : "M12 0c-6.626 0-12 5.373-12..." ,
"ariaLabel" : "GitHub"
},
{
"id" : "twitter" ,
"name" : "twitter" ,
"url" : "https://twitter.com/lewis_kihiu" ,
"icon" : "M18.244 2.25h3.308l-7.227..." ,
"ariaLabel" : "X (Twitter)"
}
]
Location: src/data/socials.jsonOther Data Collections
Books, Tech Stack, and Desktop Setup
Books Collection: // src/content.config.ts:15-25
const books = defineCollection ({
loader: file ( 'src/data/books.json' ),
schema: z . object ({
id: z . string (),
title: z . string (),
author: z . string (),
image: z . string (),
description: z . string (),
link: z . string (),
}),
});
Tech Stack Collection: // src/content.config.ts:27-36
const techStack = defineCollection ({
loader: file ( 'src/data/tech-stack.json' ),
schema: z . object ({
id: z . string (),
name: z . string (),
image: z . string (),
description: z . string (),
link: z . string (). url (),
}),
});
Desktop Setup Collection: // src/content.config.ts:38-47
const desktopSetup = defineCollection ({
loader: file ( 'src/data/desktop-setup.json' ),
schema: z . object ({
id: z . string (),
name: z . string (),
image: z . string (),
description: z . string (),
link: z . string (),
}),
});
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
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...
The post will automatically be available through the blog collection
Adding a Project
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
Content Collections Best Practices
Use strict schemas - Define all required fields explicitly
Leverage Zod coercion - Use z.coerce.date() for date fields
Keep collections focused - Don’t mix unrelated content types
Use optional fields wisely - Mark fields as optional only when necessary
Validate URLs - Use z.string().url() for URL fields
Sort consistently - Always sort collections before rendering
Cache collection queries - Queries are cached during build
Use TypeScript types - Leverage CollectionEntry for type safety
Related Pages
Project Structure See where content files are located
Components Learn how components use content collections
i18n Understand internationalized content