6 November 2025
Conquering Astro 5: A Developer’s Journey

Jimmy Burns
← Back to PostsThat moment hits every developer: a new framework version drops, promising faster, better, and easier workflows. Yet, hours later, you're deep in error stacks, questioning your life choices, and the terminal seems to mock you.

That was our reality at ChipsXP with Astro 5.
Our mission? To forge a high-performance, accessible, and visually consistent blogging platform for serious technical content. Our initial reality? A chaotic blend of errors, late-night debugging, schema overhauls that stubbornly refused to compile, and an alarming amount of caffeine.
But here's the truth: those frustrations became the bedrock of this very guide. The continuous cycle of breaking and fixing compelled us to document robust patterns and pinpoint pitfalls that you, our fellow developers, can now expertly navigate. Consider this your "hard knocks curriculum for Astro 5."
And we promise—while this is a deeply technical guide—you'll encounter moments of genuine comic relief. After all, if developers can't chuckle when a pipeline works one day and inexplicably fails the next, we'd all be weeping into our YAML files.
The Astro 5 Upgrade Journey: Building Our Platform
We approached Astro 5 with an optimism that bordered on comedic. Our vision was straightforward: construct a powerful blogging platform designed for advanced research and development content.
Astro's core design philosophy has always championed simplicity. By leveraging `getCollection()` and Markdown (`.md` or `.mdx`) files for content, developers can bypass database overheads entirely, achieving near-instant rendering performance. Each `.mdx` file transforms into a structured, queryable entry, allowing content to be fetched and displayed without unnecessary layers of abstraction.
This file-based approach offers compelling advantages:
- Exceptional Speed: Without database queries, content is immediately available at build or request time, driving latency down to virtually zero.
- Enhanced Reliability: Your Markdown files reside alongside your codebase, version-controlled in Git. This ensures content changes are transparent, auditable, and resilient.
- Superior Performance: Integrated with Astro’s built-in image handling, large images are automatically optimized into lighter formats like `.webp`, guaranteeing faster load times and impressive Lighthouse scores.
- Streamlined Developer Experience: Authors can create content using familiar Markdown, while developers query collections via `getCollection()`, benefiting from type safety, schema validation, and a consistent data structure.
For projects prioritizing rapid load times and minimal complexity, storing articles in `.mdx` files instead of a remote database delivers both a streamlined workflow and production-grade performance. Over time, this also simplifies scaling, as deployments remain straightforward static builds without typical backend bottlenecks.
For more technical details on deprecations and their modern replacement APIs, refer to the [Astro v5 Upgrade Guide – Deprecated: Astro.glob()](https://docs.astro.build/en/guides/upgrade-to-v5/#deprecated-astroglob).
Using `getCollection()` with Markdown Articles
Ready to dive in? Here’s a practical example of how to implement `getCollection()` for your Markdown articles.
1. Define Your Content Collection Schema
In `src/content/config.ts` within the cloned `ChipsXP-Substack/blog/src/content/config.ts` repository, you will define your content schema for articles. This ensures that every `.mdx` file in your `src/content/advance/` and `src/content/research/` folders adheres to consistent rules.
```typescript
// src/content/config.ts
import { z, defineCollection, type SchemaContext } from "astro:content";
import { glob } from "astro/loaders";
// Define schemas for each collection
const schema = ({ image }: SchemaContext) =>
z.object({
title: z.string(),
description: z.string(),
// ... more schema properties at repository
tags: z.array(z.string()).optional(),
category: z.enum(["research", "advance"]),
author: z.object({
// ... more author schema at repository
});
});
export const collections = {
research: defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/research" }),
type: "content_layer",
schema,
}),
advance: defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/advance" }),
type: "content_layer",
schema,
}),
};
```
2. Create Your Content in Markdown Files
An example `.mdx` content file might look like this: `blog/src/content/advance/AI-Assistant-3D-Ecommerce.mdx`
```markdown
---
title: "Deployment: The AI Assistant 3D Ecommerce platform"
description: "A detailed exploration of our development process, and challenges in creating Deployment:The AI Assistant 3D Ecommerce platform"
pubDate: "2025-05-31"
heroImage:
src: "../../assets/advanceimg/layout-system-architecture.webp"
alt: "A 3D customization tool for apparel, with a clean and intuitive interface"
category: "advance"
author:
name: "Jimmy Burns"
nickname: "pluckCode"
url: "https://github.com/chipsxp"
tags:
[
"AI Assistant",
"3D Ecommerce",
"3D Rendering",
"AI Image Generation",
"GLTF Models",
"color customization",
]
---
Deployment:
The AI Assistant 3D Ecommerce platform discussed in this research is deployed and available at [https://ai-assistant-3d-ecommerce-production.up.railway.app/](https://ai-assistant-3d-ecommerce-production.up.railway.app/)
Introduction:
This project introduces an innovative platform that merges 3D visualization with AI-generated designs, revolutionizing apparel customization. Our application empowers users to craft unique, personalized clothing items within a real-time 3D environment, offering intuitive tools for color selection, logo placement, and AI-assisted design generation. For more details on the project's codebase, explore its GitHub repository: https://github.com/chipsxp/AI-Assistant-3D-Ecommerce.
Research & Problem Statement
Market Analysis
The e-commerce landscape is rapidly shifting towards personalization, with studies indicating that 36% of consumers now expect tailored products. However, conventional 2D preview systems often fail to provide accurate product representations, leading to increased customer dissatisfaction and higher return rates.
Technical Challenge:
... more content at repository
```
3. Fetch & Render Articles with `getCollection()`
Within a page component (e.g., `src/pages/research/index.astro`), you can fetch and display your articles:
```astro
---
// ... more code at top
import { SITE_TITLE, SITE_DESCRIPTION } from "../../consts";
import { getCollection } from "astro:content";
import { Image } from "astro:assets";
import FormattedDate from "../../components/FormattedDate.astro";
// Type for research collection posts
const posts = await getCollection("research");
const sortedPosts = posts.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
---
<html lang="en">
<head>
<title>Blog</title>
</head>
<body>
... more code at repository
<main>
<section>
<h2>Research Articles</h2>
<ul>
{
sortedPosts.map((post) => (
<li>
<a href={`/research/${post.id}`}>
{post.data.heroImage?.src && (
<Image
src={post.data.heroImage.src}
width={960}
height={480}
alt={post.data.heroImage.alt}
class="aspect-[2/1] object-cover"
/>
)}
<h4 class="title">{post.data.title}</h4>
<p class="date">
<FormattedDate date={post.data.pubDate} />
</p>
... more code at repository
</body>
</html>
```
4. Implement Dynamic Routing for Each Article
Create a dynamic route file, for example: `src/pages/research/[...slug].astro`
```astro
---
import { type CollectionEntry, getCollection, render } from "astro:content";
import BlogPost from "../../layouts/ResearchPost.astro";
export async function getStaticPaths() {
const posts = await getCollection("research");
return posts.map((post) => ({
params: { slug: post.id },
props: post,
}));
}
type Props = CollectionEntry<"research">;
// Get the post from props
const post = Astro.props;
// Handle case where post might not be available
if (!post || !post.data) {
return Astro.redirect("/404");
}
// Use the new render function instead of post.render()
const { Content } = await render(post);
---
<BlogPost
title={post.data.title}
description={post.data.description || ""}
pubDate={post.data.pubDate}
category={post.data.category || "uncategorized"}
tags={post.data.tags || []}
heroImage={{
src: post.data.heroImage?.src || "",
alt: post.data.heroImage?.alt || "",
}}
author={post.data.author || "Anonymous"}
updatedDate={post.data.updatedDate}
>
<Content />
</BlogPost>
```
Why Content Collections Outperform Traditional Databases
Opting for content collections with Markdown offers significant advantages over a conventional database setup:
- Zero Latency: Content loads instantaneously at build-time or as static assets, eliminating the need for database queries.
- Enhanced SEO: Every `.md` file converts into a fully optimized HTML page, boosting your search engine visibility.
- Robust Reliability: With no database servers to manage, your content is inherently more stable and less prone to downtime. Everything is versioned in Git and deploys as static files.
- Developer-Centric Experience: Benefit from straightforward schema validation using Zod, structured data access, and compile-time type safety.
The theoretical elegance of this approach was undeniable. The practical execution, however, felt more like a reality TV show, with frameworks testing our patience at every turn.
Architecting Content: Our Schema Evolution
Our journey began, naturally, with the schema—the essential backbone defining how blog posts and content would be structured. In principle, this seemed straightforward: titles, descriptions, dates, images, and categories. Our initial schema reflected this clean, minimalist, and professional aspiration:
```typescript
// Initial Content Schema (a.k.a. "the overconfident phase")
const collection = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
heroImage: z.string(),
category: z.enum(["research", "advance"]),
}),
});
```
Elegant, wasn't it? Much like a pristine apartment before the chaos of moving in.
When Minimalism Became a Liability
We quickly discovered that real-world content is inherently messy. Developers, in their haste, might omit a description. Date formats arrive like untamed animals—sometimes `YYYY-MM-DD`, sometimes `MM/DD/YY`, and sometimes simply "Monday." Images, those persistent imps, often reside in disparate directories or lack consistent sizing.
Initially, we confused minimalism with efficiency. The idea was to start lean and add complexity later. But Astro 5’s content collections taught us a crucial lesson: schemas are not merely suggestions. They are strict gatekeepers.
Suddenly, our posts refused to build because an author (we won't name names) decided to skip a description "just this once." Debugging transformed into a frustrating treasure hunt, with the ultimate prize being the simple re-rendering of our blog.
At this juncture, it became abundantly clear: content validation with types through interfaces isn't a tedious detail—it's a developer's essential insurance policy against chaos. We pivoted. Instead of resisting schema strictness, we embraced it, fortifying our rules. Required fields remained required. Object structures became explicitly defined. Metadata was iron-clad.
The Iterative Evolution of Our Schema
Over time, a mantra emerged as our guiding principle: "Start simple, iterate often."
Here’s how that iterative process unfolded:
- Basic Schema (Phase 1): Our "We'll fill in descriptions later" phase. (Spoiler: We didn't).
- Intermediate Schema (Phase 2): We introduced validators, stricter categories, and standardized metadata handling. Errors decreased, and developer satisfaction soared.
- Finalized Schema (Phase 3): We centralized all definitions, automated validations, and enforced consistent hero image dimensions (960x480). Finally, our screenshots stopped resembling squashed memes.
Each iteration didn't just reduce errors; it significantly lowered the mental overhead involved in creating new content. A meticulously structured schema eliminates decision fatigue—much like consistently ordering the same coffee, freeing up brainpower from "cappuccino versus latte" dilemmas.
Humor in Schema Hell
At one memorable point, we tried to bypass strict validation for a draft post, hoping to expedite the process. The immediate build error felt almost sentient, as if the schema was looking back at us, declaring, "I warned you."
The lesson was profound: Astro 5 compels consistency, and while its rigidity can feel unforgiving initially, once you surrender to its logic, your development workflow becomes remarkably smoother.
```typescript
// src/content/config.ts
import { z, defineCollection, type SchemaContext } from "astro:content";
import { glob } from "astro/loaders";
// Define schemas for each collection
const schema = ({ image }: SchemaContext) =>
z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.object({
src: image(),
alt: z.string(),
}),
tags: z.array(z.string()).optional(),
category: z.enum(["research", "advance"]),
author: z.object({
name: z.string(),
nickname: z.string().optional(),
url: z.string().url().optional(),
}),
});
// Define collections with their respective schemas
export const collections = {
research: defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/research" }),
type: "content_layer",
schema,
}),
advance: defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/advance" }),
type: "content_layer",
schema,
}),
};
```
Ultimately, our configuration schema blossomed into a highly structured framework that powerfully enforces consistency. While its rigidity might initially feel burdensome, adapting to it unveils a clarity that significantly simplifies your workflow—much like adjusting to a standing desk: uncomfortable at first, but invaluable for long-term well-being and productivity.
The Transformative Power of Content Collections
Beyond the initial debugging headaches, Astro 5’s content collections bestowed upon us something truly unexpected: unwavering confidence.
Instead of constantly questioning, "Does this post contain the correct metadata? Is the hero image properly linked? Do the categories strictly follow our rules?"—we simply knew. The system was designed to inherently reject malformed data, acting as a robust quality gate.
You might notice `glob` from `astro/loaders` in our `src/content/config.ts`. In earlier Astro versions, content collections directly used the `glob` function to load files from the filesystem. While functional, this approach offered limited flexibility, making it challenging to extend beyond simple file-based content. Over time, Astro meticulously refined this system into what is now known as loaders.
Loaders provide a more consistent and extensible mechanism for integrating data into content collections. Instead of being confined solely to filesystem patterns, loaders establish a formalized interface for fetching content from diverse sources. The familiar `glob()` method still exists as a built-in loader for local files, but it now operates within a broader framework where developers can also utilize the `file()` loader or even construct custom loaders.
This evolution mandates that each collection specifies a loader in its configuration. A loader can be defined inline within your `src/content/config.ts`, shared across multiple collections, or even published as an `NPM package` to foster wider usage and community contributions through Astro’s integrations library.
By transitioning from a hardwired, `glob`-only approach to a comprehensive loader architecture, Astro has unlocked the potential for far more flexible content pipelines. What began as a single-purpose utility has matured into a powerful system capable of supporting both built-in and community-driven data sources.
And for developers meticulously maintaining a growing blog or an expansive research archive, this transformation is everything. Structure equates to scalability. A well-defined schema ensures sanity.
At this juncture, we had successfully wrestled content architecture into submission. Yet, just around the corner, the next set of trials awaited: broken image pipelines, layouts with a stubborn will of their own, and a sleek new View Transitions API that performed flawlessly on paper but transformed our dev screens into a dizzying light show.
But that, dear reader, is a compelling story for our next installment. Stay tuned next week for the second part, where we delve into the new View Transitions API and more. Be sure to check out the updated ChipsXP.com!
We are actively developing an External Serverless Proxy and API Link Embed solution for more secure and generalized access in the near future.

Also, discover how Railway powered our AI applications online at [AI Personal Data Assistant](https://ai-assistant-3d-ecommerce-production.up.railway.app/).