Guía de servicios · La colección

La colección: cómo nace un servicio

Crear un servicio en este sitio no es tocar código: es escribir un archivo Markdown. Cada .md es un servicio; un esquema Zod .strict() valida su frontmatter en build-time, y la ficha y el schema se generan solos.

Esta es la pieza-raíz de la guía. Antes de hablar de copy, alcance, proceso, objeciones o conversión, conviene fijar qué es un servicio como DATO: una entrada en una Content Collection, no markup dibujado a mano. Esa decisión —contenido en colecciones, nunca hardcodeado— es la regla D1 del sistema.

Con una consecuencia que conviene marcar de entrada: el contenido y el diseño viven separados. El .md dice QUÉ es el servicio; el componente dice CÓMO se ve. Quien edita el catálogo escribe texto en Markdown; quien mantiene el diseño toca un componente, una vez, para todos los servicios.

Definición

¿Qué es la colección de servicios?

La carpeta src/content/servicios/ donde cada archivo Markdown es un servicio. Astro la carga como Content Collection y valida su frontmatter contra un esquema Zod .strict() en build-time.

La colección de servicios es el conjunto de archivos .md que viven en src/content/servicios/. Cada archivo es un servicio: su frontmatter (la cabecera entre ---) declara los datos —título, descripción, categoría, imagen, pricing, includes, FAQs…— y su cuerpo Markdown es la descripción larga que se renderiza en la ficha. Astro 6 los carga con un loader glob y los expone con getCollection.

Lo que la vuelve una colección y no «unos archivos sueltos» es el esquema: un objeto Zod .strict() definido en src/content.config.ts que valida CADA servicio cuando se compila el sitio. El esquema es el contrato —qué campos existen, de qué tipo, qué rango— y .strict() es la garantía de que nadie cuela un campo inventado. Si un .md no cumple, el build falla con un mensaje claro.

Función e importancia

¿Para qué sirve?

Hace tres trabajos a la vez: baja la barrera de crear un servicio a «saber escribir», valida el contenido en build (no en producción) y convierte un solo .md en la card, la ficha, el schema y el FAQ.

Su función es separar el contenido del código. En un sitio sin colecciones, agregar un servicio significa copiar markup, con el riesgo de romper el diseño en cada copia. Con la colección, agregar un servicio es crear un .md: la landing y la ficha se regeneran solas. Quien escribe no necesita saber Astro; el sistema se encarga del resto.

Y por eso es la pieza-raíz: el mismo archivo alimenta cuatro salidas —la card del catálogo, la ficha L3, el schema Service y el FAQPage— sin duplicar el dato. La validación Zod atrapa los errores cuando se compila el sitio, no cuando un cliente abre una ficha rota.

Crear ≠ programar

Para sumar un servicio escribes un archivo Markdown, no editas un .astro. Quien mantiene el catálogo trabaja con texto; quien mantiene el diseño toca el componente una sola vez para todos los servicios. La barrera de entrada baja de «saber Astro» a «saber escribir».

Validación en build, no en producción

El esquema Zod .strict() revisa cada .md cuando se compila el sitio: campos faltantes, tipos equivocados, categorías inventadas o una imagen fuera de /images/ rompen el build con un mensaje claro —no se publican en silencio—.

Una fuente, muchas salidas

El mismo .md alimenta la card en el catálogo, la ficha de detalle (L3), el schema Service y el FAQPage. No se duplica el dato: se escribe una vez y cada vista lo lee. Cambiar el título cambia todo a la vez.

Anatomía

¿Qué lleva el frontmatter?

Cinco grupos de campos: identidad, clasificación, imagen, alcance+pricing y objeciones+relacionados. Cuatro campos son obligatorios; el resto enriquecen la ficha si vienen.

El frontmatter es la cabecera del .md entre dos líneas de tres guiones. Cada campo cumple un papel y Zod lo vigila: los cuatro obligatorios (title, description, category, image) garantizan que ningún servicio entre incompleto al catálogo; los opcionales (includes, pricing, faqs, relatedServices, seoFields) enriquecen la ficha solo si vienen.

Abajo, el ejemplo en vivo —un frontmatter real anotado—. Cada punto numerado se desglosa en su tarjeta.

1

Identidad — title + description

El nombre y la descripción corta del servicio. Zod exige title de 10–110 caracteres y description de 70–280. Ese rango fuerza títulos escaneables y descripciones que dicen algo sin ser un párrafo entero.

Dato title: z.string().min(10).max(110) · description: 70–280

2

Clasificación — category + order + draft

Dónde encaja y cómo se ordena. category es un enum CERRADO (instalacion · mantenimiento · general); order define la posición en la landing; draft:true lo oculta (borrador). Controlan el catálogo sin tocar código.

Dato category: z.enum(SERVICE_CATEGORIES) · order · draft · featured

3

Imagen — image

La foto del servicio. Obligatoria y validada con una regex: ruta absoluta bajo /images/. Sin imagen válida, el build falla antes de publicar. AVIF ligero, alt descriptivo con la keyword del servicio.

Dato image: imagePath (regex ^/images/) — OBLIGATORIA

4

Alcance + pricing — includes[] + pricing

Qué incluye el servicio (lista de entregables) y cómo se cotiza. includes[] es el array de items de la sección «Qué incluye»; pricing.note es la nota de precio honesta (no un número forzado). Ambos opcionales.

Dato includes?: string[] · pricing?: { unit?, note? }

5

Objeciones + relacionados — faqs[] + related

Las preguntas frecuentes del servicio y el interlinking. faqs[] alimenta el FAQAccordion y el schema FAQPage. relatedServices/relatedProducts usan reference() tipado: un slug roto falla el build.

Dato faqs?: { question, answer }[] · relatedServices/relatedProducts: reference[]

Variantes

Configuraciones del frontmatter

Un servicio puede ir desde lo mínimo (cuatro campos) hasta una ficha rica con alcance, FAQs y relacionados. Todas son configuraciones del esquema actual: se obtienen poniendo o quitando campos opcionales.

No hay un único molde: hay un esquema con cuatro campos obligatorios y varios opcionales que cada servicio activa según lo que necesite. Un servicio simple va al mínimo; uno estratégico lleva includes, FAQs y relacionados.

Abajo, seis configuraciones —todas válidas hoy—. Cada una con el tipo de servicio donde rinde mejor.

  • title · description category · image — nada opcional —

    Mínimo viable

    Servicio simple · Arranque

    Los cuatro campos obligatorios: title, description, category e image. Genera una card y una ficha válidas con el modelo «bajo cotización». El punto de partida de cualquier servicio.

  • title · description category · image includes[]

    Con alcance (includes[])

    Servicio con entregables

    Suma includes[] con la lista de entregables concretos. La ficha pinta la sección «Qué incluye» con checkmarks. Para servicios donde el cliente necesita saber exactamente qué recibe antes de escribir.

  • title · description category · image pricing.note

    Con pricing note

    Precio transparente

    Añade pricing.note con una nota de precio honesta («Bajo cotización según el alcance»). La ficha la muestra sin inventar una cifra fija. Para servicios con tarifa variable por proyecto.

  • title · description category · image faqs[]

    Con FAQs

    Manejo de objeciones

    faqs[] de pregunta/respuesta. La ficha pinta el FAQAccordion y el schema emite FAQPage. Resuelve las objeciones más comunes antes de que lleguen por WhatsApp.

  • title · description category · image relatedServices[]

    Con relacionados

    Interlinking · Cross-sell

    relatedServices/relatedProducts con reference() tipado. Enlaza a otras fichas por slug, validado en build. Reparte autoridad interna y sugiere el siguiente paso natural.

  • title · description category · image draft: true

    Borrador (draft: true)

    En redacción · Roadmap

    draft:true mantiene el servicio FUERA del sitio: no entra al catálogo, no genera ficha, no aparece en el schema. Quitar el draft (o ponerlo en false) lo activa.

Responsive y móvil

El mismo .md, en cualquier pantalla

El frontmatter no tiene breakpoints. Un servicio es un dato; la card y la ficha lo leen y se adaptan solos. El responsive vive en los componentes, no en el contenido.

La gran ventaja de separar contenido y diseño se nota aquí: el frontmatter es agnóstico de la pantalla. El teléfono y el escritorio reciben el mismo servicio —mismo título, misma imagen, mismo includes[]— y cada vista decide cómo presentarlo.

Y el filtrado y el orden (draft, order) viven en getCollection, antes de pintar: el teléfono recibe exactamente los servicios publicados y en el mismo orden. Abajo, el patrón con su receta.

Una fuente, muchas vistas

El .md es el dato; no sabe de breakpoints. La card, la ficha y el schema lo leen y cada uno se adapta. El responsive vive en el componente, no en el contenido.

CSS · responsive en el componente (no en el .md)
/* UNA FUENTE → MUCHAS VISTAS (el mismo .md, en cualquier pantalla).
   El .md es el dato; no sabe de breakpoints.
   El responsive vive en los componentes, no en el contenido. */
.grid { grid-template-columns: 1fr; }
@media (min-width: 768px) {
  .grid { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
}

Posición

¿Dónde vive?

Un archivo .md por servicio en src/content/servicios/. El nombre del archivo es el slug de la URL. El esquema vive en src/content.config.ts; ambos se sincronizan con SERVICES en site.ts.

Cada servicio es un archivo en src/content/servicios/<slug>.md, y ese slug es la URL de su ficha (/servicios/<slug>). Para crear un servicio, creas un archivo; para borrarlo, lo borras. El esquema vive aparte, en src/content.config.ts, y los slugs de su enum SERVICE_CATEGORIES deben coincidir con la taxonomía en site.ts.

Además, el slug del archivo debe coincidir con el id del servicio en el array SERVICES de site.ts, que es la fuente del menú. Si ambos están en sync, el dropdown, la landing y la ficha de detalle apuntan al mismo lugar.

Implementación

Cómo se construye

Tres piezas: el esquema Zod .strict() (content.config.ts), un archivo de servicio (.md con frontmatter) y la lectura con getCollection. El loader glob conecta la carpeta con la colección.

El esquema se define una vez con defineCollection: un loader glob apunta a src/content/servicios/**/*.md y un schema Zod .strict() valida cada campo en build. Crear un servicio es escribir el .md; consumirlo es llamar getCollection('servicios'), filtrar drafts y ordenar por order.

Abajo, las tres recetas reales del sistema.

content.config.ts · esquema Zod .strict()
// src/content.config.ts — esquema Zod .strict() para la colección servicios.
const servicios = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/servicios' }),
  schema: z.object({
    title:       z.string().min(10).max(110),
    description: z.string().min(70).max(280),
    category:    z.enum(['instalacion', 'mantenimiento', 'general']),
    image:       imagePath,                       // OBLIGATORIA (regex /images/)
    pricing: z.object({
      unit: z.enum(['pieza','set','evento','hora','dia','mes','servicio']).optional(),
      note: z.string().optional(),
    }).optional(),
    includes:        z.array(z.string()).optional(),
    relatedServices: z.array(reference('servicios')).optional(),
    relatedProducts: z.array(reference('productos')).optional(),
    faqs:            faqSchema,
    featured:        z.boolean().default(false),
    order:           z.number().default(0),
    draft:           z.boolean().default(false),
    ...seoFields,
  }).strict(),   // ← rechaza cualquier campo desconocido
})
src/content/servicios/consultoria.md
---
# src/content/servicios/consultoria.md  → URL: /servicios/consultoria
title: "Consultoría y diagnóstico para tu proyecto"
description: "Antes de invertir, definimos contigo qué necesitas y por qué.
  Diagnóstico claro, alcance recomendado y propuesta por escrito — sin venderte de más."
category: "general"           # enum cerrado: instalacion | mantenimiento | general
image: "/images/servicios/consultoria-desarrollo-web-astro.avif"
pricing:
  unit: "servicio"
  note: "Bajo cotización según el alcance. Primera asesoría sin compromiso."
includes:
  - "Diagnóstico de tu necesidad y objetivos"
  - "Recomendación de alcance en lenguaje claro"
  - "Propuesta por escrito con tiempos y costo"
faqs:
  - question: "¿La asesoría tiene costo?"
    answer: "La primera asesoría es sin compromiso."
featured: true
order: 1
seoTitle: "Consultoría web | diagnóstico honesto"
seoDescription: "Diagnóstico claro, alcance recomendado y propuesta por escrito.
  Asesoría honesta, sin venderte de más."
---

## Consultoría: decidir bien antes de invertir

Cuerpo en **Markdown**: se renderiza en la ficha de detalle.
Astro · leer la colección
---
// Cualquier página lee la colección con getCollection.
import { getCollection } from 'astro:content'

// Filtra borradores y ordena por 'order'.
const servicios = (await getCollection('servicios', ({ data }) => !data.draft))
  .sort((a, b) => a.data.order - b.data.order)
---

{servicios.map((s) => (
  <ServiceCard
    label={s.data.title}
    href={`/servicios/${s.id}`}
    image={s.data.image}
    desc={s.data.description}
  />
))}

defineCollection recibe un loader: glob(...) que carga todos los .md y un schema cerrado con .strict(). Un campo mal escrito, una categoría fuera del enum o una imagen sin /images/ detienen la compilación con la ruta del archivo y el campo culpable. Al consumir, getCollection('servicios', filtro) devuelve un array tipado donde cada entrada trae id (el slug) y data (el frontmatter validado).

Buenas prácticas

Qué hacer y qué evitar

La colección valida sola cuando se respeta el esquema. El detalle a vigilar: mantener en sync la colección (dato) con SERVICES en site.ts (menú).

Casi todo se sostiene solo cuando se respeta la colección y el esquema Zod. La única sincronía manual es la que existe entre la colección (archivo .md) y el array SERVICES de site.ts (la fuente del menú): si añades un servicio a la colección pero no a SERVICES, la ficha existe pero el menú no lo enlaza.

Abajo, lo que conviene y lo que conviene evitar, enfrentados.

Sí conviene

  • Crea UN archivo .md por servicio en src/content/servicios/. El nombre del archivo es el slug de la URL (/servicios/<slug>).
  • Respeta los rangos de Zod: title 10–110, description 70–280. Si el build se queja, ajusta el texto — no relajes el esquema sin razón.
  • Usa draft:true mientras escribes un servicio incompleto: queda fuera del sitio hasta que lo publiques.
  • Para que el servicio aparezca en el MENÚ y la landing /servicios, también añádelo al array SERVICES en src/config/site.ts (SSoT del dropdown).
  • Usa el campo order para controlar la posición en la landing. Servicios sin order caen al final; reserva 1, 2, 3… para los que van primero.

Mejor evita

  • NO hardcodees un servicio dentro de un .astro: rompe la regla D1 (toda entidad repetible vive en una colección) y se queda fuera del schema.
  • NO inventes campos en el frontmatter: con .strict() un campo desconocido falla el build, no se ignora en silencio.
  • NO uses una categoría fuera del enum SERVICE_CATEGORIES: Zod la rechaza. Si necesitas una nueva, añádela al enum en content.config.ts.
  • NO dejes la imagen apuntando a una ruta relativa o fuera de /images/: la regex la rechaza. Siempre ruta absoluta bajo /images/.
  • NO confundas la colección con el dropdown del Header: son dos cosas distintas. La colección es el dato; SERVICES en site.ts es la fuente del menú. Ambas deben estar en sync.

Continúa con la guía

Los siguientes pasos del flujo de crear una página de servicio.

¿Necesitas ayuda?