Skip to main content
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:
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

src/content.config.ts
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:
pubDate: '2025-09-13'
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: string[]Keywords for categorization and SEO:
tags: ["web design", "tutorial", "SEO"]
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.

Promotion Collections

The three promotion collections (promoSingle, promoPro, promoTienda) share a common schema:

Promotion Schema

src/content.config.ts
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
});

Example Promotion

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."
---

Using Promotion Data

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

1

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(),
  }),
});
2

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
3

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
})
4

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:
1

Create the directory

mkdir src/content/testimonials
2

Define the schema

src/content.config.ts
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
};
3

Add content files

src/content/testimonials/john-doe.json
{
  "author": "John Doe",
  "company": "Example Corp",
  "rating": 5,
  "text": "Excellent service!",
  "date": "2026-03-01"
}
4

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

Build docs developers (and LLMs) love