Guía de productos · El precio

El precio: público o bajo cotización, sin inventar

El precio es un campo opcional y de texto libre. Si hay tarifa pública, se muestra; si no, el sistema usa «bajo cotización» por WhatsApp en lugar de inventar una cifra —y el schema emite un Offer honesto, no un número falso—.

Es la cuarta decisión al crear un producto: cuánto cuesta —o cómo se cotiza—. Y es donde muchos catálogos tropiezan: rellenan el precio con un «$0» o una cifra de relleno para no dejar el campo vacío. Esta plantilla toma la decisión contraria: el precio es opcional, y omitirlo activa un modelo de cotización honesto, no un placeholder.

Detrás hay un modelo de negocio: WhatsApp-first, sin carrito. El siguiente paso de un producto es una conversación, no un checkout. El CTA se arma con waUrl() y un mensaje que ya nombra el producto, el número vive en site.ts (nunca a mano), y el Offer del schema refleja la realidad —precio real o «bajo cotización»—, lo que protege la confianza y evita penalizaciones.

Definición

¿Qué es el precio de un producto?

El campo price del frontmatter: opcional y de texto libre. Si está, se muestra; si se omite, el sistema usa «Precio bajo cotización» y el siguiente paso es WhatsApp. El precio también viaja al schema como un Offer honesto.

El precio es el campo price del frontmatter, y dos decisiones lo definen: es opcional y es un string. Opcional porque no todo producto tiene tarifa pública; string porque la realidad comercial no cabe siempre en un número —«Desde $450», «$1,299 + IVA», «Cotizar» son todas respuestas válidas que un campo numérico no permitiría—.

Cuando el precio está, la card y la ficha lo muestran tal cual lo escribiste. Cuando se omite, el sistema no lo trata como un error ni lo rellena: muestra «Precio bajo cotización» y enfoca al visitante hacia WhatsApp. Y en ambos casos, el precio llega al JSON-LD como un Offer que refleja la realidad, sin fabricar cifras.

Función e importancia

¿Para qué sirve?

Comunica el costo sin mentir: muestra el precio real cuando existe, y «bajo cotización» cuando no, en lugar de inventar. Encauza el siguiente paso a WhatsApp con contexto, y modela un Offer honesto para el buscador.

Su función es resolver la pregunta «¿cuánto cuesta?» de la forma más honesta posible. Para productos con tarifa pública, mostrarla reduce fricción. Para los que se cotizan —la mayoría en B2B—, fingir un precio es peor que no tenerlo: el sistema dice «bajo cotización» y convierte la duda en una conversación, que es donde se cierra la venta consultiva.

Y protege dos cosas a la vez. La confianza del visitante: un precio inventado que luego cambia quema al lead antes de hablar. Y la salud del SEO: el Offer del schema refleja la realidad (precio real o «bajo cotización»), evitando la penalización de Google por datos estructurados que no coinciden con la página. Honestidad que además es técnicamente correcta.

Honestidad por defecto

Sin precio público, el sistema no rellena con un «$0» ni con una cifra inventada: dice «bajo cotización» y abre la conversación. Esa honestidad protege la confianza del visitante y evita la penalización de Google por datos estructurados que no coinciden con la realidad de la página.

WhatsApp-first, sin carrito

El modelo del cluster es conversacional: el siguiente paso de un producto no es «añadir al carrito», es «cotizar por WhatsApp». El CTA se arma con waUrl() y un mensaje que ya nombra el producto, así el lead llega con contexto y el asesor responde sin preguntar qué quiere.

Flexible para B2B

Muchos productos no tienen una tarifa pública única: dependen de volumen, configuración o entrega. El precio como string opcional cubre todos los casos —«Desde $X», «Cotizar», un rango— sin forzar un número que tendrías que inventar. Ideal para catálogos industriales y mayoristas.

Anatomía

¿Qué lo compone?

Cuatro piezas: el campo price (opcional, string), el fallback «bajo cotización» cuando se omite, el CTA a WhatsApp armado con waUrl(), y el Offer honesto que viaja al schema con o sin cifra.

Cada pieza cubre un escenario. El campo expresa el precio cuando lo hay; el fallback responde con dignidad cuando no; el CTA convierte en ambos casos hacia WhatsApp con contexto; y el Offer lleva todo al schema sin mentir. Juntas, cubren tanto el retail con precio fijo como el B2B por cotización.

Abajo, el ejemplo en vivo —los dos estados del precio, con y sin cifra—. Cada punto numerado se desglosa en su tarjeta: qué resuelve y qué línea del sistema lo respalda.

1

El campo price (opcional, string)

price es opcional y es un STRING libre, no un número forzado: «Desde $450 MXN», «$1,299», «Cotizar». Esa libertad permite expresar la realidad comercial (rangos, «desde», moneda) sin un esquema rígido. Si no lo pones, no es un error: activa el modelo cotización.

Dato price: z.string().optional()

2

El fallback «bajo cotización»

Cuando price se omite, el sistema NO inventa una cifra: la ficha muestra «Precio bajo cotización» y la card se mantiene limpia. Es una decisión de honestidad —mejor pedir el contacto que mostrar un número que no aplica— y el modelo de negocio del cluster (WhatsApp-first, sin carrito).

Dato price ?? "Precio bajo cotización"

3

El CTA a WhatsApp (waUrl)

Con o sin precio, el siguiente paso es cotizar/comprar por WhatsApp. El enlace se arma con waUrl() y un mensaje pre-armado que ya incluye el nombre del producto, para que el asesor entre en materia. NUNCA un wa.me escrito a mano (regla D4): el número vive en site.ts.

Dato waUrl(`Hola, quiero cotizar: ${title}`)

4

El Offer honesto (schema)

El precio llega al JSON-LD sin mentir: con price, el Offer lleva la cifra y priceValidUntil; sin price, emite un UnitPriceSpecification «bajo cotización» en vez de un precio falso. Google penaliza precios inventados; el sistema modela la realidad, tenga o no tarifa pública.

Dato productSchema → Offer (price | "bajo cotización")

Variantes

Formas de expresar el precio

El string libre cubre desde un precio fijo hasta «Desde $X», un rango, condiciones (+ IVA) o el modelo cotización al omitirlo. Todas son válidas; la elección depende de tu realidad comercial.

No hay un único formato de precio: hay un string que dice la verdad de cada producto. Un retail pone la cifra; un configurable pone «Desde»; un B2B lo omite y cotiza; un mayorista aclara «+ IVA». El campo es el mismo; cambia lo que escribes (o si lo escribes).

Abajo, cinco formas —todas configuraciones reales del campo actual—. Cada una con el tipo de negocio donde encaja.

  • Precio público fijo

    Retail · B2C

    price: "$1,299 MXN". La card y la ficha lo muestran; el Offer del schema lleva la cifra y priceValidUntil. Para productos con tarifa pública estable. El caso más directo.

  • «Desde $X» (precio de entrada)

    Configurables · Variantes

    price: "Desde $450 MXN". Comunica el punto de entrada sin comprometer cada variante. Honesto y útil cuando el precio sube según opciones; el detalle se cierra en la cotización.

  • Bajo cotización (sin price)

    B2B · Proyecto

    Omitir price activa «Precio bajo cotización» en la ficha y un Offer UnitPriceSpecification en el schema —sin cifra falsa—. El CTA lleva directo a WhatsApp. Para tarifas por volumen o proyecto.

  • Con condiciones (+ IVA / envío)

    Transparencia · Mayoreo

    price: "$1,299 + IVA". El string libre permite decir la verdad completa en una línea. Evita la sorpresa en la cotización y comunica seriedad. La transparencia es parte del precio.

  • Promoción / oferta

    Campaña · Temporada

    price: "Oferta: $999 (antes $1,299)". El string admite el gancho de promoción. Úsalo con cuidado y con fecha real; una oferta perpetua deja de serlo y resta credibilidad.

Responsive y móvil

El precio y la cotización, en el teléfono

En la ficha móvil, el precio y el botón de cotizar quedan a mano: el sidebar de conversión se apila, el botón es full-width en la zona del pulgar, y el mensaje pre-armado viaja en el enlace de WhatsApp.

En el teléfono, cotizar tiene que estar a un toque. En la ficha L4, el sidebar de conversión (precio + botón WhatsApp) se apila bajo el contenido y el botón se vuelve full-width, cómodo para el pulgar y respetando el safe-area de iOS. El precio —cifra o «bajo cotización»— se lee igual de claro que en escritorio.

Y el mensaje pre-armado es justo donde más rinde en móvil: al tocar el botón, WhatsApp abre con el texto ya escrito, nombrando el producto, sin que el visitante teclee nada. waUrl() codifica ese mensaje en el enlace. Abajo, los dos patrones con su receta.

1 · Precio + CTA full-width en la zona del pulgar

En la ficha móvil, el bloque de conversión se apila y el botón «Cotizar» es full-width, en la zona del pulgar, respetando el safe-area de iOS. El precio (cifra o «bajo cotización») se lee igual de claro.

CSS · precio + CTA en la ficha móvil
/* PRECIO + CTA EN LA FICHA MÓVIL (zona del pulgar)
   En la ficha L4, el sidebar de conversión (precio + botón WhatsApp)
   en móvil se apila y queda accesible; el botón es full-width y
   respeta el safe-area de iOS para no quedar bajo el home indicator. */
.prod-price { font-size: var(--text-xl); font-weight: 700; color: var(--c-primary); }
.btn--block { width: 100%; }
@media (max-width: 560px) {
  .prod-hero__actions .btn { width: 100%; justify-content: center; }
}

2 · El mensaje pre-armado viaja en el enlace

waUrl() codifica el mensaje en el enlace wa.me. Al tocarlo, WhatsApp abre con el texto ya escrito —nombrando el producto—, sin teclear. El número sale de site.ts, nunca a mano.

JS · waUrl() con mensaje pre-armado
/* EL MENSAJE PRE-ARMADO VIAJA EN EL ENLACE (igual en móvil)
   waUrl() codifica el mensaje en el enlace wa.me. Al tocarlo en el
   teléfono, WhatsApp abre con el texto ya escrito: el lead llega
   con el producto nombrado, sin teclear. El número sale de site.ts. */
const waLink = waUrl(`Hola, quiero cotizar: ${title}. ¿Precio y disponibilidad?`)
// → https://wa.me/<numero>?text=Hola%2C%20quiero%20cotizar%3A%20...

Posición

¿Dónde se decide?

El precio se declara en el frontmatter del .md (price, opcional). El fallback y el CTA viven en ProductLayout; el Offer honesto, en lib/seo.ts. El número de WhatsApp, siempre en site.ts.

El precio entra por el frontmatter del producto (price). De ahí, el dato se reparte: ProductLayout decide qué mostrar (la cifra o «bajo cotización») y arma el CTA de WhatsApp con waUrl(); lib/seo.ts construye el Offer del schema según haya o no precio. El producto solo declara el string; la lógica de presentación y de SEO vive en los componentes.

El número de teléfono que usa el CTA NO vive en el producto ni en el layout: vive en CONTACT.whatsapp de site.ts (SSoT), y waUrl() lo formatea. Esa centralización (regla D4) es lo que permite cambiar el número en un solo lugar sin cazar enlaces wa.me por todo el sitio. La pieza «La ficha» cubre el resto del layout; aquí el foco es el precio y su conversión.

Implementación

Cómo se construye

El campo price en el .md (o su ausencia), el fallback + CTA en ProductLayout (price ?? «bajo cotización», waUrl), y el Offer honesto en lib/seo.ts (cifra real o UnitPriceSpecification).

En el contenido, todo es un string opcional: lo pones para mostrar precio, lo omites para cotización. En el layout, una sola expresión decide la presentación —price ?? "Precio bajo cotización"— y el CTA se arma con waUrl() y un mensaje que nombra el producto. No hay ramas complicadas: el operador ?? hace el trabajo.

En el schema, productSchema bifurca: con price, el Offer lleva la cifra y una fecha de validez; sin price, emite un UnitPriceSpecification «bajo cotización» con price "0" pero descrito como cotización —honesto y conformante—. Abajo, las tres recetas: el .md, el layout y el Offer.

Markdown · con precio y sin precio (los dos casos)
---
# Con precio público (string libre — tú pones el formato):
title: "Casco de seguridad industrial NOM-115"
price: "Desde $450 MXN"          # ← string: «Desde», moneda, condiciones…
# ...
---

# ──────────────────────────────────────────────
# Sin precio → modelo «bajo cotización» (omite el campo):
---
title: "Sistema de protección perimetral a medida"
# (sin 'price') → la ficha muestra "Precio bajo cotización"
# ...
---
ProductLayout · el fallback y el CTA (waUrl)
// src/layouts/ProductLayout.astro — el fallback y el CTA (extracto real).
import { waUrl } from '@config/site'

// El CTA se arma con waUrl() + mensaje pre-armado que nombra el producto.
// NUNCA un wa.me a mano (regla D4): el número vive en site.ts.
const waLink = waUrl(`Hola, quiero cotizar: ${title}. ¿Precio y disponibilidad?`)
---

<!-- Precio: la cifra si existe; si no, "bajo cotización" (sin inventar) -->
<p class="prod-price">{price ?? "Precio bajo cotización"}</p>

<a class="btn btn--primary" href={waLink}>Cotizar por WhatsApp</a>
lib/seo.ts · el Offer honesto (con/sin precio)
// src/lib/seo.ts — el Offer HONESTO (extracto real de productSchema).
const offer = {
  '@type': 'Offer',
  url, priceCurrency: 'MXN',
  availability: 'https://schema.org/InStock',
  seller: { '@id': BUSINESS_ID },
}

if (p.price) {
  offer.price = p.price                                   // precio real
  offer.priceValidUntil = `${new Date().getFullYear() + 1}-12-31`
} else {
  // SIN precio → NO se inventa una cifra: se declara "bajo cotización".
  offer.price = '0'
  offer.priceSpecification = {
    '@type': 'UnitPriceSpecification',
    price: '0', priceCurrency: 'MXN',
    description: 'Precio bajo cotización — contáctanos para tarifa personalizada.',
  }
}

En concreto: price es un z.string().optional(). En la ficha, {price ?? "Precio bajo cotización"} resuelve la presentación en una línea; el CTA usa waUrl(`Hola, quiero cotizar: ${title}…`), que codifica el mensaje y usa el número de site.ts (regla D4, jamás un wa.me a mano).

En productSchema (lib/seo.ts), si hay price el Offer lleva la cifra y priceValidUntil; si no, emite un UnitPriceSpecification con price: '0' pero descrito como «bajo cotización». Así el dato estructurado refleja la realidad de la página —nunca un precio fabricado— y se mantiene conformante con schema.org.

Buenas prácticas

Qué hacer y qué evitar

El precio honesto cabe en unos hábitos: string descriptivo cuando lo hay, omitirlo cuando no, CTA con waUrl() y precios actualizados. La regla de oro: nunca inventar una cifra.

Ninguno de estos hábitos es capricho: salen del modelo WhatsApp-first y de la regla de honestidad del schema. El sistema te respalda —el fallback «bajo cotización» y el Offer honesto son automáticos—; lo que queda a tu disciplina es no inventar precios, usar waUrl() y mantener las cifras al día.

La buena noticia: omitir el precio es una decisión válida y completa, no un hueco. Y el CTA se arma solo con el número correcto. Lo único que no debes hacer es rellenar con cifras falsas o enlaces wa.me a mano. Abajo, lo que conviene y lo que conviene evitar.

Sí conviene

  • Usa price como string descriptivo: «Desde $450 MXN», «$1,299 + IVA». Refleja la realidad comercial, no solo un número pelado.
  • Omite price cuando la tarifa no es pública o depende del proyecto: el sistema activa «bajo cotización» y el Offer honesto, sin que inventes nada.
  • Deja que el CTA de WhatsApp se arme con waUrl() y un mensaje que nombre el producto: el lead llega con contexto y sube la calidad de la conversación.
  • Si pones precio, manténlo actualizado: un «Desde $450» viejo que ya no aplica es peor que «bajo cotización». Revísalo al actualizar el catálogo.
  • Para productos con IVA, envío o condiciones, dilo en el string («+ IVA», «entrega aparte»): la transparencia evita sorpresas en la cotización.

Mejor evita

  • NO inventes un precio para «llenar el campo»: si no hay tarifa pública, omítelo. Una cifra falsa rompe la confianza y arriesga una penalización de schema.
  • NO hardcodees un enlace wa.me con el número escrito a mano (regla D4): usa waUrl(). El número vive en site.ts; un wa.me suelto se desincroniza al cambiarlo.
  • NO metas el precio como número (450) esperando formato automático: el campo es string libre a propósito. El formato («$», «MXN», «Desde») lo pones tú.
  • NO pongas «$0» ni «Gratis» para significar «cotización»: el sistema ya tiene «bajo cotización». «$0» en el Offer comunica algo distinto (y falso).
  • NO dejes precios desactualizados en producción: revisa las cifras al refrescar el catálogo. Un precio viejo es una promesa que no vas a cumplir.
¿Necesitas ayuda?