Arte y Web Creaciones uses Astro’s Content Collections API to manage all content with type-safe schemas and automatic validation. This ensures consistency and catches errors at build time.
What Are Content Collections?
Content Collections are Astro’s way of organizing and managing content files (Markdown, MDX, JSON) with:
Type safety - Define schemas with Zod for automatic validation
IntelliSense - Get autocomplete for frontmatter fields
Build-time validation - Catch errors before deployment
Flexible loading - Use glob patterns to load files from anywhere
This project uses Astro’s modern loader-based API with the glob loader, which replaced the older directory-based approach.
Project Collections
The project has four content collections defined in src/content.config.ts:
import { glob } from 'astro/loaders' ;
import { defineCollection , z } from 'astro:content' ;
export const collections = {
blog , // Blog posts
promoSingle , // One-page website promotions
promoPro , // Professional website promotions
promoTienda , // E-commerce website promotions
};
Collection Structure
src/content/
├── blog/ # Blog posts (Markdown/MDX)
│ ├── 10-pasos-para-crear-tu-web-profesional.md
│ ├── cuanto-cuesta-una-pagina-web.md
│ └── ... (50+ blog posts)
├── promoSingle/ # One-page promotion data
│ └── promoSingle.md
├── promoPro/ # Professional web promotion data
│ └── promoPro.md
└── promoTienda/ # E-commerce promotion data
└── promoTienda.md
Blog Collection
The blog collection is the most commonly used collection. Here’s how it’s defined:
Schema Definition
const blog = defineCollection ({
// Load all .md and .mdx files from src/content/blog/
loader: glob ({
base: './src/content/blog' ,
pattern: '**/*.{md,mdx}'
}),
// Schema defines required and optional fields
schema: z . object ({
title: z . string (),
description: z . string (),
pubDate: z . coerce . date (), // Automatically converts to Date
draft: z . boolean (),
tags: z . array ( z . string ()). optional (),
heroImage: z . string (). optional (),
publisher: z . string (). optional (),
}),
});
Field Breakdown
Type: stringThe blog post title, used in:
Page <title> tag
Open Graph meta tags
Blog post header
title: "10 Pasos Para Crear Tu Web Profesional"
Type: stringA compelling description for SEO and social sharing: description: "✅ Guía completa: 10 pasos para crear tu web profesional desde CERO | Diseño, desarrollo y SEO"
Type: date (coerced from string)Publication date in YYYY-MM-DD format: The z.coerce.date() automatically converts the string to a JavaScript Date object.
Type: booleanControls whether the post is published: draft: false # Published
draft: true # Hidden from build
Draft posts are excluded from the production build.
Type: stringPath to the featured image: heroImage: "/img/posts/web-profesional.webp"
Used in Open Graph tags and as the post header image.
Type: stringContent publisher name: publisher: "Arte y Web Creaciones"
Example Blog Post
Here’s a real blog post from the project:
src/content/blog/10-pasos-para-crear-tu-web-profesional.md
---
title : "10 Pasos Para Crear Tu Web Profesional"
description : "✅ Guía completa: 10 pasos para crear tu web profesional desde CERO | Diseño, desarrollo y SEO | 📱 Presupuesto gratis en 24h"
pubDate : '2025-09-13'
heroImage : "/img/posts/web-profesional.webp"
slug : "10-pasos-para-crear-tu-web-profesional"
draft : false
tags : [ "webs profesional" , "diseño web" , "desarrollo web" ]
publisher : "Arte y Web Creaciones"
---
¿Quieres saber los 10 pasos para crear tu web pero no sabes por dónde empezar?
## Guía paso a paso para Crear tu Página Web Profesional en 2026
### 1. Elige un nombre de dominio estratégico
El primer paso para crear una página web es elegir un nombre que refleje tu marca...
### 2. Decide qué tipo de página web quieres
Una vez que tengas un nombre, es hora de decidir qué tipo de página web...
<!-- More content -->
Querying Blog Posts
In any Astro component or page, you can query the blog collection:
src/pages/blog/index.astro
---
import { getCollection } from 'astro:content' ;
import Layout from '@/layouts/Layout.astro' ;
// Get all blog posts
const allPosts = await getCollection ( 'blog' );
// Filter out drafts and sort by date
const posts = allPosts
. filter ( post => ! post . data . draft )
. sort (( a , b ) => b . data . pubDate . valueOf () - a . data . pubDate . valueOf ());
---
< Layout title = "Blog" >
< h1 > Blog Posts </ h1 >
< ul >
{ posts . map ( post => (
< li >
< a href = { `/blog/ ${ post . id } ` } >
{ post . data . title }
</ a >
< time > { post . data . pubDate . toLocaleDateString ( 'es-ES' ) } </ time >
</ li >
)) }
</ ul >
</ Layout >
Rendering Blog Posts
The dynamic blog post page uses getStaticPaths() and render():
src/pages/blog/[...slug].astro
---
import { type CollectionEntry , getCollection , render } from "astro:content" ;
import BlogPost from "@/layouts/BlogPost.astro" ;
import Layout from "@/layouts/Layout.astro" ;
import Alert from "@/components/Alert.astro" ;
// Generate a route for each blog post
export async function getStaticPaths () {
const posts = await getCollection ( "blog" );
return posts . map (( post : CollectionEntry < "blog" >) => ({
params: { slug: post . id },
props: post ,
}));
}
type Props = CollectionEntry < "blog" >;
const post = Astro . props ;
const { title , description , heroImage , tags } = post . data ;
// Render the MDX/Markdown content to a component
const { Content } = await render ( post );
---
< Layout title = { title } description = { description } image = { heroImage } keywords = { tags } >
< BlogPost { ... post . data } >
<!-- Inject Alert component for use in MDX -->
< Content components = { { Alert } } />
</ BlogPost >
</ Layout >
The components prop allows MDX files to use custom components like <Alert> directly in the Markdown.
The three promotion collections (promoSingle, promoPro, promoTienda) share a common schema:
const detallesSideBarSchema = z . array (
z . object ({
icono: z . string (), // Icon class (e.g., "bi bi-heart")
titulo: z . string (), // Detail title
contenido: z . string (), // Detail content
})
);
const promoSchema = z . object ({
titulo: z . string (), // Main title
subtitulo: z . string (), // Subtitle
precio: z . string (), // Price (e.g., "190.00")
destacado: z . string (), // Highlighted text
backgroundImage: z . string (), // Hero background image
detalles: z . array ( z . string ()), // Feature list
link: z . string (), // CTA link
tituloMain: z . string (), // Main section title
parrafosMain: z . array ( z . string ()), // Main section paragraphs
imagenMain: z . string (), // Main section image
detallesSideBar: detallesSideBarSchema , // Sidebar details
});
src/content/promoSingle/promoSingle.md
---
titulo : "Web SINGLEPAGE Completa"
subtitulo : "Una web veloz en 7 días"
precio : "190.00"
destacado : "IVA no incluido"
backgroundImage : "/img/ofertas/web-onepage-en-oferta.webp"
detalles :
- "Una página responsive"
- "5 secciones en menú"
- "Rápida y sin recargas"
- "Hecha por profesionales"
- "Certificado SSL"
- "Optimizada para SEO"
- "Puede crecer en el tiempo"
link : "/promocion/web-onepage-en-oferta/"
tituloMain : "Detalles de la Web SinglePage"
parrafosMain :
- "La Página Web Profesional One Page es una excelente opción para iniciar con una página web rápida."
- "Tiene todo lo que necesita una web para darse a conocer al público y crecer en el tiempo."
- "Está preparada para posicionarse orgánicamente en Google (SEO)."
imagenMain : "/img/ofertas/promo-single-page.webp"
detallesSideBar :
- icono : "bi bi-heart"
titulo : "La web es de tu propiedad"
contenido : "La creación de tu sitio web será completamente tuya."
- icono : "bi bi-lightning"
titulo : "Acceso inmediato"
contenido : "Una vez elijas plantilla: entrega 7 días"
- icono : "bi bi-menu-button-wide"
titulo : "Menú completo de 5 secciones"
contenido : "Menú con todo el contenido de forma instantánea."
---
Promotion collections are queried similarly to the blog:
src/components/Promociones/PromoCard.astro
---
import { getCollection } from 'astro:content' ;
const promoSingle = await getCollection ( 'promoSingle' );
const promoPro = await getCollection ( 'promoPro' );
const promoTienda = await getCollection ( 'promoTienda' );
const allPromos = [ ... promoSingle , ... promoPro , ... promoTienda ];
---
< div class = "promo-grid" >
{ allPromos . map ( promo => (
< div class = "promo-card" >
< h3 > { promo . data . titulo } </ h3 >
< p class = "price" > { promo . data . precio } € </ p >
< ul >
{ promo . data . detalles . map ( detalle => (
< li > { detalle } </ li >
)) }
</ ul >
< a href = { promo . data . link } > Más información </ a >
</ div >
)) }
</ div >
Content Collections API
The Content Collections API provides these key functions:
defineCollection()
Define a collection with a loader and schema:
import { defineCollection , z } from 'astro:content' ;
import { glob } from 'astro/loaders' ;
const myCollection = defineCollection ({
loader: glob ({
base: './src/content/myCollection' ,
pattern: '**/*.{md,mdx,json}'
}),
schema: z . object ({
title: z . string (),
date: z . coerce . date (),
}),
});
getCollection()
Retrieve all entries from a collection:
import { getCollection } from 'astro:content' ;
const posts = await getCollection ( 'blog' );
// posts: CollectionEntry<'blog'>[]
With filtering:
const publishedPosts = await getCollection ( 'blog' , ({ data }) => {
return data . draft !== true ;
});
getEntry()
Get a single entry by ID:
import { getEntry } from 'astro:content' ;
const post = await getEntry ( 'blog' , '10-pasos-para-crear-tu-web-profesional' );
// post: CollectionEntry<'blog'> | undefined
render()
Render Markdown/MDX content to a component:
import { render } from 'astro:content' ;
const post = await getEntry ( 'blog' , 'my-post' );
const { Content , headings } = await render ( post );
// Use in template:
< Content />
Type Safety Benefits
IntelliSense Get autocomplete for all frontmatter fields while writing content.
Build-Time Validation Catch missing or incorrect fields before deployment.
Type-Safe Queries TypeScript knows the exact shape of your data when querying.
Refactoring Safety Rename fields with confidence - TypeScript will find all usages.
Example Type Safety
---
import { getCollection } from 'astro:content' ;
const posts = await getCollection ( 'blog' );
// TypeScript knows the exact shape:
posts [ 0 ]. data . title // ✅ string
posts [ 0 ]. data . pubDate // ✅ Date
posts [ 0 ]. data . tags // ✅ string[] | undefined
posts [ 0 ]. data . invalid // ❌ TypeScript error!
---
Best Practices
Always define schemas
Even for simple collections, schemas prevent errors and provide IntelliSense: // ❌ Bad - no validation
defineCollection ({
loader: glob ({ base: './src/content/blog' , pattern: '**/*.md' }),
});
// ✅ Good - full type safety
defineCollection ({
loader: glob ({ base: './src/content/blog' , pattern: '**/*.md' }),
schema: z . object ({
title: z . string (),
date: z . coerce . date (),
}),
});
Use z.coerce for dates
Dates in frontmatter are strings. Use z.coerce.date() for automatic conversion: pubDate : z . coerce . date () // ✅ Converts "2026-03-05" to Date object
pubDate : z . date () // ❌ Will fail - frontmatter is string
Mark optional fields
Use .optional() for fields that may not exist: schema : z . object ({
title: z . string (), // Required
tags: z . array ( z . string ()). optional (), // Optional
})
Filter in queries
Filter drafts and sort in your query, not in the template: // ✅ Good - filter early
const posts = ( await getCollection ( 'blog' ))
. filter ( p => ! p . data . draft )
. sort (( a , b ) => b . data . pubDate - a . data . pubDate );
// ❌ Bad - filter in template
const posts = await getCollection ( 'blog' );
// Then filter in template with {#if}
Adding a New Collection
To add a new content collection:
Create the directory
mkdir src/content/testimonials
Define the schema
const testimonials = defineCollection ({
loader: glob ({
base: './src/content/testimonials' ,
pattern: '**/*.{md,json}'
}),
schema: z . object ({
author: z . string (),
company: z . string (),
rating: z . number (). min ( 1 ). max ( 5 ),
text: z . string (),
date: z . coerce . date (),
}),
});
export const collections = {
blog ,
promoSingle ,
promoPro ,
promoTienda ,
testimonials , // Add new collection
};
Add content files
src/content/testimonials/john-doe.json
{
"author" : "John Doe" ,
"company" : "Example Corp" ,
"rating" : 5 ,
"text" : "Excellent service!" ,
"date" : "2026-03-01"
}
Query in components
---
import { getCollection } from 'astro:content' ;
const testimonials = await getCollection ( 'testimonials' );
---
{ testimonials . map ( t => (
< blockquote >
< p > { t . data . text } </ p >
< cite > — { t . data . author } , { t . data . company } </ cite >
</ blockquote >
)) }
Next Steps
Component Architecture Learn how components consume and display content
Astro Framework Understand how Astro processes content collections
Further Reading
Content Collections Guide Official Astro documentation on Content Collections