Guía de productos · Las categorías

Las categorías: organizar el catálogo sin fragmentarlo

Cada producto declara su categoría, y el campo no es texto libre: es un enum cerrado (equipos · accesorios · general). Una decisión deliberada que evita el SEO fragmentado y mantiene el menú, las rutas y el contenido sincronizados.

Es la segunda decisión al crear un producto: dónde encaja. Un catálogo se organiza por categorías, y la tentación es dejarlas como texto libre —hasta que, con el tiempo, aparecen «Guías», «Guias» y «guía» como tres ramas distintas que fragmentan el SEO y rompen los filtros. El enum cerrado lo impide de raíz: obliga a elegir de una lista fija que Zod valida en build.

Y va más allá del catálogo: los slugs del enum se sincronizan con la taxonomía en site.ts, la fuente del menú y las rutas. Esa coincidencia es la que mantiene la navegación alineada con el contenido. El interlinking entre fichas (relatedProducts) usa referencias tipadas que Astro valida —un enlace roto rompe el build, no se publica—.

Definición

¿Qué es la categoría de un producto?

El campo category del frontmatter: un valor de un enum CERRADO (equipos · accesorios · general) que clasifica el producto. Zod lo valida en build; los slugs se sincronizan con la taxonomía del sitio.

La categoría es la rama del catálogo a la que pertenece un producto. En esta plantilla se declara en el frontmatter (category: "equipos") y NO admite cualquier texto: es un z.enum() definido en content.config.ts con una lista fija. Cuando se compila el sitio, Zod comprueba que el valor esté en esa lista; si no, el build falla con el archivo y el valor culpables.

Esa restricción es la diferencia entre una taxonomía sana y una que se degrada. Sin enum, cada quien escribe la categoría a su manera y el catálogo termina con variantes tipográficas de lo mismo. Con enum, la categoría es siempre el mismo string, sincronizado además con TAXONOMY en site.ts para que el menú y las rutas hablen el mismo idioma que el contenido.

Función e importancia

¿Para qué sirve?

Tres trabajos: evita el SEO fragmentado por variantes de una misma categoría, mantiene sincronizados el menú y las rutas con el contenido, y habilita un interlinking validado en build entre fichas.

Su función es ordenar el catálogo de forma estable. Las categorías agrupan productos para que el visitante navegue («quiero accesorios») y para que el buscador entienda la estructura. El enum cerrado garantiza que ese agrupamiento sea consistente: diez productos de «equipos» comparten exactamente la misma categoría, no diez variaciones del mismo concepto.

Y conecta el catálogo con el resto del sitio. Como los slugs del enum coinciden con la taxonomía de site.ts, el menú «Productos», las migas de pan y las rutas se alimentan de la misma verdad. El interlinking (relatedProducts/relatedServices) cierra el círculo: enlaza fichas relacionadas con referencias tipadas que Astro valida, repartiendo autoridad interna sin enlaces rotos.

Cero fragmentación de SEO

Un campo de texto libre genera, con el tiempo, «Guías» vs «Guias» vs «guía»: tres categorías que Google ve como distintas, dividiendo la autoridad y rompiendo los filtros. El enum cerrado obliga a elegir de una lista; la categoría es siempre el mismo string, letra por letra.

Navegación que no se desincroniza

Como los slugs del enum coinciden con TAXONOMY en site.ts, el menú, las migas de pan y las rutas se generan de la misma fuente que la categoría del producto. Renombrar o añadir una categoría es un cambio en un sitio, no una cacería de strings por todo el código.

Interlinking validado en build

Las relaciones entre fichas (relatedProducts) usan reference(), no URLs a mano. Astro comprueba en build que cada slug referenciado exista: un enlace a un producto borrado rompe la compilación, no se publica como enlace muerto. El grafo interno se mantiene íntegro solo.

Anatomía

¿Qué la compone?

Cuatro piezas: el enum cerrado (la lista fija), la sincronía con TAXONOMY en site.ts, el badge que la muestra en la card, y el interlinking con reference() entre fichas relacionadas.

Cada pieza cumple un papel. El enum define el universo de categorías válidas; la sincronía con site.ts las conecta con el menú y las rutas; el badge las hace visibles en la vitrina; y reference() las teje entre sí. Juntas convierten un campo de texto en una taxonomía con garantías.

Abajo, el ejemplo en vivo —el enum y los badges que produce—. Cada punto numerado se desglosa en su tarjeta: qué resuelve y qué línea del esquema lo respalda.

1

El enum cerrado

category no es texto libre: es z.enum(PRODUCT_CATEGORIES), una lista fija (equipos · accesorios · general) en content.config.ts. Zod rechaza cualquier valor fuera de ella en build. Es la decisión que evita que «equipos», «Equipos» y «equipo» convivan como tres categorías distintas.

Dato category: z.enum(PRODUCT_CATEGORIES)

2

La sincronía con site.ts

Los slugs del enum deben coincidir con TAXONOMY.categories en site.ts (la fuente del menú, el footer y las rutas). Esa coincidencia es la que mantiene navegación y contenido alineados: si el enum dice «equipos», el menú y las rutas también.

Dato TAXONOMY.categories ↔ PRODUCT_CATEGORIES

3

El badge en la card

En la vitrina, la categoría se muestra como badge sobre la imagen de la ProductCard (p. data.category). Da contexto de un vistazo —«Equipos», «Accesorios»— sin abrir la ficha, y mantiene la rejilla legible cuando hay categorías mezcladas.

Dato badge={p.data.category} → ProductCard

4

El interlinking — reference()

relatedProducts/relatedServices enlazan fichas por slug con reference() tipado: Astro valida en build que el destino exista (un slug roto rompe el build). Reparte autoridad interna entre productos de la misma familia y sugiere el siguiente paso.

Dato relatedProducts: z.array(reference("productos"))

Variantes

Estrategias de categorización

La del sitio es la canónica (tres categorías amplias), pero la estrategia se adapta al catálogo: pocas ramas anchas, sub-fichas L4 para variantes, una sola categoría, badge de norma o filtrado navegable.

No hay un número mágico de categorías: hay un principio —pocas y amplias, profundidad por jerarquía no por más ramas—. Un catálogo simple vive con tres; uno grande mantiene esas tres y baja la granularidad a sub-fichas; un mono-producto usa una. El enum cerrado es el mismo; cambia cómo se usa.

Abajo, cinco estrategias —todas compatibles con el esquema actual—. Cada una con el tipo de catálogo donde rinde mejor.

  • Tres categorías amplias (default)

    Catálogo simple · Recomendado

    El enum del sitio: equipos · accesorios · general. Pocas ramas anchas que cubren todo el catálogo y orientan sin saturar. El punto de partida para casi cualquier negocio; se amplía solo cuando el catálogo lo pide.

  • Categoría + sub-ficha (L4)

    Catálogo grande · Variantes

    Cuando un producto se subdivide (color, talla, modelo), la categoría se mantiene amplia y la granularidad baja a sub-fichas L4. El enum no se infla: la profundidad la da la jerarquía, no más categorías.

  • Una sola categoría (general)

    Mono-producto · Servicios

    Negocios con un solo tipo de producto usan general para todo. El enum sigue cerrado (una entrada), el badge se vuelve redundante y se puede ocultar. Válido y limpio para catálogos pequeños y homogéneos.

  • Badge de norma en vez de categoría

    Industrial · Certificaciones

    En catálogos donde la NORMA pesa más que la categoría (NOM-115, NFPA), el badge puede mostrar la certificación en lugar de la categoría. La categoría sigue clasificando por dentro; el badge comunica lo que vende.

  • Filtrado por categoría

    Catálogo navegable · UX

    Con el enum cerrado, filtrar el grid por categoría es trivial y seguro: getCollection + filter por data.category. Como los valores son fijos, los chips de filtro nunca se desincronizan del contenido real.

Responsive y móvil

Las categorías, en el teléfono

La categoría se comunica igual en cualquier pantalla: el badge sobre la card y, si el catálogo lo necesita, chips de filtro en la zona del pulgar. Como el enum es fijo, esos chips nunca se desincronizan.

En el teléfono la categoría sigue cumpliendo su trabajo de orientar. El badge vive en la zona de la imagen de la card —tamaño legible, sin encimarse al título— y viaja con ella al bajar a una columna. No hay lógica especial de móvil: es el mismo badge, la misma posición.

Y cuando el catálogo ofrece filtrar por categoría, los chips se generan del enum cerrado, así que la lista de filtros nunca difiere del contenido real. En móvil van en una fila scrollable en la zona del pulgar. Abajo, los dos patrones con su receta.

1 · El badge de categoría en la card

El badge se posiciona sobre la imagen 16:9 de la ProductCard. En el teléfono mantiene tamaño legible y viaja con la card al bajar a una columna —misma posición, sin lógica de móvil—.

CSS · badge de categoría sobre la card
/* EL BADGE DE CATEGORÍA EN LA CARD (móvil y escritorio, igual)
   El badge se posiciona sobre la imagen 16:9; en el teléfono mantiene
   tamaño legible y no se encima al título (vive en la zona de la foto). */
.pcard__badge {
  position: absolute; top: var(--sp-3); left: var(--sp-3);
  font-size: var(--text-xs); text-transform: uppercase;
  color: #fff; background: var(--c-primary);
  padding: .25em .55em; border-radius: var(--radius-sm);
}

2 · Chips de filtro en la zona del pulgar

Como las categorías son un enum fijo, los chips de filtro se generan de esa lista y nunca se desincronizan del contenido. En móvil van en una fila scrollable, cómoda para el pulgar.

CSS · chips de filtro scrollables (móvil)
/* CHIPS DE FILTRO POR CATEGORÍA (zona del pulgar en móvil)
   Como las categorías son un enum fijo, los chips se generan de esa
   lista y nunca se desincronizan. En móvil van en fila scrollable. */
.filtros { display: flex; gap: var(--sp-2); overflow-x: auto; scrollbar-width: none; }
.filtros::-webkit-scrollbar { display: none; }
.filtro { flex: 0 0 auto; min-height: 40px; padding: 0 var(--sp-4);
  border-radius: var(--radius-full); border: 1px solid var(--c-border); }
.filtro[aria-pressed="true"] { background: var(--c-primary); color: #fff; }

Posición

¿Dónde se define?

El enum vive en src/content.config.ts (lo valida Zod); la taxonomía espejo, en src/config/site.ts (la consume el menú y las rutas). El producto solo declara su category en el frontmatter.

La categoría tiene dos hogares que deben coincidir. La lista válida (el enum PRODUCT_CATEGORIES) vive en content.config.ts, junto al esquema que valida los productos. La taxonomía que consume el chrome (menú, footer, rutas) vive en TAXONOMY de site.ts. Mantener los slugs idénticos entre ambos es lo que evita que el menú prometa una categoría que el contenido no tiene, o al revés.

El producto, por su parte, no repite la lista: solo declara su category en el frontmatter, y Zod la valida contra el enum. Añadir una categoría es un cambio en dos archivos coordinados (el enum y la taxonomía), no en cada .md. La pieza «La colección» cubre el frontmatter; aquí el foco es la taxonomía que lo respalda.

Implementación

Cómo se construye

El enum cerrado en content.config.ts, su espejo en TAXONOMY de site.ts, y el consumo: filtrar o agrupar el catálogo por categoría con getCollection. El badge sale de p.data.category en la ProductCard.

El enum se declara como un array as const y se pasa a z.enum(). El as const le da a TypeScript los tipos literales —autocompletado de las categorías válidas en el editor—; z.enum() le da a Zod la validación en build. Esa doble garantía (tipo + runtime) es lo que hace que una categoría inventada se atrape antes de publicar.

Consumir es trivial y seguro: getCollection devuelve cada producto con data.category ya validada, así que filtrar (data.category === 'equipos') o agrupar (Object.groupBy) no necesita defensas contra valores inesperados —no los hay—. Abajo, las tres recetas: el enum, su sincronía con site.ts y el filtrado.

content.config.ts · el enum cerrado + reference()
// src/content.config.ts — el enum CERRADO (la lista fija de categorías).
export const PRODUCT_CATEGORIES = [
  'equipos',
  'accesorios',
  'general',
] as const   // ← 'as const' = tipos literales para autocompletado

const productos = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/productos' }),
  schema: z.object({
    // ...
    category: z.enum(PRODUCT_CATEGORIES),   // SOLO acepta los 3 valores de arriba
    relatedProducts: z.array(reference('productos')).optional(), // interlinking tipado
  }).strict(),
})
site.ts · la taxonomía espejo (sincronizada)
// src/config/site.ts — la MISMA taxonomía, sincronizada (SSoT del menú/rutas).
export const TAXONOMY = {
  categories: [
    { slug: 'productos', label: 'Productos', href: '/productos' },
    { slug: 'servicios', label: 'Servicios', href: '/servicios' },
    { slug: 'blog',      label: 'Blog',      href: '/blog' },
  ],
  // ...
} as const

// REGLA: el 'category' del .md (equipos/accesorios/general) pertenece al enum de
// content.config.ts; los slugs de SECCIÓN (productos/servicios/blog) viven aquí.
// Ambos son enums cerrados: el menú, las rutas y el contenido nunca divergen.
Astro · filtrar y agrupar el catálogo por categoría
---
// Filtrar / agrupar el catálogo por categoría — seguro porque el enum es fijo.
import { getCollection } from 'astro:content'
const todos = await getCollection('productos', ({ data }) => !data.draft)

// Solo una categoría:
const equipos = todos.filter((p) => p.data.category === 'equipos')

// Agrupar por categoría (para secciones del catálogo):
const porCategoria = Object.groupBy(todos, (p) => p.data.category)
---

{Object.entries(porCategoria).map(([cat, items]) => (
  <section>
    <h2>{cat}</h2>
    <div class="grid">
      {items.map((p) => <article>{p.data.title}</article>)}
    </div>
  </section>
))}

En concreto: PRODUCT_CATEGORIES es un array as const que alimenta a la vez z.enum(PRODUCT_CATEGORIES) en el esquema y el autocompletado del editor. Cualquier category fuera de la lista detiene el build con la ruta del archivo. En la vitrina, la ProductCard recibe badge={p.data.category} y lo pinta sobre la imagen.

El interlinking se declara con relatedProducts: z.array(reference('productos')): Astro resuelve cada referencia a una entrada real de la colección y falla el build si el slug no existe. Así el «también te puede servir» de una ficha nunca apunta a un producto borrado. La sincronía con TAXONOMY en site.ts es manual y deliberada: dos listas cortas que se revisan juntas, no una abstracción que las una mágicamente.

Buenas prácticas

Qué hacer y qué evitar

La salud de la taxonomía cabe en un puñado de hábitos: enum cerrado, pocas categorías amplias, slugs sincronizados con site.ts e interlinking con reference(). El resto lo vigila Zod.

Ninguno de estos hábitos es opcional si quieres una taxonomía que aguante el tiempo. El enum cerrado es la base; mantener pocas categorías amplias evita el catálogo de mil ramas vacías; sincronizar con site.ts mantiene la navegación honesta; y reference() mantiene el interlinking íntegro.

La buena noticia es que el sistema atrapa los errores: una categoría inventada o un slug relacionado inexistente rompen el build. Lo único que queda a tu disciplina es la sincronía con TAXONOMY (dos listas que se revisan juntas). Abajo, lo que conviene y lo que conviene evitar.

Sí conviene

  • Define las categorías como un z.enum() cerrado en content.config.ts; manténlas pocas y amplias (equipos, accesorios, general) — son ramas, no etiquetas finas.
  • Sincroniza los slugs del enum con TAXONOMY.categories en site.ts: el mismo string en ambos lados mantiene menú, rutas y contenido alineados.
  • Usa el badge de categoría en la card para orientar de un vistazo; en MAYÚSCULAS y corto («EQUIPOS»), como el resto de badges del sitio.
  • Para relacionar fichas, usa relatedProducts/relatedServices con reference(): Astro valida el slug en build y el interlinking nunca queda roto.
  • Si necesitas granularidad fina (color, talla, modelo), baja a sub-fichas L4 en vez de inflar el enum con decenas de categorías.

Mejor evita

  • NO uses z.string() libre para category: reabre la puerta a variantes tipográficas que fragmentan el SEO y rompen los filtros (el anti-patrón que el enum resuelve).
  • NO cambies un slug del enum sin actualizar TAXONOMY en site.ts (y las rutas/contenido que lo usen): los desincronizas y aparecen 404 o menús que no cuadran.
  • NO metas decenas de categorías «por si acaso»: un catálogo con 30 categorías de 1 producto cada una no orienta a nadie. Pocas y amplias.
  • NO enlaces relacionados con href escrito a mano: pierdes la validación en build de reference() y un slug roto se publica como enlace muerto.
  • NO traduzcas ni acentúes los slugs de forma inconsistente: el slug es técnico (equipos), la etiqueta visible es aparte; no los mezcles.
¿Necesitas ayuda?